1use std::collections::{HashMap, HashSet};
2
3use crate::analysis::DiscoveryPass;
4use crate::arena::Arena;
5use crate::arena_ctx::AstContext;
6use crate::ast::stmt::{ClosureBody, Expr, Literal, Pattern, Stmt};
7use crate::drs::WorldState;
8use crate::error::ParseError;
9use crate::intern::{Interner, Symbol};
10use crate::lexer::Lexer;
11use crate::parser::Parser;
12
13use crate::analysis::callgraph::CallGraph;
14
15#[derive(Debug, Clone, Default)]
22pub struct EffectSet {
23 pub reads: HashSet<Symbol>,
24 pub writes: HashSet<Symbol>,
25 pub allocates: bool,
26 pub io: bool,
27 pub security_check: bool,
28 pub diverges: bool,
29 pub unknown: bool,
30 pub nondet: bool,
34 pub concurrent: bool,
37}
38
39impl EffectSet {
40 pub fn pure() -> Self {
41 Self::default()
42 }
43
44 pub fn read(sym: Symbol) -> Self {
45 let mut s = Self::default();
46 s.reads.insert(sym);
47 s
48 }
49
50 pub fn write(sym: Symbol) -> Self {
51 let mut s = Self::default();
52 s.writes.insert(sym);
53 s
54 }
55
56 pub fn io() -> Self {
57 let mut s = Self::default();
58 s.io = true;
59 s
60 }
61
62 pub fn alloc() -> Self {
63 let mut s = Self::default();
64 s.allocates = true;
65 s
66 }
67
68 pub fn unknown() -> Self {
69 let mut s = Self::default();
70 s.unknown = true;
71 s
72 }
73
74 pub fn nondet() -> Self {
75 let mut s = Self::default();
76 s.nondet = true;
77 s
78 }
79
80 pub fn concurrent() -> Self {
81 let mut s = Self::default();
82 s.concurrent = true;
83 s
84 }
85
86 pub fn is_pure(&self) -> bool {
87 self.reads.is_empty()
88 && self.writes.is_empty()
89 && !self.allocates
90 && !self.io
91 && !self.security_check
92 && !self.diverges
93 && !self.unknown
94 && !self.nondet
95 && !self.concurrent
96 }
97
98 pub fn join(&mut self, other: &EffectSet) {
100 self.reads.extend(&other.reads);
101 self.writes.extend(&other.writes);
102 self.allocates |= other.allocates;
103 self.io |= other.io;
104 self.security_check |= other.security_check;
105 self.diverges |= other.diverges;
106 self.unknown |= other.unknown;
107 self.nondet |= other.nondet;
108 self.concurrent |= other.concurrent;
109 }
110}
111
112pub struct EffectEnv {
118 bindings: HashMap<Symbol, EffectSet>,
120 aggregate: EffectSet,
122 functions: HashMap<Symbol, EffectSet>,
124 interner: Interner,
126}
127
128impl EffectEnv {
129 pub fn from_stmts(stmts: &[Stmt<'_>], interner: &Interner) -> Self {
131 let env = analyze_program(stmts, interner);
132 Self {
133 bindings: env.bindings,
134 aggregate: env.aggregate,
135 functions: env.functions,
136 interner: interner.clone(),
137 }
138 }
139
140 pub fn analyze_source(source: &str) -> Result<Self, ParseError> {
142 let mut interner = Interner::new();
143 let mut lexer = Lexer::new(source, &mut interner);
144 let tokens = lexer.tokenize();
145
146 let type_registry = {
147 let mut discovery = DiscoveryPass::new(&tokens, &mut interner);
148 let result = discovery.run_full();
149 result.types
150 };
151
152 let mut world_state = WorldState::new();
153 let expr_arena = Arena::new();
154 let term_arena = Arena::new();
155 let np_arena = Arena::new();
156 let sym_arena = Arena::new();
157 let role_arena = Arena::new();
158 let pp_arena = Arena::new();
159 let stmt_arena = Arena::new();
160 let imperative_expr_arena = Arena::new();
161 let type_expr_arena = Arena::new();
162
163 let ast_ctx = AstContext::with_types(
164 &expr_arena,
165 &term_arena,
166 &np_arena,
167 &sym_arena,
168 &role_arena,
169 &pp_arena,
170 &stmt_arena,
171 &imperative_expr_arena,
172 &type_expr_arena,
173 );
174
175 let mut parser = Parser::new(tokens, &mut world_state, &mut interner, ast_ctx, type_registry);
176 let stmts = parser.parse_program()?;
177
178 let env = analyze_program(&stmts, &interner);
179
180 Ok(Self {
181 bindings: env.bindings,
182 aggregate: env.aggregate,
183 functions: env.functions,
184 interner,
185 })
186 }
187
188 fn resolve_name(&self, name: &str) -> Option<Symbol> {
189 self.interner.lookup(name)
191 }
192
193 pub fn is_binding_pure(&self, var_name: &str) -> bool {
194 if let Some(sym) = self.resolve_name(var_name) {
195 self.bindings.get(&sym).map(|e| e.is_pure()).unwrap_or(true)
196 } else {
197 true
198 }
199 }
200
201 pub fn binding_reads(&self, var_name: &str, read_var: &str) -> bool {
202 let var_sym = match self.resolve_name(var_name) {
203 Some(s) => s,
204 None => return false,
205 };
206 let read_sym = match self.resolve_name(read_var) {
207 Some(s) => s,
208 None => return false,
209 };
210 self.bindings
211 .get(&var_sym)
212 .map(|e| e.reads.contains(&read_sym))
213 .unwrap_or(false)
214 }
215
216 pub fn binding_allocates(&self, var_name: &str) -> bool {
217 if let Some(sym) = self.resolve_name(var_name) {
218 self.bindings.get(&sym).map(|e| e.allocates).unwrap_or(false)
219 } else {
220 false
221 }
222 }
223
224 pub fn has_write_to(&self, var_name: &str) -> bool {
225 if let Some(sym) = self.resolve_name(var_name) {
226 self.aggregate.writes.contains(&sym)
227 } else {
228 false
229 }
230 }
231
232 pub fn has_io(&self) -> bool {
233 self.aggregate.io
234 }
235
236 pub fn has_unknown(&self) -> bool {
237 self.aggregate.unknown
238 }
239
240 pub fn has_security_check(&self) -> bool {
241 self.aggregate.security_check
242 }
243
244 pub fn may_diverge(&self) -> bool {
245 self.aggregate.diverges
246 }
247
248 pub fn function_is_pure(&self, fn_name: &str) -> bool {
249 if let Some(sym) = self.resolve_name(fn_name) {
250 self.functions.get(&sym).map(|e| e.is_pure()).unwrap_or(true)
251 } else {
252 true
253 }
254 }
255
256 pub fn function_has_io(&self, fn_name: &str) -> bool {
257 if let Some(sym) = self.resolve_name(fn_name) {
258 self.functions.get(&sym).map(|e| e.io).unwrap_or(false)
259 } else {
260 false
261 }
262 }
263
264 pub fn function_is_specialization_safe(&self, fn_name: &str) -> bool {
276 if let Some(sym) = self.resolve_name(fn_name) {
277 match self.functions.get(&sym) {
278 Some(e) => !e.io && !e.nondet && !e.concurrent,
279 None => false,
280 }
281 } else {
282 false
283 }
284 }
285}
286
287struct AnalysisResult {
292 bindings: HashMap<Symbol, EffectSet>,
293 aggregate: EffectSet,
294 functions: HashMap<Symbol, EffectSet>,
295}
296
297fn analyze_program(stmts: &[Stmt<'_>], interner: &Interner) -> AnalysisResult {
298 let callgraph = CallGraph::build(stmts, interner);
299
300 let mut functions: HashMap<Symbol, EffectSet> = HashMap::new();
302
303 let mut known_fns: HashSet<Symbol> = HashSet::new();
305 for stmt in stmts {
306 if let Stmt::FunctionDef { name, .. } = stmt {
307 known_fns.insert(*name);
308 }
309 }
310
311 for stmt in stmts {
313 if let Stmt::FunctionDef { name, body, is_native, params, .. } = stmt {
314 if *is_native {
315 let effects = classify_native_function(*name, interner);
317 functions.insert(*name, effects);
318 } else {
319 let mut effects = analyze_block_effects_with(body, &known_fns);
320 for (param_sym, _) in params {
322 effects.reads.remove(param_sym);
323 }
324 functions.insert(*name, effects);
325 }
326 }
327 }
328
329 let mut changed = true;
331 while changed {
332 changed = false;
333 for scc in &callgraph.sccs {
334 for &fn_sym in scc {
336 if callgraph.native_fns.contains(&fn_sym) {
337 continue;
338 }
339 let callees = callgraph.edges.get(&fn_sym).cloned().unwrap_or_default();
340 for callee in &callees {
341 if let Some(callee_effects) = functions.get(callee).cloned() {
342 if let Some(fn_effects) = functions.get_mut(&fn_sym) {
343 let before = format!("{:?}", fn_effects);
344 fn_effects.join(&callee_effects);
345 let after = format!("{:?}", fn_effects);
346 if before != after {
347 changed = true;
348 }
349 }
350 }
351 }
352 }
353
354 if scc.len() > 1 {
356 for &fn_sym in scc {
357 if let Some(fn_effects) = functions.get_mut(&fn_sym) {
358 if !fn_effects.diverges {
359 fn_effects.diverges = true;
360 changed = true;
361 }
362 }
363 }
364 } else if scc.len() == 1 {
365 let fn_sym = scc[0];
366 if callgraph.edges.get(&fn_sym).map(|e| e.contains(&fn_sym)).unwrap_or(false) {
367 if let Some(fn_effects) = functions.get_mut(&fn_sym) {
368 if !fn_effects.diverges {
369 fn_effects.diverges = true;
370 changed = true;
371 }
372 }
373 }
374 }
375 }
376 }
377
378 let mut bindings: HashMap<Symbol, EffectSet> = HashMap::new();
380 let mut aggregate = EffectSet::default();
381
382 for stmt in stmts {
383 let effects = analyze_stmt_effects(stmt, &functions);
384 aggregate.join(&effects);
385
386 if let Stmt::Let { var, value, .. } = stmt {
388 let expr_effects = analyze_expr_effects(value);
389 bindings.insert(*var, expr_effects);
390 }
391 }
392
393 AnalysisResult {
394 bindings,
395 aggregate,
396 functions,
397 }
398}
399
400fn analyze_block_effects(stmts: &[Stmt<'_>]) -> EffectSet {
401 analyze_block_effects_with(stmts, &HashSet::new())
402}
403
404fn analyze_block_effects_with(stmts: &[Stmt<'_>], known_fns: &HashSet<Symbol>) -> EffectSet {
405 let mut result = EffectSet::default();
406 for stmt in stmts {
407 let effects = analyze_stmt_effects_initial(stmt, known_fns);
408 result.join(&effects);
409 }
410 result
411}
412
413fn analyze_stmt_effects_initial(stmt: &Stmt<'_>, known_fns: &HashSet<Symbol>) -> EffectSet {
417 analyze_stmt_effects_core(stmt, None, known_fns)
418}
419
420fn analyze_stmt_effects(stmt: &Stmt<'_>, functions: &HashMap<Symbol, EffectSet>) -> EffectSet {
421 analyze_stmt_effects_core(stmt, Some(functions), &HashSet::new())
422}
423
424fn analyze_stmt_effects_core(
425 stmt: &Stmt<'_>,
426 functions: Option<&HashMap<Symbol, EffectSet>>,
427 known_fns: &HashSet<Symbol>,
428) -> EffectSet {
429 match stmt {
430 Stmt::Let { var, value, .. } => {
431 let mut effects = analyze_expr_effects_core(value, known_fns);
432 effects.writes.insert(*var);
433 effects
434 }
435 Stmt::Set { target, value } => {
436 let mut effects = analyze_expr_effects_core(value, known_fns);
437 effects.writes.insert(*target);
438 effects
439 }
440 Stmt::Call { function, args } => {
441 let mut effects = EffectSet::default();
442 for arg in args {
443 effects.join(&analyze_expr_effects_core(arg, known_fns));
444 }
445 if let Some(fns) = functions {
446 if let Some(fn_effects) = fns.get(function) {
447 effects.join(fn_effects);
448 } else if !known_fns.contains(function) {
449 effects.unknown = true;
450 }
451 } else if !known_fns.contains(function) {
452 effects.unknown = true;
453 }
454 effects
455 }
456 Stmt::If { cond, then_block, else_block } => {
457 let mut effects = analyze_expr_effects_core(cond, known_fns);
458 let then_effects = analyze_block_effects_with(then_block, known_fns);
459 effects.join(&then_effects);
460 if let Some(else_b) = else_block {
461 let else_effects = analyze_block_effects_with(else_b, known_fns);
462 effects.join(&else_effects);
463 }
464 effects
465 }
466 Stmt::While { cond, body, .. } => {
467 let mut effects = analyze_expr_effects_core(cond, known_fns);
468 let body_effects = analyze_block_effects_with(body, known_fns);
469 effects.join(&body_effects);
470 effects.diverges = true;
471 effects
472 }
473 Stmt::Repeat { iterable, body, .. } => {
474 let mut effects = analyze_expr_effects_core(iterable, known_fns);
475 let body_effects = analyze_block_effects_with(body, known_fns);
476 effects.join(&body_effects);
477 effects
478 }
479 Stmt::Return { value } => {
480 if let Some(v) = value {
481 analyze_expr_effects_core(v, known_fns)
482 } else {
483 EffectSet::pure()
484 }
485 }
486 Stmt::Show { object, .. } => {
487 let mut effects = analyze_expr_effects_core(object, known_fns);
488 effects.io = true;
489 effects
490 }
491 Stmt::Push { value, collection } => {
492 let mut effects = analyze_expr_effects_core(value, known_fns);
493 effects.join(&analyze_expr_effects_core(collection, known_fns));
494 if let Expr::Identifier(sym) = collection {
495 effects.writes.insert(*sym);
496 }
497 effects
498 }
499 Stmt::Pop { collection, into } => {
500 let mut effects = analyze_expr_effects_core(collection, known_fns);
501 if let Expr::Identifier(sym) = collection {
502 effects.writes.insert(*sym);
503 }
504 if let Some(into_sym) = into {
505 effects.writes.insert(*into_sym);
506 }
507 effects
508 }
509 Stmt::Add { value, collection } | Stmt::Remove { value, collection } => {
510 let mut effects = analyze_expr_effects_core(value, known_fns);
511 effects.join(&analyze_expr_effects_core(collection, known_fns));
512 if let Expr::Identifier(sym) = collection {
513 effects.writes.insert(*sym);
514 }
515 effects
516 }
517 Stmt::SetIndex { collection, index, value } => {
518 let mut effects = analyze_expr_effects_core(value, known_fns);
519 effects.join(&analyze_expr_effects_core(index, known_fns));
520 if let Expr::Identifier(sym) = collection {
521 effects.writes.insert(*sym);
522 }
523 effects
524 }
525 Stmt::SetField { object, value, .. } => {
526 let mut effects = analyze_expr_effects_core(value, known_fns);
527 if let Expr::Identifier(sym) = object {
528 effects.writes.insert(*sym);
529 }
530 effects
531 }
532 Stmt::Give { object, recipient } => {
533 let mut effects = analyze_expr_effects_core(object, known_fns);
534 effects.join(&analyze_expr_effects_core(recipient, known_fns));
535 effects
536 }
537 Stmt::Escape { .. } => EffectSet::unknown(),
538 Stmt::Check { .. } => {
539 let mut effects = EffectSet::default();
540 effects.security_check = true;
541 effects
542 }
543 Stmt::RuntimeAssert { condition, .. } => analyze_expr_effects_core(condition, known_fns),
544 Stmt::FunctionDef { .. } | Stmt::StructDef { .. } => EffectSet::pure(),
545 Stmt::Inspect { target, arms, .. } => {
546 let mut effects = analyze_expr_effects_core(target, known_fns);
547 for arm in arms {
548 let arm_effects = analyze_block_effects_with(arm.body, known_fns);
549 effects.join(&arm_effects);
550 }
551 effects
552 }
553 Stmt::Zone { body, .. } => analyze_block_effects_with(body, known_fns),
554 Stmt::Concurrent { tasks } | Stmt::Parallel { tasks } => {
555 let mut effects = analyze_block_effects_with(tasks, known_fns);
556 effects.concurrent = true;
557 effects
558 }
559 Stmt::WriteFile { content, path } => {
560 let mut effects = analyze_expr_effects_core(content, known_fns);
561 effects.join(&analyze_expr_effects_core(path, known_fns));
562 effects.io = true;
563 effects
564 }
565 Stmt::SendMessage { message, destination, .. } => {
566 let mut effects = analyze_expr_effects_core(message, known_fns);
567 effects.join(&analyze_expr_effects_core(destination, known_fns));
568 effects.io = true;
569 effects
570 }
571 Stmt::StreamMessage { values, destination } => {
572 let mut effects = analyze_expr_effects_core(values, known_fns);
573 effects.join(&analyze_expr_effects_core(destination, known_fns));
574 effects.io = true;
575 effects
576 }
577 Stmt::IncreaseCrdt { object, amount, .. } | Stmt::DecreaseCrdt { object, amount, .. } => {
578 let mut effects = analyze_expr_effects_core(object, known_fns);
579 effects.join(&analyze_expr_effects_core(amount, known_fns));
580 if let Expr::Identifier(sym) = object {
581 effects.writes.insert(*sym);
582 }
583 effects
584 }
585 Stmt::MergeCrdt { source, target } => {
586 let mut effects = analyze_expr_effects_core(source, known_fns);
587 effects.join(&analyze_expr_effects_core(target, known_fns));
588 if let Expr::Identifier(sym) = target {
589 effects.writes.insert(*sym);
590 }
591 effects
592 }
593 Stmt::Sleep { milliseconds } => {
594 let mut effects = analyze_expr_effects_core(milliseconds, known_fns);
595 effects.io = true;
596 effects
597 }
598 Stmt::SendPipe { value, pipe } => {
599 let mut effects = analyze_expr_effects_core(value, known_fns);
600 effects.join(&analyze_expr_effects_core(pipe, known_fns));
601 effects.io = true;
602 effects.diverges = true;
603 effects.concurrent = true;
604 effects
605 }
606 Stmt::TrySendPipe { value, pipe, result } => {
607 let mut effects = analyze_expr_effects_core(value, known_fns);
608 effects.join(&analyze_expr_effects_core(pipe, known_fns));
609 effects.io = true;
610 effects.concurrent = true;
611 effects.nondet = true;
613 if let Some(result_sym) = result {
614 effects.writes.insert(*result_sym);
615 }
616 effects
617 }
618 Stmt::ReceivePipe { pipe, var, .. } => {
619 let mut effects = analyze_expr_effects_core(pipe, known_fns);
620 effects.writes.insert(*var);
621 effects.io = true;
622 effects.diverges = true;
623 effects.concurrent = true;
624 effects
625 }
626 Stmt::ReadFrom { var, .. } => {
627 let mut effects = EffectSet::default();
628 effects.writes.insert(*var);
629 effects.io = true;
630 effects
631 }
632 Stmt::Select { branches } => {
634 let mut effects = EffectSet::nondet();
637 for branch in branches {
638 match branch {
639 crate::ast::stmt::SelectBranch::Receive { var, pipe, body } => {
640 effects.join(&analyze_expr_effects_core(pipe, known_fns));
641 effects.writes.insert(*var);
642 effects.join(&analyze_block_effects_with(body, known_fns));
643 }
644 crate::ast::stmt::SelectBranch::Timeout { milliseconds, body } => {
645 effects.join(&analyze_expr_effects_core(milliseconds, known_fns));
646 effects.join(&analyze_block_effects_with(body, known_fns));
647 }
648 }
649 }
650 effects.concurrent = true;
651 effects
652 }
653 Stmt::LaunchTask { args, .. } => {
654 let mut effects = EffectSet::concurrent();
655 for arg in args {
656 effects.join(&analyze_expr_effects_core(arg, known_fns));
657 }
658 effects
659 }
660 Stmt::LaunchTaskWithHandle { handle, args, .. } => {
661 let mut effects = EffectSet::concurrent();
662 effects.writes.insert(*handle);
663 for arg in args {
664 effects.join(&analyze_expr_effects_core(arg, known_fns));
665 }
666 effects
667 }
668 Stmt::CreatePipe { var, .. } => {
669 let mut effects = EffectSet::concurrent();
670 effects.allocates = true;
671 effects.writes.insert(*var);
672 effects
673 }
674 Stmt::TryReceivePipe { var, pipe } => {
675 let mut effects = analyze_expr_effects_core(pipe, known_fns);
678 effects.writes.insert(*var);
679 effects.io = true;
680 effects.nondet = true;
681 effects.concurrent = true;
682 effects
683 }
684 Stmt::StopTask { handle } => {
685 let mut effects = analyze_expr_effects_core(handle, known_fns);
687 effects.nondet = true;
688 effects.concurrent = true;
689 effects
690 }
691 Stmt::Spawn { name, .. } => {
692 let mut effects = EffectSet::concurrent();
693 effects.writes.insert(*name);
694 effects
695 }
696 Stmt::AwaitMessage { source, into, view: _, stream: _ } => {
698 let mut effects = analyze_expr_effects_core(source, known_fns);
699 effects.writes.insert(*into);
700 effects.io = true;
701 effects.concurrent = true;
702 effects
703 }
704 Stmt::Listen { address, secure } | Stmt::ConnectTo { address, secure } => {
705 let mut effects = analyze_expr_effects_core(address, known_fns);
706 if let Some(bind) = secure {
707 let pad_fx = analyze_expr_effects_core(bind.pad, known_fns);
709 effects.io |= pad_fx.io;
710 effects.concurrent |= pad_fx.concurrent;
711 effects.writes.extend(pad_fx.writes);
712 }
713 effects.io = true;
714 effects.concurrent = true;
715 effects
716 }
717 Stmt::LetPeerAgent { var, address } => {
718 let mut effects = analyze_expr_effects_core(address, known_fns);
719 effects.writes.insert(*var);
720 effects.io = true;
721 effects.concurrent = true;
722 effects
723 }
724 Stmt::Sync { var, topic } => {
725 let mut effects = analyze_expr_effects_core(topic, known_fns);
728 effects.reads.insert(*var);
729 effects.writes.insert(*var);
730 effects.io = true;
731 effects.concurrent = true;
732 effects
733 }
734 Stmt::Mount { var, path } => {
735 let mut effects = analyze_expr_effects_core(path, known_fns);
736 effects.writes.insert(*var);
737 effects.io = true;
738 effects
739 }
740 Stmt::AppendToSequence { sequence, value } => {
742 let mut effects = analyze_expr_effects_core(sequence, known_fns);
743 effects.join(&analyze_expr_effects_core(value, known_fns));
744 if let Expr::Identifier(sym) = sequence {
745 effects.writes.insert(*sym);
746 }
747 effects
748 }
749 Stmt::ResolveConflict { object, value, .. } => {
750 let mut effects = analyze_expr_effects_core(object, known_fns);
751 effects.join(&analyze_expr_effects_core(value, known_fns));
752 if let Expr::Identifier(sym) = object {
753 effects.writes.insert(*sym);
754 }
755 effects
756 }
757 _ => EffectSet::default(),
758 }
759}
760
761fn analyze_expr_effects(expr: &Expr<'_>) -> EffectSet {
762 analyze_expr_effects_core(expr, &HashSet::new())
763}
764
765fn analyze_expr_effects_core(expr: &Expr<'_>, known_fns: &HashSet<Symbol>) -> EffectSet {
766 match expr {
767 Expr::Literal(_) | Expr::OptionNone => EffectSet::pure(),
768 Expr::Identifier(sym) => EffectSet::read(*sym),
769 Expr::BinaryOp { left, right, .. } => {
770 let mut effects = analyze_expr_effects_core(left, known_fns);
771 effects.join(&analyze_expr_effects_core(right, known_fns));
772 effects
773 }
774 Expr::Not { operand } => analyze_expr_effects_core(operand, known_fns),
775 Expr::Call { function, args, .. } => {
776 let mut effects = EffectSet::default();
777 for arg in args {
778 effects.join(&analyze_expr_effects_core(arg, known_fns));
779 }
780 if !known_fns.contains(function) {
782 effects.unknown = true;
783 }
784 effects
785 }
786 Expr::CallExpr { callee, args } => {
787 let mut effects = analyze_expr_effects_core(callee, known_fns);
788 for arg in args {
789 effects.join(&analyze_expr_effects_core(arg, known_fns));
790 }
791 effects.unknown = true;
792 effects
793 }
794 Expr::Index { collection, index } => {
795 let mut effects = analyze_expr_effects_core(collection, known_fns);
796 effects.join(&analyze_expr_effects_core(index, known_fns));
797 effects
798 }
799 Expr::Slice { collection, start, end } => {
800 let mut effects = analyze_expr_effects_core(collection, known_fns);
801 effects.join(&analyze_expr_effects_core(start, known_fns));
802 effects.join(&analyze_expr_effects_core(end, known_fns));
803 effects
804 }
805 Expr::Length { collection } => analyze_expr_effects_core(collection, known_fns),
806 Expr::Contains { collection, value } => {
807 let mut effects = analyze_expr_effects_core(collection, known_fns);
808 effects.join(&analyze_expr_effects_core(value, known_fns));
809 effects
810 }
811 Expr::Union { left, right } | Expr::Intersection { left, right }
812 | Expr::Range { start: left, end: right } => {
813 let mut effects = analyze_expr_effects_core(left, known_fns);
814 effects.join(&analyze_expr_effects_core(right, known_fns));
815 effects
816 }
817 Expr::Copy { expr } => {
818 let mut effects = analyze_expr_effects_core(expr, known_fns);
819 effects.allocates = true;
820 effects
821 }
822 Expr::Give { value } => analyze_expr_effects_core(value, known_fns),
823 Expr::FieldAccess { object, .. } => analyze_expr_effects_core(object, known_fns),
824 Expr::OptionSome { value } => analyze_expr_effects_core(value, known_fns),
825 Expr::ManifestOf { zone } => analyze_expr_effects_core(zone, known_fns),
826 Expr::ChunkAt { index, zone } => {
827 let mut effects = analyze_expr_effects_core(index, known_fns);
828 effects.join(&analyze_expr_effects_core(zone, known_fns));
829 effects
830 }
831 Expr::New { init_fields, .. } => {
832 let mut effects = EffectSet::default();
833 effects.allocates = true;
834 for (_, val) in init_fields {
835 effects.join(&analyze_expr_effects_core(val, known_fns));
836 }
837 effects
838 }
839 Expr::NewVariant { fields, .. } => {
840 let mut effects = EffectSet::default();
841 for (_, val) in fields {
842 effects.join(&analyze_expr_effects_core(val, known_fns));
843 }
844 effects
845 }
846 Expr::WithCapacity { value, capacity } => {
847 let mut effects = analyze_expr_effects_core(value, known_fns);
848 effects.join(&analyze_expr_effects_core(capacity, known_fns));
849 effects.allocates = true;
850 effects
851 }
852 Expr::List(elems) | Expr::Tuple(elems) => {
853 let mut effects = EffectSet::default();
854 for elem in elems {
855 effects.join(&analyze_expr_effects_core(elem, known_fns));
856 }
857 effects
858 }
859 Expr::Closure { .. } => EffectSet::pure(),
860 Expr::Escape { .. } => EffectSet::unknown(),
861 Expr::InterpolatedString(parts) => {
862 let mut effects = EffectSet::default();
863 for part in parts {
864 if let crate::ast::stmt::StringPart::Expr { value, .. } = part {
865 effects.join(&analyze_expr_effects_core(value, known_fns));
866 }
867 }
868 effects
869 }
870 }
871}
872
873fn classify_native_function(sym: Symbol, interner: &Interner) -> EffectSet {
875 let name = interner.resolve(sym);
876 match name {
877 "parseInt" | "parseFloat" | "decimal" | "complex" | "modular" | "quantity" | "convert" | "money"
879 | "uuid" | "uuid_nil" | "uuid_max" | "uuid_v3" | "uuid_v5" | "uuid_version"
880 | "uuid_dns" | "uuid_url" | "uuid_oid" | "uuid_x500"
881 | "md5" | "sha1" | "text_bytes" | "uuid_bytes" | "uuid_from_bytes"
882 | "parse_timestamp" | "format_timestamp" | "year_of" | "month_of" | "day_of" | "weekday_of"
883 | "hour_of" | "minute_of" | "second_of" | "week_of" | "quarter_of" | "date_of" | "time_of"
884 | "seconds_between" | "months_between" | "years_between" | "add_seconds" | "in_zone"
885 | "local_instant" | "abs" | "min" | "max" | "sqrt" | "floor"
886 | "ceil" | "round" | "pow" | "log" | "sin" | "cos" | "tan"
887 | "toString" | "toInt" | "toFloat" | "trim" | "uppercase" | "lowercase"
888 | "split" | "join" | "replace" | "startsWith" | "endsWith"
889 | "substring" | "charAt" | "indexOf" | "lastIndexOf" | "repeat" => {
890 EffectSet::pure()
891 }
892 "args" | "show" | "readLine" | "sleep" | "readFile" | "writeFile"
894 | "print" | "println" | "eprintln" | "exit" => {
895 EffectSet::io()
896 }
897 "newSeq" | "newMap" | "newSet" | "copy" | "clone" | "mapOf" | "setOf" | "repeatSeq" => {
899 EffectSet::alloc()
900 }
901 _ => EffectSet::io(),
903 }
904}
905
906#[cfg(test)]
907mod concurrency_effect_tests {
908 use super::EffectEnv;
915
916 #[test]
917 fn effects_select_is_not_pure() {
918 let src = "\
921## To sel (ch: Int):
922 Await the first of:
923 Receive x from ch:
924 Show x.
925 After 1 seconds:
926 Show 0.
927
928## Main
929 Let ch be a Pipe of Int.
930 Launch a task to sel with ch.
931";
932 let env = EffectEnv::analyze_source(src).expect("parse");
933 assert!(
934 !env.function_is_pure("sel"),
935 "a function whose body is a Select must not be classified Pure"
936 );
937 }
938
939 #[test]
940 fn effects_launch_task_not_pure() {
941 let src = "\
942## To helper (ch: Int):
943 Receive y from ch.
944 Show y.
945
946## To launcher (ch: Int):
947 Launch a task to helper with ch.
948
949## Main
950 Let ch be a Pipe of Int.
951 Launch a task to launcher with ch.
952";
953 let env = EffectEnv::analyze_source(src).expect("parse");
954 assert!(
955 !env.function_is_pure("launcher"),
956 "a function that launches a task is concurrent, not Pure"
957 );
958 }
959
960 #[test]
961 fn effects_try_receive_nondet() {
962 let src = "\
963## To tryer (ch: Int):
964 Try to receive v from ch.
965
966## Main
967 Let ch be a Pipe of Int.
968 Launch a task to tryer with ch.
969";
970 let env = EffectEnv::analyze_source(src).expect("parse");
971 assert!(
972 !env.function_is_pure("tryer"),
973 "a non-blocking receive depends on instantaneous buffer state — not Pure"
974 );
975 }
976
977 #[test]
978 fn effects_mount_writes_var() {
979 let src = "\
980## Main
981 Mount counter at \"data/counter.journal\".
982 Show counter.
983";
984 let env = EffectEnv::analyze_source(src).expect("parse");
985 assert!(
986 env.has_write_to("counter"),
987 "Mount binds its variable — the write must be recorded"
988 );
989 }
990}