Skip to content

Commit 841f717

Browse files
committed
Reject non-pure calls in pure functions
1 parent b873233 commit 841f717

4 files changed

Lines changed: 154 additions & 1 deletion

File tree

src/analyzer.rs

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet};
22

33
use crate::checks;
44
use crate::diagnostic::{Diagnostic, code};
5-
use crate::hir::{DuplicateSymbolKind, Hir, HirTypeKind};
5+
use crate::hir::{CallResolution, DuplicateSymbolKind, Hir, HirTypeKind, ResolvedCalleeKind};
66
use crate::interfaces::CORE_INTERFACES;
77
use crate::lexer::{Token, lex};
88
use crate::syntax::ast::{
@@ -73,6 +73,7 @@ impl Analyzer<'_> {
7373
self.check_duplicate_declarations();
7474
self.check_signature_explicitness();
7575
self.check_generic_constraints();
76+
self.check_pure_bodies();
7677
self.check_noalloc_allocations();
7778
self.check_try_operator_result_returns();
7879
self.check_resource_fields();
@@ -747,6 +748,95 @@ impl Analyzer<'_> {
747748
}
748749
}
749750

751+
fn check_pure_bodies(&mut self) {
752+
let items = self.syntax_program.items.clone();
753+
for item in &items {
754+
let Item::Function(function) = item else {
755+
continue;
756+
};
757+
if !function_has_effect(function, "pure") {
758+
continue;
759+
}
760+
self.check_pure_block(&function.name, &function.body);
761+
}
762+
}
763+
764+
fn check_pure_block(&mut self, function_name: &str, block: &Block) {
765+
for statement in &block.statements {
766+
self.check_pure_stmt(function_name, statement);
767+
}
768+
}
769+
770+
fn check_pure_stmt(&mut self, function_name: &str, statement: &Stmt) {
771+
match statement {
772+
Stmt::Let(stmt) => {
773+
if let Some(value) = &stmt.value {
774+
self.check_pure_expr(function_name, value);
775+
}
776+
}
777+
Stmt::Return(stmt) => {
778+
if let Some(value) = &stmt.value {
779+
self.check_pure_expr(function_name, value);
780+
}
781+
}
782+
Stmt::Expr(value) => self.check_pure_expr(function_name, value),
783+
Stmt::With(stmt) => {
784+
self.check_pure_expr(function_name, &stmt.resource);
785+
self.check_pure_block(function_name, &stmt.body);
786+
}
787+
Stmt::If(stmt) => {
788+
self.check_pure_expr(function_name, &stmt.condition);
789+
self.check_pure_block(function_name, &stmt.then_body);
790+
if let Some(else_body) = &stmt.else_body {
791+
self.check_pure_block(function_name, else_body);
792+
}
793+
}
794+
Stmt::Loop(stmt) => {
795+
if let Some(condition) = &stmt.condition {
796+
self.check_pure_expr(function_name, condition);
797+
}
798+
self.check_pure_block(function_name, &stmt.body);
799+
}
800+
Stmt::Break(_) | Stmt::Continue(_) | Stmt::Unknown(_) => {}
801+
}
802+
}
803+
804+
fn check_pure_expr(&mut self, function_name: &str, expr: &Expr) {
805+
match expr {
806+
Expr::Call {
807+
callee, args, span, ..
808+
} => {
809+
match self.hir.resolve_call(callee) {
810+
CallResolution::Resolved { signature, kind } => {
811+
if !matches!(kind, ResolvedCalleeKind::Constructor { .. })
812+
&& !signature.effects.iter().any(|effect| effect == "pure")
813+
{
814+
self.non_pure_call_diagnostic(function_name, callee, span);
815+
}
816+
}
817+
CallResolution::EnumVariant | CallResolution::Unknown => {}
818+
}
819+
for arg in args {
820+
self.check_pure_expr(function_name, &arg.value);
821+
}
822+
}
823+
Expr::Effect { value, .. } | Expr::Manage { value, .. } | Expr::Try { value, .. } => {
824+
self.check_pure_expr(function_name, value);
825+
}
826+
Expr::Binary { left, right, .. } => {
827+
self.check_pure_expr(function_name, left);
828+
self.check_pure_expr(function_name, right);
829+
}
830+
Expr::Field { base, .. } => self.check_pure_expr(function_name, base),
831+
Expr::Index { base, index, .. } => {
832+
self.check_pure_expr(function_name, base);
833+
self.check_pure_expr(function_name, index);
834+
}
835+
Expr::Closure { body, .. } => self.check_pure_block(function_name, body),
836+
Expr::Ident(_, _) | Expr::Number(_, _) | Expr::String(_, _) | Expr::Unknown(_) => {}
837+
}
838+
}
839+
750840
fn check_duplicate_declarations(&mut self) {
751841
for duplicate in self.hir.duplicate_symbols() {
752842
self.diagnostics.push(
@@ -1126,6 +1216,33 @@ impl Analyzer<'_> {
11261216
),
11271217
);
11281218
}
1219+
1220+
fn non_pure_call_diagnostic(
1221+
&mut self,
1222+
function_name: &str,
1223+
callee: &Callee,
1224+
span: &crate::diagnostic::Span,
1225+
) {
1226+
self.diagnostics.push(
1227+
Diagnostic::error(
1228+
code::INVALID_PURE_EFFECT,
1229+
format!(
1230+
"`{function_name}` is declared pure but calls non-pure function `{}`.",
1231+
callee_display(callee)
1232+
),
1233+
span.clone(),
1234+
"non-pure call in pure function",
1235+
)
1236+
.with_cause(
1237+
"A `pure` function may only call constructors, enum variants, or functions also declared `effects(pure)`.",
1238+
)
1239+
.with_fix(
1240+
"remove_pure_or_call_pure",
1241+
"Remove `pure`, or call only APIs whose signatures are declared `effects(pure)`.",
1242+
"manual",
1243+
),
1244+
);
1245+
}
11291246
}
11301247

