Skip to main content

logicaffeine_compile/optimize/
effects.rs

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/// Effect classification for expressions, statements, and functions.
16///
17/// Effects form a lattice: Pure ⊂ Read ⊂ Write ⊂ IO ⊂ Unknown.
18/// Each EffectSet tracks the precise set of variables read/written,
19/// plus boolean flags for allocation, IO, security checks, divergence,
20/// and unknown (escape blocks).
21#[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    /// Nondeterministic choice — `Select`, `Try*` (depends on instantaneous buffer
31    /// state), and cooperative `StopTask`. The outcome is not a function of the inputs,
32    /// so a function carrying one must never be specialized on a static argument.
33    pub nondet: bool,
34    /// Spawns or communicates across a concurrency boundary — `LaunchTask`, `CreatePipe`,
35    /// `Spawn`, and the networking statements. Opaque to the partial evaluator.
36    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    /// Join two EffectSets (union of all effects).
99    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
112/// Environment tracking effects for variables and functions.
113///
114/// For each variable binding (`Let x be <expr>`), tracks the EffectSet
115/// of the bound expression. For each function, tracks the aggregate
116/// EffectSet of its body.
117pub struct EffectEnv {
118    /// Binding-level effects: var_sym → effect of the bound expression
119    bindings: HashMap<Symbol, EffectSet>,
120    /// Statement-level aggregate effects (joined across all statements)
121    aggregate: EffectSet,
122    /// Function-level effects: fn_sym → aggregate effect of body
123    functions: HashMap<Symbol, EffectSet>,
124    /// Interner for name resolution in test queries
125    interner: Interner,
126}
127
128impl EffectEnv {
129    /// Analyze already-parsed statements and return the EffectEnv.
130    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    /// Analyze a complete LOGOS source and return the EffectEnv.
141    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        // Try to find the symbol in the interner
190        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    /// Whether the partial evaluator may specialize a call to `fn_name` on a static
265    /// argument. Reads, writes, allocation, and divergence are ordinary PE targets and
266    /// stay safe. IO already blocks specialization (the historical gate). Nondeterminism
267    /// and concurrency are the new exclusions: a concurrency / networking statement carries
268    /// expression arguments (a timeout, spawn args, a branch body) that reference the
269    /// static parameter, and the specializer's substitution does not enter those
270    /// statements — folding across one would drop the parameter and leave a dangling
271    /// reference. A `Check` (security_check) is deliberately *not* excluded: it is preserved
272    /// verbatim and never references a foldable parameter, so specializing across it is
273    /// sound (see `pe_effect_env_check_is_not_io`). Escape blocks (`unknown`) are handled by
274    /// the caller's separate `body_has_escape` guard.
275    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
287// =============================================================================
288// Internal analysis
289// =============================================================================
290
291struct 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    // Phase 1: Analyze function bodies
301    let mut functions: HashMap<Symbol, EffectSet> = HashMap::new();
302
303    // Collect all known function names first
304    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    // Initialize all functions with their direct effects
312    for stmt in stmts {
313        if let Stmt::FunctionDef { name, body, is_native, params, .. } = stmt {
314            if *is_native {
315                // Native functions: classify by name
316                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                // Remove parameter reads — reading a parameter is not a side effect
321                for (param_sym, _) in params {
322                    effects.reads.remove(param_sym);
323                }
324                functions.insert(*name, effects);
325            }
326        }
327    }
328
329    // Fixed-point: propagate effects through call graph
330    let mut changed = true;
331    while changed {
332        changed = false;
333        for scc in &callgraph.sccs {
334            // For each SCC, join effects of all callees
335            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            // Recursive SCCs always diverge
355            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    // Phase 2: Analyze main body and bindings
379    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        // Track per-binding effects
387        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
413/// Analyze statement effects during initial function body analysis.
414/// known_fns tracks which functions are defined in the program so calls to them
415/// don't get marked as unknown (the fixed-point iteration handles transitive effects).
416fn 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            // Success depends on instantaneous buffer state — nondeterministic.
612            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        // ----- Go-like concurrency: opaque effectful boundaries -----
633        Stmt::Select { branches } => {
634            // A Select is a nondeterministic choice over its ready branches. Its outcome
635            // is not a function of the inputs, and each branch body is part of the effect.
636            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            // Non-blocking receive: returns value-or-Nothing depending on instantaneous
676            // buffer state — nondeterministic, and it binds `var`.
677            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            // Cooperative cancellation — scheduling-dependent.
686            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        // ----- Networking over the relay: I/O boundaries -----
697        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                // The pad path is evaluated too; fold in its (read-only) effects for completeness.
708                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            // Subscribe + auto-publish-on-mutation + auto-merge-on-receive: reads and
726            // writes the synced CRDT and talks to the relay.
727            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        // ----- CRDT structural mutations not covered above -----
741        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            // Only mark as unknown if the callee is not a known function
781            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
873/// Classify a native function's effects by name.
874fn classify_native_function(sym: Symbol, interner: &Interner) -> EffectSet {
875    let name = interner.resolve(sym);
876    match name {
877        // Pure functions
878        "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        // IO functions
893        "args" | "show" | "readLine" | "sleep" | "readFile" | "writeFile"
894        | "print" | "println" | "eprintln" | "exit" => {
895            EffectSet::io()
896        }
897        // Alloc functions
898        "newSeq" | "newMap" | "newSet" | "copy" | "clone" | "mapOf" | "setOf" | "repeatSeq" => {
899            EffectSet::alloc()
900        }
901        // Default: IO (conservative for unknown natives)
902        _ => EffectSet::io(),
903    }
904}
905
906#[cfg(test)]
907mod concurrency_effect_tests {
908    //! A concurrency / networking construct is an effectful, often nondeterministic
909    //! boundary. The partial evaluator's specialization gate (`partial_eval.rs`) keys
910    //! off these effects; if any of them are misclassified as Pure, a function holding
911    //! one can be specialized and a static parameter flowing into it dropped, leaving a
912    //! dangling free variable. These tests pin the classification at the source.
913
914    use super::EffectEnv;
915
916    #[test]
917    fn effects_select_is_not_pure() {
918        // A `Select` is a nondeterministic choice — never Pure. The classifier must also
919        // see *into* the branch bodies (the `Show` here is an effect of the function).
920        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}