11311248
fn resource_pool_namespace_arg(namespace: &str) -> Option<&str> {
@@ -1195,6 +1312,13 @@ fn function_has_effect(function: &crate::syntax::ast::FunctionDecl, effect_name:
11951312
.any(|effect| matches!(effect, EffectDecl::Name(name) if name == effect_name))
11961313
}
11971314

1315+
fn callee_display(callee: &Callee) -> String {
1316+
match callee {
1317+
Callee::Name(name) => name.clone(),
1318+
Callee::Qualified { namespace, name } => format!("{namespace}.{name}"),
1319+
}
1320+
}
1321+
11981322
fn removed_runtime_effect_replacement(effect_name: &str) -> Option<&'static str> {
11991323
match effect_name {
12001324
"io" => Some(

src/hir.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub struct FunctionSig {
4040
pub params: Vec<ParamSig>,
4141
pub return_type: Option<String>,
4242
pub returns_fresh: bool,
43+
pub effects: Vec<String>,
4344
pub retained_params: HashSet<String>,
4445
pub is_builtin: bool,
4546
}
@@ -1412,6 +1413,14 @@ fn function_sig_from_decl(function: &FunctionDecl, is_builtin: bool) -> Function
14121413
params: function.params.iter().map(param_sig_from_decl).collect(),
14131414
return_type: function.return_ty.as_ref().map(type_ref_name),
14141415
returns_fresh: function.returns_fresh,
1416+
effects: function
1417+
.effects
1418+
.iter()
1419+
.filter_map(|effect| match effect {
1420+
EffectDecl::Name(name) => Some(name.clone()),
1421+
EffectDecl::Retains(_) => None,
1422+
})
1423+
.collect(),
14151424
retained_params: function
14161425
.effects
14171426
.iter()
@@ -1561,6 +1570,7 @@ fn constructor_sig_from_type(type_info: &TypeInfo, is_builtin: bool) -> Function
15611570
.collect(),
15621571
return_type: Some(type_info.name.clone()),
15631572
returns_fresh: true,
1573+
effects: Vec::new(),
15641574
retained_params: HashSet::new(),
15651575
is_builtin,
15661576
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// expect: RS0009
2+
3+
fn noisy(message: read String) -> Unit
4+
effects(pure)
5+
{
6+
Log.write(message: read message)
7+
return Unit
8+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
fn double(value: read Int) -> Int
2+
effects(pure)
3+
{
4+
return value + value
5+
}
6+
7+
fn quadruple(value: read Int) -> Int
8+
effects(pure)
9+
{
10+
return double(value: read value) + double(value: read value)
11+
}

0 commit comments

Comments
 (0)