Skip to main content

lumen_compiler/compiler/
resolve.rs

1//! Name resolution pass — resolve cells, types, and tool aliases.
2
3use crate::compiler::ast::*;
4use std::collections::{BTreeSet, HashMap, HashSet};
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum ResolveError {
9    #[error("undefined type '{name}' at line {line}")]
10    UndefinedType {
11        name: String,
12        line: usize,
13        suggestions: Vec<String>,
14    },
15    #[error(
16        "generic type '{name}' has wrong number of type arguments at line {line}: expected {expected}, got {actual}"
17    )]
18    GenericArityMismatch {
19        name: String,
20        expected: usize,
21        actual: usize,
22        line: usize,
23    },
24    #[error("undefined cell '{name}' at line {line}")]
25    UndefinedCell {
26        name: String,
27        line: usize,
28        suggestions: Vec<String>,
29    },
30    #[error("undefined trait '{name}' at line {line}")]
31    UndefinedTrait { name: String, line: usize },
32    #[error("undefined tool alias '{name}' at line {line}")]
33    UndefinedTool { name: String, line: usize },
34    #[error("duplicate definition '{name}' at line {line}")]
35    Duplicate { name: String, line: usize },
36    #[error("cell '{cell}' requires effect '{effect}' but no compatible grant is in scope (line {line})")]
37    MissingEffectGrant {
38        cell: String,
39        effect: String,
40        line: usize,
41    },
42    #[error("cell '{cell}' performs effect '{effect}' but it is not declared in its effect row (line {line}){cause}")]
43    UndeclaredEffect {
44        cell: String,
45        effect: String,
46        line: usize,
47        cause: String,
48    },
49    #[error("cell '{caller}' calls '{callee}' which requires effect '{effect}' not present in caller effect row (line {line})")]
50    EffectContractViolation {
51        caller: String,
52        callee: String,
53        effect: String,
54        line: usize,
55    },
56    #[error("cell '{cell}' uses nondeterministic operation/effect '{operation}' under @deterministic (line {line})")]
57    NondeterministicOperation {
58        cell: String,
59        operation: String,
60        line: usize,
61    },
62    #[error("machine '{machine}' initial state '{state}' is undefined (line {line})")]
63    MachineUnknownInitial {
64        machine: String,
65        state: String,
66        line: usize,
67    },
68    #[error("machine '{machine}' state '{state}' transitions to undefined state '{target}' (line {line})")]
69    MachineUnknownTransition {
70        machine: String,
71        state: String,
72        target: String,
73        line: usize,
74    },
75    #[error("machine '{machine}' state '{state}' is unreachable from initial state '{initial}' (line {line})")]
76    MachineUnreachableState {
77        machine: String,
78        state: String,
79        initial: String,
80        line: usize,
81    },
82    #[error("machine '{machine}' declares no terminal states (line {line})")]
83    MachineMissingTerminal { machine: String, line: usize },
84    #[error("machine '{machine}' state '{state}' transition arg count mismatch for '{target}' at line {line}: expected {expected}, got {actual}")]
85    MachineTransitionArgCount {
86        machine: String,
87        state: String,
88        target: String,
89        expected: usize,
90        actual: usize,
91        line: usize,
92    },
93    #[error("machine '{machine}' state '{state}' transition arg type mismatch for '{target}' at line {line}: expected {expected}, got {actual}")]
94    MachineTransitionArgType {
95        machine: String,
96        state: String,
97        target: String,
98        expected: String,
99        actual: String,
100        line: usize,
101    },
102    #[error("machine '{machine}' state '{state}' has unsupported expression in {context} at line {line}")]
103    MachineUnsupportedExpr {
104        machine: String,
105        state: String,
106        context: String,
107        line: usize,
108    },
109    #[error("machine '{machine}' state '{state}' guard must be Bool-compatible, got {actual} at line {line}")]
110    MachineGuardType {
111        machine: String,
112        state: String,
113        actual: String,
114        line: usize,
115    },
116    #[error("pipeline '{pipeline}' references unknown stage cell '{stage}' at line {line}")]
117    PipelineUnknownStage {
118        pipeline: String,
119        stage: String,
120        line: usize,
121    },
122    #[error("pipeline '{pipeline}' stage '{stage}' has invalid arity at line {line}: expected exactly one data argument")]
123    PipelineStageArity {
124        pipeline: String,
125        stage: String,
126        line: usize,
127    },
128    #[error("pipeline '{pipeline}' stage type mismatch from '{from_stage}' to '{to_stage}' at line {line}: expected {expected}, got {actual}")]
129    PipelineStageTypeMismatch {
130        pipeline: String,
131        from_stage: String,
132        to_stage: String,
133        expected: String,
134        actual: String,
135        line: usize,
136    },
137    #[error(
138        "circular import detected: module '{module}' is already being compiled (chain: {chain})"
139    )]
140    CircularImport { module: String, chain: String },
141    #[error("module '{module}' not found at line {line}")]
142    ModuleNotFound { module: String, line: usize },
143    #[error("imported symbol '{symbol}' not found in module '{module}' at line {line}")]
144    ImportedSymbolNotFound {
145        symbol: String,
146        module: String,
147        line: usize,
148    },
149    #[error(
150        "impl for trait '{trait_name}' on '{target_type}' is missing required methods {missing:?} at line {line}"
151    )]
152    TraitMissingMethods {
153        trait_name: String,
154        target_type: String,
155        missing: Vec<String>,
156        line: usize,
157    },
158    #[error(
159        "impl method '{method}' for trait '{trait_name}' on '{target_type}' has incompatible signature at line {line}: {reason}. expected `{expected}`, found `{actual}`"
160    )]
161    TraitMethodSignatureMismatch {
162        trait_name: String,
163        target_type: String,
164        method: String,
165        reason: String,
166        expected: String,
167        actual: String,
168        line: usize,
169    },
170}
171
172/// Symbol table built during resolution
173#[derive(Debug, Clone)]
174pub struct SymbolTable {
175    pub types: HashMap<String, TypeInfo>,
176    pub cells: HashMap<String, CellInfo>,
177    pub cell_policies: HashMap<String, Vec<GrantPolicy>>,
178    pub tools: HashMap<String, ToolInfo>,
179    pub agents: HashMap<String, AgentInfo>,
180    pub processes: HashMap<String, ProcessInfo>,
181    pub effects: HashMap<String, EffectInfo>,
182    pub effect_binds: Vec<EffectBindInfo>,
183    pub handlers: HashMap<String, HandlerInfo>,
184    pub addons: Vec<AddonInfo>,
185    pub type_aliases: HashMap<String, TypeExpr>,
186    pub traits: HashMap<String, TraitInfo>,
187    pub impls: Vec<ImplInfo>,
188    pub consts: HashMap<String, ConstInfo>,
189}
190
191#[derive(Debug, Clone)]
192pub struct TypeInfo {
193    pub kind: TypeInfoKind,
194    pub generic_params: Vec<String>,
195}
196
197#[derive(Debug, Clone)]
198pub enum TypeInfoKind {
199    Builtin,
200    Record(RecordDef),
201    Enum(EnumDef),
202}
203
204#[derive(Debug, Clone)]
205pub struct CellInfo {
206    pub params: Vec<(String, TypeExpr)>,
207    pub return_type: Option<TypeExpr>,
208    pub effects: Vec<String>,
209}
210
211#[derive(Debug, Clone)]
212pub struct ToolInfo {
213    pub tool_path: String,
214    pub mcp_url: Option<String>,
215}
216
217#[derive(Debug, Clone)]
218pub struct AgentInfo {
219    pub name: String,
220    pub methods: Vec<String>,
221}
222
223#[derive(Debug, Clone)]
224pub struct ProcessInfo {
225    pub kind: String,
226    pub name: String,
227    pub methods: Vec<String>,
228    pub pipeline_stages: Vec<String>,
229    pub machine_initial: Option<String>,
230    pub machine_states: Vec<MachineStateInfo>,
231}
232
233#[derive(Debug, Clone)]
234pub struct MachineStateInfo {
235    pub name: String,
236    pub params: Vec<(String, TypeExpr)>,
237    pub terminal: bool,
238    pub guard: Option<Expr>,
239    pub transition_to: Option<String>,
240    pub transition_args: Vec<Expr>,
241}
242
243#[derive(Debug, Clone)]
244pub struct EffectInfo {
245    pub name: String,
246    pub operations: Vec<String>,
247}
248
249#[derive(Debug, Clone)]
250pub struct EffectBindInfo {
251    pub effect_path: String,
252    pub tool_alias: String,
253}
254
255#[derive(Debug, Clone)]
256pub struct HandlerInfo {
257    pub name: String,
258    pub handles: Vec<String>,
259}
260
261#[derive(Debug, Clone)]
262pub struct AddonInfo {
263    pub kind: String,
264    pub name: Option<String>,
265}
266
267#[derive(Debug, Clone)]
268pub struct TraitInfo {
269    pub name: String,
270    pub parent_traits: Vec<String>,
271    pub methods: Vec<String>,
272}
273
274#[derive(Debug, Clone)]
275pub struct ImplInfo {
276    pub trait_name: Option<String>,
277    pub target_type: String,
278    pub methods: Vec<String>,
279}
280
281#[derive(Debug, Clone)]
282pub struct ConstInfo {
283    pub name: String,
284    pub ty: Option<TypeExpr>,
285    pub value: Option<Expr>,
286}
287
288#[derive(Debug, Clone)]
289pub struct GrantPolicy {
290    pub tool_alias: String,
291    pub allowed_effects: Option<BTreeSet<String>>,
292}
293
294impl Default for SymbolTable {
295    fn default() -> Self {
296        Self::new()
297    }
298}
299
300impl SymbolTable {
301    pub fn new() -> Self {
302        let mut types = HashMap::new();
303        // Register builtin types
304        for name in &[
305            "String", "Int", "Float", "Bool", "Bytes", "Json", "Null", "Self",
306        ] {
307            types.insert(
308                name.to_string(),
309                TypeInfo {
310                    kind: TypeInfoKind::Builtin,
311                    generic_params: vec![],
312                },
313            );
314        }
315        Self {
316            types,
317            cells: HashMap::new(),
318            cell_policies: HashMap::new(),
319            tools: HashMap::new(),
320            agents: HashMap::new(),
321            processes: HashMap::new(),
322            effects: HashMap::new(),
323            effect_binds: Vec::new(),
324            handlers: HashMap::new(),
325            addons: Vec::new(),
326            type_aliases: HashMap::new(),
327            traits: HashMap::new(),
328            impls: Vec::new(),
329            consts: HashMap::new(),
330        }
331    }
332
333    /// Import a cell from an external module
334    pub fn import_cell(&mut self, name: String, info: CellInfo) {
335        self.cells.insert(name, info);
336    }
337
338    /// Import a type from an external module
339    pub fn import_type(&mut self, name: String, info: TypeInfo) {
340        self.types.insert(name, info);
341    }
342
343    /// Import a type alias from an external module
344    pub fn import_type_alias(&mut self, name: String, type_expr: TypeExpr) {
345        self.type_aliases.insert(name, type_expr);
346    }
347}
348
349/// Resolve all names in a program, building the symbol table.
350pub fn resolve(program: &Program) -> Result<SymbolTable, Vec<ResolveError>> {
351    resolve_with_base(program, SymbolTable::new())
352}
353
354/// Resolve all names in a program, using a pre-populated symbol table as the base.
355/// This is useful for multi-file compilation where imported symbols need to be available.
356pub fn resolve_with_base(
357    program: &Program,
358    mut table: SymbolTable,
359) -> Result<SymbolTable, Vec<ResolveError>> {
360    let mut errors = Vec::new();
361    let doc_mode = parse_directive_bool(program, "doc_mode").unwrap_or(false);
362
363    // First pass: register all type and cell definitions
364    for item in &program.items {
365        use std::collections::hash_map::Entry;
366        match item {
367            Item::Record(r) => match table.types.entry(r.name.clone()) {
368                Entry::Occupied(_) => {
369                    errors.push(ResolveError::Duplicate {
370                        name: r.name.clone(),
371                        line: r.span.line,
372                    });
373                }
374                Entry::Vacant(entry) => {
375                    entry.insert(TypeInfo {
376                        kind: TypeInfoKind::Record(r.clone()),
377                        generic_params: r.generic_params.iter().map(|gp| gp.name.clone()).collect(),
378                    });
379                }
380            },
381            Item::Enum(e) => match table.types.entry(e.name.clone()) {
382                Entry::Occupied(_) => {
383                    errors.push(ResolveError::Duplicate {
384                        name: e.name.clone(),
385                        line: e.span.line,
386                    });
387                }
388                Entry::Vacant(entry) => {
389                    entry.insert(TypeInfo {
390                        kind: TypeInfoKind::Enum(e.clone()),
391                        generic_params: e.generic_params.iter().map(|gp| gp.name.clone()).collect(),
392                    });
393                }
394            },
395            Item::Cell(c) => match table.cells.entry(c.name.clone()) {
396                Entry::Occupied(_) => {
397                    errors.push(ResolveError::Duplicate {
398                        name: c.name.clone(),
399                        line: c.span.line,
400                    });
401                }
402                Entry::Vacant(entry) => {
403                    entry.insert(CellInfo {
404                        params: c
405                            .params
406                            .iter()
407                            .map(|p| (p.name.clone(), p.ty.clone()))
408                            .collect(),
409                        return_type: c.return_type.clone(),
410                        effects: c.effects.clone(),
411                    });
412                }
413            },
414            Item::Agent(a) => {
415                match table.agents.entry(a.name.clone()) {
416                    Entry::Occupied(_) => {
417                        errors.push(ResolveError::Duplicate {
418                            name: a.name.clone(),
419                            line: a.span.line,
420                        });
421                    }
422                    Entry::Vacant(entry) => {
423                        entry.insert(AgentInfo {
424                            name: a.name.clone(),
425                            methods: a.cells.iter().map(|c| c.name.clone()).collect(),
426                        });
427                    }
428                }
429
430                if !table.types.contains_key(&a.name) {
431                    table.types.insert(
432                        a.name.clone(),
433                        TypeInfo {
434                            kind: TypeInfoKind::Record(RecordDef {
435                                name: a.name.clone(),
436                                generic_params: vec![],
437                                fields: vec![],
438                                is_pub: true,
439                                span: a.span,
440                            }),
441                            generic_params: vec![],
442                        },
443                    );
444                }
445
446                if !table.cells.contains_key(&a.name) {
447                    table.cells.insert(
448                        a.name.clone(),
449                        CellInfo {
450                            params: vec![],
451                            return_type: Some(TypeExpr::Named(a.name.clone(), a.span)),
452                            effects: vec![],
453                        },
454                    );
455                }
456
457                for cell in &a.cells {
458                    let method_name = format!("{}.{}", a.name, cell.name);
459                    match table.cells.entry(method_name.clone()) {
460                        Entry::Occupied(_) => {
461                            errors.push(ResolveError::Duplicate {
462                                name: method_name,
463                                line: cell.span.line,
464                            });
465                        }
466                        Entry::Vacant(entry) => {
467                            entry.insert(CellInfo {
468                                params: cell
469                                    .params
470                                    .iter()
471                                    .map(|p| (p.name.clone(), p.ty.clone()))
472                                    .collect(),
473                                return_type: cell.return_type.clone(),
474                                effects: cell.effects.clone(),
475                            });
476                        }
477                    }
478                }
479
480                for g in &a.grants {
481                    table.tools.entry(g.tool_alias.clone()).or_insert(ToolInfo {
482                        tool_path: g.tool_alias.to_lowercase(),
483                        mcp_url: None,
484                    });
485                }
486            }
487            Item::Process(p) => {
488                let process_key = format!("{}:{}", p.kind, p.name);
489                match table.processes.entry(process_key) {
490                    Entry::Occupied(_) => {
491                        errors.push(ResolveError::Duplicate {
492                            name: p.name.clone(),
493                            line: p.span.line,
494                        });
495                    }
496                    Entry::Vacant(entry) => {
497                        entry.insert(ProcessInfo {
498                            kind: p.kind.clone(),
499                            name: p.name.clone(),
500                            methods: p.cells.iter().map(|c| c.name.clone()).collect(),
501                            pipeline_stages: p.pipeline_stages.clone(),
502                            machine_initial: p.machine_initial.clone(),
503                            machine_states: p
504                                .machine_states
505                                .iter()
506                                .map(|s| MachineStateInfo {
507                                    name: s.name.clone(),
508                                    params: s
509                                        .params
510                                        .iter()
511                                        .map(|p| (p.name.clone(), p.ty.clone()))
512                                        .collect(),
513                                    terminal: s.terminal,
514                                    guard: s.guard.clone(),
515                                    transition_to: s.transition_to.clone(),
516                                    transition_args: s.transition_args.clone(),
517                                })
518                                .collect(),
519                        });
520                    }
521                }
522                if !table.types.contains_key(&p.name) {
523                    table.types.insert(
524                        p.name.clone(),
525                        TypeInfo {
526                            kind: TypeInfoKind::Record(RecordDef {
527                                name: p.name.clone(),
528                                generic_params: vec![],
529                                fields: vec![],
530                                is_pub: true,
531                                span: p.span,
532                            }),
533                            generic_params: vec![],
534                        },
535                    );
536                }
537                if !table.cells.contains_key(&p.name) {
538                    table.cells.insert(
539                        p.name.clone(),
540                        CellInfo {
541                            params: vec![],
542                            return_type: Some(TypeExpr::Named(p.name.clone(), p.span)),
543                            effects: vec![],
544                        },
545                    );
546                }
547                for cell in &p.cells {
548                    let method_name = format!("{}.{}", p.name, cell.name);
549                    table.cells.entry(method_name).or_insert(CellInfo {
550                        params: cell
551                            .params
552                            .iter()
553                            .map(|p| (p.name.clone(), p.ty.clone()))
554                            .collect(),
555                        return_type: cell.return_type.clone(),
556                        effects: cell.effects.clone(),
557                    });
558                }
559                for g in &p.grants {
560                    table.tools.entry(g.tool_alias.clone()).or_insert(ToolInfo {
561                        tool_path: g.tool_alias.to_lowercase(),
562                        mcp_url: None,
563                    });
564                }
565            }
566            Item::Effect(e) => {
567                match table.effects.entry(e.name.clone()) {
568                    Entry::Occupied(_) => {
569                        errors.push(ResolveError::Duplicate {
570                            name: e.name.clone(),
571                            line: e.span.line,
572                        });
573                    }
574                    Entry::Vacant(entry) => {
575                        entry.insert(EffectInfo {
576                            name: e.name.clone(),
577                            operations: e.operations.iter().map(|c| c.name.clone()).collect(),
578                        });
579                    }
580                }
581                for op in &e.operations {
582                    let fq_name = format!("{}.{}", e.name, op.name);
583                    table.cells.entry(fq_name).or_insert(CellInfo {
584                        params: op
585                            .params
586                            .iter()
587                            .map(|p| (p.name.clone(), p.ty.clone()))
588                            .collect(),
589                        return_type: op.return_type.clone(),
590                        effects: op.effects.clone(),
591                    });
592                }
593            }
594            Item::EffectBind(b) => {
595                table.effect_binds.push(EffectBindInfo {
596                    effect_path: b.effect_path.clone(),
597                    tool_alias: b.tool_alias.clone(),
598                });
599                table.tools.entry(b.tool_alias.clone()).or_insert(ToolInfo {
600                    tool_path: b.tool_alias.to_lowercase(),
601                    mcp_url: None,
602                });
603            }
604            Item::Handler(h) => {
605                match table.handlers.entry(h.name.clone()) {
606                    Entry::Occupied(_) => {
607                        errors.push(ResolveError::Duplicate {
608                            name: h.name.clone(),
609                            line: h.span.line,
610                        });
611                    }
612                    Entry::Vacant(entry) => {
613                        entry.insert(HandlerInfo {
614                            name: h.name.clone(),
615                            handles: h.handles.iter().map(|c| c.name.clone()).collect(),
616                        });
617                    }
618                }
619                for handle in &h.handles {
620                    let fq_name = format!("{}.{}", h.name, handle.name);
621                    table.cells.entry(fq_name).or_insert(CellInfo {
622                        params: handle
623                            .params
624                            .iter()
625                            .map(|p| (p.name.clone(), p.ty.clone()))
626                            .collect(),
627                        return_type: handle.return_type.clone(),
628                        effects: handle.effects.clone(),
629                    });
630                }
631            }
632            Item::Addon(a) => {
633                table.addons.push(AddonInfo {
634                    kind: a.kind.clone(),
635                    name: a.name.clone(),
636                });
637            }
638            Item::UseTool(u) => {
639                table.tools.insert(
640                    u.alias.clone(),
641                    ToolInfo {
642                        tool_path: u.tool_path.clone(),
643                        mcp_url: u.mcp_url.clone(),
644                    },
645                );
646            }
647            Item::Grant(_) => {} // Grants reference tools, checked below
648            Item::TypeAlias(ta) => match table.type_aliases.entry(ta.name.clone()) {
649                Entry::Occupied(_) => {
650                    errors.push(ResolveError::Duplicate {
651                        name: ta.name.clone(),
652                        line: ta.span.line,
653                    });
654                }
655                Entry::Vacant(entry) => {
656                    entry.insert(ta.type_expr.clone());
657                }
658            },
659            Item::Trait(t) => {
660                let methods: Vec<String> = t.methods.iter().map(|m| m.name.clone()).collect();
661                match table.traits.entry(t.name.clone()) {
662                    Entry::Occupied(_) => {
663                        errors.push(ResolveError::Duplicate {
664                            name: t.name.clone(),
665                            line: t.span.line,
666                        });
667                    }
668                    Entry::Vacant(entry) => {
669                        entry.insert(TraitInfo {
670                            name: t.name.clone(),
671                            parent_traits: t.parent_traits.clone(),
672                            methods,
673                        });
674                    }
675                }
676            }
677            Item::Impl(i) => {
678                let methods: Vec<String> = i.cells.iter().map(|m| m.name.clone()).collect();
679                table.impls.push(ImplInfo {
680                    trait_name: Some(i.trait_name.clone()),
681                    target_type: i.target_type.clone(),
682                    methods,
683                });
684            }
685            Item::ConstDecl(c) => {
686                table.consts.insert(
687                    c.name.clone(),
688                    ConstInfo {
689                        name: c.name.clone(),
690                        ty: c.type_ann.clone(),
691                        value: Some(c.value.clone()),
692                    },
693                );
694            }
695            Item::Import(_) | Item::MacroDecl(_) => {}
696        }
697    }
698
699    table.cell_policies = build_cell_policies(program);
700    let type_alias_arities = collect_type_alias_arities(program);
701    let trait_defs = collect_trait_defs(program);
702
703    // Second pass: verify all type references exist
704    for item in &program.items {
705        match item {
706            Item::Record(r) => {
707                check_generic_param_bounds(&r.generic_params, &table, &mut errors);
708                let generics: Vec<String> =
709                    r.generic_params.iter().map(|g| g.name.clone()).collect();
710                for field in &r.fields {
711                    check_type_refs_with_generics(
712                        &field.ty,
713                        &table,
714                        &type_alias_arities,
715                        &mut errors,
716                        &generics,
717                    );
718                }
719            }
720            Item::Enum(e) => {
721                check_generic_param_bounds(&e.generic_params, &table, &mut errors);
722                let enum_generics: Vec<String> =
723                    e.generic_params.iter().map(|g| g.name.clone()).collect();
724                for variant in &e.variants {
725                    if let Some(payload) = &variant.payload {
726                        check_type_refs_with_generics(
727                            payload,
728                            &table,
729                            &type_alias_arities,
730                            &mut errors,
731                            &enum_generics,
732                        );
733                    }
734                }
735                for method in &e.methods {
736                    check_generic_param_bounds(&method.generic_params, &table, &mut errors);
737                    let mut method_generics = enum_generics.clone();
738                    method_generics.extend(method.generic_params.iter().map(|g| g.name.clone()));
739                    for param in &method.params {
740                        check_type_refs_with_generics(
741                            &param.ty,
742                            &table,
743                            &type_alias_arities,
744                            &mut errors,
745                            &method_generics,
746                        );
747                    }
748                    if let Some(return_type) = &method.return_type {
749                        check_type_refs_with_generics(
750                            return_type,
751                            &table,
752                            &type_alias_arities,
753                            &mut errors,
754                            &method_generics,
755                        );
756                    }
757                }
758            }
759            Item::Cell(c) => {
760                if c.body.is_empty() {
761                    continue;
762                }
763                check_generic_param_bounds(&c.generic_params, &table, &mut errors);
764                let generics: Vec<String> =
765                    c.generic_params.iter().map(|g| g.name.clone()).collect();
766                for p in &c.params {
767                    check_type_refs_with_generics(
768                        &p.ty,
769                        &table,
770                        &type_alias_arities,
771                        &mut errors,
772                        &generics,
773                    );
774                }
775                if let Some(ref rt) = c.return_type {
776                    check_type_refs_with_generics(
777                        rt,
778                        &table,
779                        &type_alias_arities,
780                        &mut errors,
781                        &generics,
782                    );
783                }
784                if !doc_mode {
785                    check_effect_grants_for(&c.name, c.span.line, &c.effects, &table, &mut errors);
786                }
787            }
788            Item::Agent(a) => {
789                for c in &a.cells {
790                    if c.body.is_empty() {
791                        continue;
792                    }
793                    check_generic_param_bounds(&c.generic_params, &table, &mut errors);
794                    let generics: Vec<String> =
795                        c.generic_params.iter().map(|g| g.name.clone()).collect();
796                    for p in &c.params {
797                        check_type_refs_with_generics(
798                            &p.ty,
799                            &table,
800                            &type_alias_arities,
801                            &mut errors,
802                            &generics,
803                        );
804                    }
805                    if let Some(ref rt) = c.return_type {
806                        check_type_refs_with_generics(
807                            rt,
808                            &table,
809                            &type_alias_arities,
810                            &mut errors,
811                            &generics,
812                        );
813                    }
814                    if !doc_mode {
815                        let fq = format!("{}.{}", a.name, c.name);
816                        check_effect_grants_for(&fq, c.span.line, &c.effects, &table, &mut errors);
817                    }
818                }
819            }
820            Item::Process(p) => {
821                if p.kind == "pipeline" {
822                    validate_pipeline_stages(p, &table, &mut errors);
823                }
824                if p.kind == "machine" {
825                    validate_machine_graph(p, &mut errors);
826                    for state in &p.machine_states {
827                        for param in &state.params {
828                            check_type_refs_with_generics(
829                                &param.ty,
830                                &table,
831                                &type_alias_arities,
832                                &mut errors,
833                                &[],
834                            );
835                        }
836                    }
837                }
838                for c in &p.cells {
839                    if c.body.is_empty() {
840                        continue;
841                    }
842                    check_generic_param_bounds(&c.generic_params, &table, &mut errors);
843                    let generics: Vec<String> =
844                        c.generic_params.iter().map(|g| g.name.clone()).collect();
845                    for par in &c.params {
846                        check_type_refs_with_generics(
847                            &par.ty,
848                            &table,
849                            &type_alias_arities,
850                            &mut errors,
851                            &generics,
852                        );
853                    }
854                    if let Some(ref rt) = c.return_type {
855                        check_type_refs_with_generics(
856                            rt,
857                            &table,
858                            &type_alias_arities,
859                            &mut errors,
860                            &generics,
861                        );
862                    }
863                    if !doc_mode {
864                        let fq = format!("{}.{}", p.name, c.name);
865                        check_effect_grants_for(&fq, c.span.line, &c.effects, &table, &mut errors);
866                    }
867                }
868                for g in &p.grants {
869                    table.tools.entry(g.tool_alias.clone()).or_insert(ToolInfo {
870                        tool_path: g.tool_alias.to_lowercase(),
871                        mcp_url: None,
872                    });
873                }
874            }
875            Item::Effect(e) => {
876                for c in &e.operations {
877                    check_generic_param_bounds(&c.generic_params, &table, &mut errors);
878                    let generics: Vec<String> =
879                        c.generic_params.iter().map(|g| g.name.clone()).collect();
880                    for p in &c.params {
881                        check_type_refs_with_generics(
882                            &p.ty,
883                            &table,
884                            &type_alias_arities,
885                            &mut errors,
886                            &generics,
887                        );
888                    }
889                    if let Some(ref rt) = c.return_type {
890                        check_type_refs_with_generics(
891                            rt,
892                            &table,
893                            &type_alias_arities,
894                            &mut errors,
895                            &generics,
896                        );
897                    }
898                }
899            }
900            Item::EffectBind(b) => {
901                table.tools.entry(b.tool_alias.clone()).or_insert(ToolInfo {
902                    tool_path: b.tool_alias.to_lowercase(),
903                    mcp_url: None,
904                });
905            }
906            Item::Handler(h) => {
907                for c in &h.handles {
908                    check_generic_param_bounds(&c.generic_params, &table, &mut errors);
909                    let generics: Vec<String> =
910                        c.generic_params.iter().map(|g| g.name.clone()).collect();
911                    for p in &c.params {
912                        check_type_refs_with_generics(
913                            &p.ty,
914                            &table,
915                            &type_alias_arities,
916                            &mut errors,
917                            &generics,
918                        );
919                    }
920                    if let Some(ref rt) = c.return_type {
921                        check_type_refs_with_generics(
922                            rt,
923                            &table,
924                            &type_alias_arities,
925                            &mut errors,
926                            &generics,
927                        );
928                    }
929                    if !doc_mode && !c.body.is_empty() {
930                        let fq = format!("{}.{}", h.name, c.name);
931                        check_effect_grants_for(&fq, c.span.line, &c.effects, &table, &mut errors);
932                    }
933                }
934            }
935            Item::Trait(t) => {
936                for parent in &t.parent_traits {
937                    if !table.traits.contains_key(parent) {
938                        errors.push(ResolveError::UndefinedTrait {
939                            name: parent.clone(),
940                            line: t.span.line,
941                        });
942                    }
943                }
944                for method in &t.methods {
945                    check_generic_param_bounds(&method.generic_params, &table, &mut errors);
946                    let generics: Vec<String> = method
947                        .generic_params
948                        .iter()
949                        .map(|g| g.name.clone())
950                        .collect();
951                    for p in &method.params {
952                        check_type_refs_with_generics(
953                            &p.ty,
954                            &table,
955                            &type_alias_arities,
956                            &mut errors,
957                            &generics,
958                        );
959                    }
960                    if let Some(ref rt) = method.return_type {
961                        check_type_refs_with_generics(
962                            rt,
963                            &table,
964                            &type_alias_arities,
965                            &mut errors,
966                            &generics,
967                        );
968                    }
969                }
970            }
971            Item::Impl(i) => {
972                check_generic_param_bounds(&i.generic_params, &table, &mut errors);
973                let impl_generics: Vec<String> =
974                    i.generic_params.iter().map(|g| g.name.clone()).collect();
975                check_impl_target_type_refs(
976                    i,
977                    &table,
978                    &type_alias_arities,
979                    &mut errors,
980                    &impl_generics,
981                );
982                for method in &i.cells {
983                    check_generic_param_bounds(&method.generic_params, &table, &mut errors);
984                    let mut generics = impl_generics.clone();
985                    generics.extend(method.generic_params.iter().map(|g| g.name.clone()));
986                    for p in &method.params {
987                        check_type_refs_with_generics(
988                            &p.ty,
989                            &table,
990                            &type_alias_arities,
991                            &mut errors,
992                            &generics,
993                        );
994                    }
995                    if let Some(ref rt) = method.return_type {
996                        check_type_refs_with_generics(
997                            rt,
998                            &table,
999                            &type_alias_arities,
1000                            &mut errors,
1001                            &generics,
1002                        );
1003                    }
1004                }
1005
1006                let Some(_) = table.traits.get(&i.trait_name) else {
1007                    errors.push(ResolveError::UndefinedTrait {
1008                        name: i.trait_name.clone(),
1009                        line: i.span.line,
1010                    });
1011                    continue;
1012                };
1013
1014                let required = collect_required_trait_methods(&i.trait_name, &table);
1015                let implemented: HashSet<&str> = i.cells.iter().map(|m| m.name.as_str()).collect();
1016                let missing: Vec<String> = required
1017                    .into_iter()
1018                    .filter(|name| !implemented.contains(name.as_str()))
1019                    .collect();
1020                if !missing.is_empty() {
1021                    errors.push(ResolveError::TraitMissingMethods {
1022                        trait_name: i.trait_name.clone(),
1023                        target_type: i.target_type.clone(),
1024                        missing,
1025                        line: i.span.line,
1026                    });
1027                }
1028
1029                let mut implemented_methods: HashMap<&str, &CellDef> = HashMap::new();
1030                for method in &i.cells {
1031                    implemented_methods
1032                        .entry(method.name.as_str())
1033                        .or_insert(method);
1034                }
1035
1036                for required_method in
1037                    collect_required_trait_method_defs(&i.trait_name, &trait_defs)
1038                {
1039                    let Some(actual_method) =
1040                        implemented_methods.get(required_method.name.as_str())
1041                    else {
1042                        continue;
1043                    };
1044                    if let Some(reason) =
1045                        trait_method_signature_mismatch_reason(required_method, actual_method)
1046                    {
1047                        errors.push(ResolveError::TraitMethodSignatureMismatch {
1048                            trait_name: i.trait_name.clone(),
1049                            target_type: i.target_type.clone(),
1050                            method: required_method.name.clone(),
1051                            reason,
1052                            expected: format_method_signature(required_method),
1053                            actual: format_method_signature(actual_method),
1054                            line: actual_method.span.line,
1055                        });
1056                    }
1057                }
1058            }
1059            Item::Grant(g) => {
1060                table.tools.entry(g.tool_alias.clone()).or_insert(ToolInfo {
1061                    tool_path: g.tool_alias.to_lowercase(),
1062                    mcp_url: None,
1063                });
1064            }
1065            Item::TypeAlias(ta) => {
1066                check_generic_param_bounds(&ta.generic_params, &table, &mut errors);
1067                let generics: Vec<String> =
1068                    ta.generic_params.iter().map(|g| g.name.clone()).collect();
1069                check_type_refs_with_generics(
1070                    &ta.type_expr,
1071                    &table,
1072                    &type_alias_arities,
1073                    &mut errors,
1074                    &generics,
1075                );
1076            }
1077            Item::Addon(_) => {}
1078            _ => {}
1079        }
1080    }
1081
1082    apply_effect_inference(program, &mut table, &mut errors);
1083
1084    if errors.is_empty() {
1085        Ok(table)
1086    } else {
1087        Err(errors)
1088    }
1089}
1090
1091fn check_generic_param_bounds(
1092    params: &[GenericParam],
1093    table: &SymbolTable,
1094    errors: &mut Vec<ResolveError>,
1095) {
1096    for param in params {
1097        for bound in &param.bounds {
1098            if !table.traits.contains_key(bound) {
1099                errors.push(ResolveError::UndefinedTrait {
1100                    name: bound.clone(),
1101                    line: param.span.line,
1102                });
1103            }
1104        }
1105    }
1106}
1107
1108fn check_impl_target_type_refs(
1109    impl_decl: &ImplDef,
1110    table: &SymbolTable,
1111    type_alias_arities: &HashMap<String, usize>,
1112    errors: &mut Vec<ResolveError>,
1113    generics: &[String],
1114) {
1115    if !impl_decl.trait_name.is_empty() && !table.traits.contains_key(&impl_decl.trait_name) {
1116        errors.push(ResolveError::UndefinedTrait {
1117            name: impl_decl.trait_name.clone(),
1118            line: impl_decl.span.line,
1119        });
1120    }
1121
1122    let target = TypeExpr::Named(impl_decl.target_type.clone(), impl_decl.span);
1123    check_type_refs_with_generics(&target, table, type_alias_arities, errors, generics);
1124}
1125
1126fn check_effect_grants_for(
1127    cell_name: &str,
1128    line: usize,
1129    effects: &[String],
1130    table: &SymbolTable,
1131    errors: &mut Vec<ResolveError>,
1132) {
1133    if effects.is_empty() {
1134        return;
1135    }
1136    let policies = table
1137        .cell_policies
1138        .get(cell_name)
1139        .cloned()
1140        .unwrap_or_default();
1141    let effect_bind_map = build_effect_bind_map(table);
1142
1143    for effect in effects {
1144        let effect = normalize_effect(effect);
1145        if matches!(
1146            effect.as_str(),
1147            "pure" | "trace" | "state" | "approve" | "emit" | "cache" | "async" | "random" | "time"
1148        ) {
1149            continue;
1150        }
1151
1152        let satisfied =
1153            is_effect_satisfied_by_policies(&effect, table, &policies, &effect_bind_map);
1154
1155        if !satisfied {
1156            errors.push(ResolveError::MissingEffectGrant {
1157                cell: cell_name.to_string(),
1158                effect,
1159                line,
1160            });
1161        }
1162    }
1163}
1164
1165fn build_effect_bind_map(table: &SymbolTable) -> HashMap<String, BTreeSet<String>> {
1166    let mut map: HashMap<String, BTreeSet<String>> = HashMap::new();
1167    for bind in &table.effect_binds {
1168        let root = bind
1169            .effect_path
1170            .split('.')
1171            .next()
1172            .unwrap_or(bind.effect_path.as_str())
1173            .to_ascii_lowercase();
1174        map.entry(root).or_default().insert(bind.tool_alias.clone());
1175    }
1176    map
1177}
1178
1179fn parse_policy_effects_from_expr(expr: &Expr, out: &mut BTreeSet<String>) {
1180    match expr {
1181        Expr::StringLit(s, _) => {
1182            for part in s.split(',') {
1183                let normalized = normalize_effect(part);
1184                if !normalized.is_empty() {
1185                    out.insert(normalized);
1186                }
1187            }
1188        }
1189        Expr::Ident(name, _) => {
1190            let normalized = normalize_effect(name);
1191            if !normalized.is_empty() {
1192                out.insert(normalized);
1193            }
1194        }
1195        Expr::ListLit(items, _) | Expr::SetLit(items, _) | Expr::TupleLit(items, _) => {
1196            for item in items {
1197                parse_policy_effects_from_expr(item, out);
1198            }
1199        }
1200        _ => {}
1201    }
1202}
1203
1204fn grant_to_policy(grant: &GrantDecl) -> GrantPolicy {
1205    let mut declared_effects = BTreeSet::new();
1206    let mut has_effect_clause = false;
1207
1208    for constraint in &grant.constraints {
1209        let key = constraint.key.to_ascii_lowercase();
1210        if key == "effect" || key == "effects" {
1211            has_effect_clause = true;
1212            parse_policy_effects_from_expr(&constraint.value, &mut declared_effects);
1213        }
1214    }
1215
1216    GrantPolicy {
1217        tool_alias: grant.tool_alias.clone(),
1218        allowed_effects: if has_effect_clause {
1219            Some(declared_effects)
1220        } else {
1221            None
1222        },
1223    }
1224}
1225
1226fn build_cell_policies(program: &Program) -> HashMap<String, Vec<GrantPolicy>> {
1227    let mut map: HashMap<String, Vec<GrantPolicy>> = HashMap::new();
1228    let mut global_policies: Vec<GrantPolicy> = Vec::new();
1229
1230    for item in &program.items {
1231        if let Item::Grant(g) = item {
1232            global_policies.push(grant_to_policy(g));
1233        }
1234    }
1235
1236    for item in &program.items {
1237        match item {
1238            Item::Cell(c) => {
1239                map.insert(c.name.clone(), global_policies.clone());
1240            }
1241            Item::Agent(a) => {
1242                let mut scoped = global_policies.clone();
1243                scoped.extend(a.grants.iter().map(grant_to_policy));
1244                for c in &a.cells {
1245                    map.insert(format!("{}.{}", a.name, c.name), scoped.clone());
1246                }
1247            }
1248            Item::Process(p) => {
1249                let mut scoped = global_policies.clone();
1250                scoped.extend(p.grants.iter().map(grant_to_policy));
1251                for c in &p.cells {
1252                    map.insert(format!("{}.{}", p.name, c.name), scoped.clone());
1253                }
1254            }
1255            Item::Effect(e) => {
1256                for op in &e.operations {
1257                    map.insert(format!("{}.{}", e.name, op.name), global_policies.clone());
1258                }
1259            }
1260            Item::Handler(h) => {
1261                for handle in &h.handles {
1262                    map.insert(
1263                        format!("{}.{}", h.name, handle.name),
1264                        global_policies.clone(),
1265                    );
1266                }
1267            }
1268            _ => {}
1269        }
1270    }
1271
1272    map
1273}
1274
1275fn is_effect_satisfied_by_policies(
1276    effect: &str,
1277    table: &SymbolTable,
1278    policies: &[GrantPolicy],
1279    effect_bind_map: &HashMap<String, BTreeSet<String>>,
1280) -> bool {
1281    if policies.is_empty() {
1282        return false;
1283    }
1284
1285    for policy in policies {
1286        let alias = &policy.tool_alias;
1287        if !table.tools.contains_key(alias) {
1288            continue;
1289        }
1290
1291        let bound_to_alias = effect_bind_map
1292            .get(effect)
1293            .map(|aliases| aliases.contains(alias))
1294            .unwrap_or(false);
1295
1296        if let Some(allowed) = &policy.allowed_effects {
1297            if allowed.contains(effect) || bound_to_alias {
1298                return true;
1299            }
1300            continue;
1301        }
1302
1303        if bound_to_alias {
1304            return true;
1305        }
1306
1307        // Unrestricted policies allow external effects by default.
1308        return true;
1309    }
1310
1311    false
1312}
1313
1314#[derive(Debug, Clone)]
1315struct EffectCell {
1316    name: String,
1317    declared: Vec<String>,
1318    body: Vec<Stmt>,
1319    line: usize,
1320}
1321
1322fn normalize_effect(effect: &str) -> String {
1323    effect.trim().to_ascii_lowercase()
1324}
1325
1326fn normalized_non_pure_effects(effects: &[String]) -> BTreeSet<String> {
1327    effects
1328        .iter()
1329        .map(|e| normalize_effect(e))
1330        .filter(|e| !e.is_empty() && e != "pure")
1331        .collect()
1332}
1333
1334fn parse_directive_bool(program: &Program, name: &str) -> Option<bool> {
1335    if let Some(directive) = program
1336        .directives
1337        .iter()
1338        .find(|d| d.name.eq_ignore_ascii_case(name))
1339    {
1340        let raw = directive
1341            .value
1342            .as_deref()
1343            .unwrap_or("true")
1344            .trim()
1345            .to_ascii_lowercase();
1346        return match raw.as_str() {
1347            "1" | "true" | "yes" | "on" => Some(true),
1348            "0" | "false" | "no" | "off" => Some(false),
1349            _ => None,
1350        };
1351    }
1352
1353    // Support attribute-style toggles in source snippets, e.g. `@deterministic`.
1354    let has_attr = program.items.iter().any(|item| {
1355        matches!(
1356            item,
1357            Item::Addon(AddonDecl {
1358                kind,
1359                name: Some(attr_name),
1360                ..
1361            }) if kind == "attribute" && attr_name.eq_ignore_ascii_case(name)
1362        )
1363    });
1364    if has_attr {
1365        Some(true)
1366    } else {
1367        None
1368    }
1369}
1370
1371fn validate_machine_graph(process: &ProcessDecl, errors: &mut Vec<ResolveError>) {
1372    if process.machine_states.is_empty() {
1373        return;
1374    }
1375
1376    let state_names: HashSet<String> = process
1377        .machine_states
1378        .iter()
1379        .map(|s| s.name.clone())
1380        .collect();
1381    let initial = process
1382        .machine_initial
1383        .clone()
1384        .or_else(|| process.machine_states.first().map(|s| s.name.clone()))
1385        .unwrap_or_default();
1386
1387    if !state_names.contains(&initial) {
1388        errors.push(ResolveError::MachineUnknownInitial {
1389            machine: process.name.clone(),
1390            state: initial.clone(),
1391            line: process.span.line,
1392        });
1393        return;
1394    }
1395
1396    for state in &process.machine_states {
1397        if let Some(guard) = &state.guard {
1398            if !is_supported_machine_expr(guard) {
1399                errors.push(ResolveError::MachineUnsupportedExpr {
1400                    machine: process.name.clone(),
1401                    state: state.name.clone(),
1402                    context: "guard".to_string(),
1403                    line: guard.span().line,
1404                });
1405            } else {
1406                let scope: HashMap<String, TypeExpr> = state
1407                    .params
1408                    .iter()
1409                    .map(|p| (p.name.clone(), p.ty.clone()))
1410                    .collect();
1411                let guard_ty = infer_machine_expr_type(guard, &scope);
1412                if !matches!(guard_ty.as_deref(), Some("Bool") | Some("Any")) {
1413                    errors.push(ResolveError::MachineGuardType {
1414                        machine: process.name.clone(),
1415                        state: state.name.clone(),
1416                        actual: guard_ty.unwrap_or_else(|| "Unknown".to_string()),
1417                        line: guard.span().line,
1418                    });
1419                }
1420            }
1421        }
1422        if let Some(target) = &state.transition_to {
1423            if !state_names.contains(target) {
1424                errors.push(ResolveError::MachineUnknownTransition {
1425                    machine: process.name.clone(),
1426                    state: state.name.clone(),
1427                    target: target.clone(),
1428                    line: state.span.line,
1429                });
1430            } else if let Some(target_state) =
1431                process.machine_states.iter().find(|s| s.name == *target)
1432            {
1433                if state.transition_args.len() != target_state.params.len() {
1434                    errors.push(ResolveError::MachineTransitionArgCount {
1435                        machine: process.name.clone(),
1436                        state: state.name.clone(),
1437                        target: target.clone(),
1438                        expected: target_state.params.len(),
1439                        actual: state.transition_args.len(),
1440                        line: state.span.line,
1441                    });
1442                } else {
1443                    let source_scope: HashMap<String, TypeExpr> = state
1444                        .params
1445                        .iter()
1446                        .map(|p| (p.name.clone(), p.ty.clone()))
1447                        .collect();
1448                    for (idx, arg) in state.transition_args.iter().enumerate() {
1449                        if !is_supported_machine_expr(arg) {
1450                            errors.push(ResolveError::MachineUnsupportedExpr {
1451                                machine: process.name.clone(),
1452                                state: state.name.clone(),
1453                                context: format!("transition arg {}", idx + 1),
1454                                line: arg.span().line,
1455                            });
1456                            continue;
1457                        }
1458                        let actual = infer_machine_expr_type(arg, &source_scope)
1459                            .unwrap_or_else(|| "Unknown".to_string());
1460                        let expected_ty = &target_state.params[idx].ty;
1461                        if !machine_type_compatible(expected_ty, &actual) {
1462                            errors.push(ResolveError::MachineTransitionArgType {
1463                                machine: process.name.clone(),
1464                                state: state.name.clone(),
1465                                target: target.clone(),
1466                                expected: machine_type_key(expected_ty),
1467                                actual,
1468                                line: arg.span().line,
1469                            });
1470                        }
1471                    }
1472                }
1473            }
1474        }
1475    }
1476
1477    let mut reachable = HashSet::new();
1478    let mut cursor = Some(initial.clone());
1479    while let Some(state_name) = cursor {
1480        if !reachable.insert(state_name.clone()) {
1481            break;
1482        }
1483        cursor = process
1484            .machine_states
1485            .iter()
1486            .find(|s| s.name == state_name)
1487            .and_then(|s| s.transition_to.clone());
1488    }
1489
1490    for state in &process.machine_states {
1491        if !reachable.contains(&state.name) {
1492            errors.push(ResolveError::MachineUnreachableState {
1493                machine: process.name.clone(),
1494                state: state.name.clone(),
1495                initial: initial.clone(),
1496                line: state.span.line,
1497            });
1498        }
1499    }
1500
1501    if !process.machine_states.iter().any(|s| s.terminal) {
1502        errors.push(ResolveError::MachineMissingTerminal {
1503            machine: process.name.clone(),
1504            line: process.span.line,
1505        });
1506    }
1507}
1508
1509fn validate_pipeline_stages(
1510    process: &ProcessDecl,
1511    table: &SymbolTable,
1512    errors: &mut Vec<ResolveError>,
1513) {
1514    if process.pipeline_stages.is_empty() {
1515        return;
1516    }
1517
1518    let mut previous_output: Option<TypeExpr> = None;
1519    let mut previous_stage: Option<String> = None;
1520    for stage in &process.pipeline_stages {
1521        let Some(cell) = table.cells.get(stage) else {
1522            errors.push(ResolveError::PipelineUnknownStage {
1523                pipeline: process.name.clone(),
1524                stage: stage.clone(),
1525                line: process.span.line,
1526            });
1527            previous_output = None;
1528            previous_stage = Some(stage.clone());
1529            continue;
1530        };
1531
1532        let non_self_params: Vec<&(String, TypeExpr)> = cell
1533            .params
1534            .iter()
1535            .filter(|(name, _)| name != "self")
1536            .collect();
1537        if non_self_params.len() != 1 {
1538            errors.push(ResolveError::PipelineStageArity {
1539                pipeline: process.name.clone(),
1540                stage: stage.clone(),
1541                line: process.span.line,
1542            });
1543        } else if let Some(prev_out) = previous_output.as_ref() {
1544            let expected = &non_self_params[0].1;
1545            if !pipeline_type_compatible(expected, prev_out) {
1546                errors.push(ResolveError::PipelineStageTypeMismatch {
1547                    pipeline: process.name.clone(),
1548                    from_stage: previous_stage
1549                        .clone()
1550                        .unwrap_or_else(|| "<entry>".to_string()),
1551                    to_stage: stage.clone(),
1552                    expected: machine_type_key(expected),
1553                    actual: machine_type_key(prev_out),
1554                    line: process.span.line,
1555                });
1556            }
1557        }
1558
1559        previous_output = Some(
1560            cell.return_type
1561                .clone()
1562                .unwrap_or(TypeExpr::Named("Any".to_string(), process.span)),
1563        );
1564        previous_stage = Some(stage.clone());
1565    }
1566}
1567
1568fn pipeline_type_compatible(expected: &TypeExpr, actual: &TypeExpr) -> bool {
1569    match expected {
1570        TypeExpr::Named(name, _) if name == "Any" => true,
1571        TypeExpr::Union(types, _) => types
1572            .iter()
1573            .any(|candidate| pipeline_type_compatible(candidate, actual)),
1574        _ => {
1575            let actual_key = machine_type_key(actual);
1576            if actual_key == "Any" {
1577                true
1578            } else {
1579                machine_type_key(expected) == actual_key
1580            }
1581        }
1582    }
1583}
1584
1585fn machine_type_key(ty: &TypeExpr) -> String {
1586    match ty {
1587        TypeExpr::Named(name, _) => name.clone(),
1588        TypeExpr::List(inner, _) => format!("list[{}]", machine_type_key(inner)),
1589        TypeExpr::Map(k, v, _) => format!("map[{},{}]", machine_type_key(k), machine_type_key(v)),
1590        TypeExpr::Result(ok, err, _) => {
1591            format!("result[{},{}]", machine_type_key(ok), machine_type_key(err))
1592        }
1593        TypeExpr::Union(types, _) => types
1594            .iter()
1595            .map(machine_type_key)
1596            .collect::<Vec<_>>()
1597            .join("|"),
1598        TypeExpr::Null(_) => "Null".to_string(),
1599        TypeExpr::Tuple(types, _) => {
1600            let inner = types
1601                .iter()
1602                .map(machine_type_key)
1603                .collect::<Vec<_>>()
1604                .join(",");
1605            format!("({})", inner)
1606        }
1607        TypeExpr::Set(inner, _) => format!("set[{}]", machine_type_key(inner)),
1608        TypeExpr::Fn(_, _, _, _) => "fn".to_string(),
1609        TypeExpr::Generic(name, _, _) => name.clone(),
1610    }
1611}
1612
1613fn machine_type_compatible(expected: &TypeExpr, actual_key: &str) -> bool {
1614    if actual_key == "Any" {
1615        return true;
1616    }
1617    match expected {
1618        TypeExpr::Named(name, _) if name == "Any" => true,
1619        TypeExpr::Union(types, _) => types
1620            .iter()
1621            .any(|candidate| machine_type_compatible(candidate, actual_key)),
1622        _ => machine_type_key(expected) == actual_key,
1623    }
1624}
1625
1626fn is_supported_machine_expr(expr: &Expr) -> bool {
1627    match expr {
1628        Expr::IntLit(_, _)
1629        | Expr::FloatLit(_, _)
1630        | Expr::StringLit(_, _)
1631        | Expr::BoolLit(_, _)
1632        | Expr::NullLit(_) => true,
1633        Expr::Ident(_, _) => true,
1634        Expr::UnaryOp(_, inner, _) => is_supported_machine_expr(inner),
1635        Expr::BinOp(lhs, _, rhs, _) => {
1636            is_supported_machine_expr(lhs) && is_supported_machine_expr(rhs)
1637        }
1638        _ => false,
1639    }
1640}
1641
1642fn infer_machine_expr_type(expr: &Expr, scope: &HashMap<String, TypeExpr>) -> Option<String> {
1643    match expr {
1644        Expr::IntLit(_, _) => Some("Int".to_string()),
1645        Expr::FloatLit(_, _) => Some("Float".to_string()),
1646        Expr::StringLit(_, _) => Some("String".to_string()),
1647        Expr::BoolLit(_, _) => Some("Bool".to_string()),
1648        Expr::NullLit(_) => Some("Null".to_string()),
1649        Expr::Ident(name, _) => scope
1650            .get(name)
1651            .map(machine_type_key)
1652            .or_else(|| Some("Any".to_string())),
1653        Expr::UnaryOp(UnaryOp::Not, inner, _) => {
1654            let inner_ty = infer_machine_expr_type(inner, scope).unwrap_or_else(|| "Any".into());
1655            if inner_ty == "Bool" || inner_ty == "Any" {
1656                Some("Bool".to_string())
1657            } else {
1658                Some("Any".to_string())
1659            }
1660        }
1661        Expr::UnaryOp(UnaryOp::Neg, inner, _) => {
1662            let inner_ty = infer_machine_expr_type(inner, scope).unwrap_or_else(|| "Any".into());
1663            if inner_ty == "Int" || inner_ty == "Float" {
1664                Some(inner_ty)
1665            } else {
1666                Some("Any".to_string())
1667            }
1668        }
1669        Expr::UnaryOp(UnaryOp::BitNot, _inner, _) => Some("Int".to_string()),
1670        Expr::BinOp(lhs, op, rhs, _) => {
1671            let lt = infer_machine_expr_type(lhs, scope).unwrap_or_else(|| "Any".into());
1672            let rt = infer_machine_expr_type(rhs, scope).unwrap_or_else(|| "Any".into());
1673            match op {
1674                BinOp::Add
1675                | BinOp::Sub
1676                | BinOp::Mul
1677                | BinOp::Div
1678                | BinOp::FloorDiv
1679                | BinOp::Mod
1680                | BinOp::Pow => {
1681                    if lt == "Float" || rt == "Float" {
1682                        Some("Float".to_string())
1683                    } else if lt == "Int" && rt == "Int" {
1684                        Some("Int".to_string())
1685                    } else {
1686                        Some("Any".to_string())
1687                    }
1688                }
1689                BinOp::Eq
1690                | BinOp::NotEq
1691                | BinOp::Lt
1692                | BinOp::LtEq
1693                | BinOp::Gt
1694                | BinOp::GtEq
1695                | BinOp::And
1696                | BinOp::Or
1697                | BinOp::In => Some("Bool".to_string()),
1698                BinOp::PipeForward | BinOp::Concat => Some("Any".to_string()),
1699                BinOp::BitAnd | BinOp::BitOr | BinOp::BitXor | BinOp::Shl | BinOp::Shr => {
1700                    Some("Int".to_string())
1701                }
1702            }
1703        }
1704        _ => None,
1705    }
1706}
1707
1708fn collect_effect_cells(program: &Program) -> Vec<EffectCell> {
1709    let mut out = Vec::new();
1710    for item in &program.items {
1711        match item {
1712            Item::Cell(c) => out.push(EffectCell {
1713                name: c.name.clone(),
1714                declared: c.effects.clone(),
1715                body: c.body.clone(),
1716                line: c.span.line,
1717            }),
1718            Item::Agent(a) => {
1719                for c in &a.cells {
1720                    out.push(EffectCell {
1721                        name: format!("{}.{}", a.name, c.name),
1722                        declared: c.effects.clone(),
1723                        body: c.body.clone(),
1724                        line: c.span.line,
1725                    });
1726                }
1727            }
1728            Item::Process(p) => {
1729                for c in &p.cells {
1730                    out.push(EffectCell {
1731                        name: format!("{}.{}", p.name, c.name),
1732                        declared: c.effects.clone(),
1733                        body: c.body.clone(),
1734                        line: c.span.line,
1735                    });
1736                }
1737            }
1738            Item::Effect(e) => {
1739                for op in &e.operations {
1740                    out.push(EffectCell {
1741                        name: format!("{}.{}", e.name, op.name),
1742                        declared: op.effects.clone(),
1743                        body: op.body.clone(),
1744                        line: op.span.line,
1745                    });
1746                }
1747            }
1748            Item::Handler(h) => {
1749                for handle in &h.handles {
1750                    out.push(EffectCell {
1751                        name: format!("{}.{}", h.name, handle.name),
1752                        declared: handle.effects.clone(),
1753                        body: handle.body.clone(),
1754                        line: handle.span.line,
1755                    });
1756                }
1757            }
1758            _ => {}
1759        }
1760    }
1761    out
1762}
1763
1764fn effect_from_tool(alias: &str, table: &SymbolTable) -> Option<String> {
1765    // Check explicit effect bindings (bind effect X to Y)
1766    if let Some(bind) = table.effect_binds.iter().find(|b| b.tool_alias == alias) {
1767        let root = bind
1768            .effect_path
1769            .split('.')
1770            .next()
1771            .unwrap_or(bind.effect_path.as_str());
1772        return Some(normalize_effect(root));
1773    }
1774
1775    // Check grant-declared effects for this tool
1776    for policy in table.cell_policies.values().flatten() {
1777        if policy.tool_alias == alias {
1778            if let Some(ref allowed) = policy.allowed_effects {
1779                if let Some(first) = allowed.iter().next() {
1780                    return Some(first.clone());
1781                }
1782            }
1783        }
1784    }
1785
1786    // No explicit effect declaration found -- caller decides the fallback
1787    None
1788}
1789
1790fn infer_pattern_effects(
1791    pat: &Pattern,
1792    table: &SymbolTable,
1793    current: &HashMap<String, BTreeSet<String>>,
1794    out: &mut BTreeSet<String>,
1795) {
1796    match pat {
1797        Pattern::Variant(_, Some(inner), _) => {
1798            infer_pattern_effects(inner, table, current, out);
1799        }
1800        Pattern::Guard {
1801            inner, condition, ..
1802        } => {
1803            infer_pattern_effects(inner, table, current, out);
1804            infer_expr_effects(condition, table, current, out);
1805        }
1806        Pattern::Or { patterns, .. } => {
1807            for p in patterns {
1808                infer_pattern_effects(p, table, current, out);
1809            }
1810        }
1811        Pattern::ListDestructure { elements, .. } | Pattern::TupleDestructure { elements, .. } => {
1812            for p in elements {
1813                infer_pattern_effects(p, table, current, out);
1814            }
1815        }
1816        Pattern::RecordDestructure { fields, .. } => {
1817            for (_, p) in fields {
1818                if let Some(p) = p {
1819                    infer_pattern_effects(p, table, current, out);
1820                }
1821            }
1822        }
1823        _ => {}
1824    }
1825}
1826
1827#[derive(Debug, Clone)]
1828struct CallRequirement {
1829    callee: String,
1830    effects: BTreeSet<String>,
1831    line: usize,
1832}
1833
1834#[derive(Debug, Clone)]
1835struct EffectEvidence {
1836    effect: String,
1837    line: usize,
1838    cause: String,
1839}
1840
1841fn push_effect_evidence(out: &mut Vec<EffectEvidence>, effect: &str, line: usize, cause: String) {
1842    let effect = normalize_effect(effect);
1843    if effect.is_empty() || effect == "pure" {
1844        return;
1845    }
1846    out.push(EffectEvidence {
1847        effect,
1848        line,
1849        cause,
1850    });
1851}
1852
1853fn resolve_call_target_effects(
1854    callee: &Expr,
1855    table: &SymbolTable,
1856) -> Option<(String, BTreeSet<String>)> {
1857    match callee {
1858        Expr::Ident(name, _) => {
1859            if let Some(info) = table.cells.get(name) {
1860                return Some((name.clone(), normalized_non_pure_effects(&info.effects)));
1861            }
1862            if table.tools.contains_key(name) {
1863                let mut effects = BTreeSet::new();
1864                effects.insert(
1865                    effect_from_tool(name, table).unwrap_or_else(|| "external".to_string()),
1866                );
1867                return Some((format!("tool {}", name), effects));
1868            }
1869            None
1870        }
1871        Expr::DotAccess(obj, field, _) => {
1872            if let Expr::Ident(owner, _) = obj.as_ref() {
1873                let fq = format!("{}.{}", owner, field);
1874                table
1875                    .cells
1876                    .get(&fq)
1877                    .map(|info| (fq, normalized_non_pure_effects(&info.effects)))
1878            } else {
1879                None
1880            }
1881        }
1882        _ => None,
1883    }
1884}
1885
1886fn resolve_tool_call_effect(callee: &Expr, table: &SymbolTable) -> (String, String) {
1887    match callee {
1888        Expr::Ident(alias, _) => {
1889            let effect = effect_from_tool(alias, table).unwrap_or_else(|| "external".into());
1890            (format!("tool {}", alias), effect)
1891        }
1892        _ => ("tool <dynamic>".into(), "external".into()),
1893    }
1894}
1895
1896fn desugar_pipe_application(
1897    input: &Expr,
1898    stage: &Expr,
1899    span: crate::compiler::tokens::Span,
1900) -> Expr {
1901    match stage {
1902        Expr::Call(callee, args, call_span) => {
1903            let mut call_args = Vec::with_capacity(args.len() + 1);
1904            call_args.push(CallArg::Positional(input.clone()));
1905            call_args.extend(args.clone());
1906            Expr::Call(callee.clone(), call_args, *call_span)
1907        }
1908        Expr::ToolCall(callee, args, call_span) => {
1909            let mut call_args = Vec::with_capacity(args.len() + 1);
1910            call_args.push(CallArg::Positional(input.clone()));
1911            call_args.extend(args.clone());
1912            Expr::ToolCall(callee.clone(), call_args, *call_span)
1913        }
1914        _ => Expr::Call(
1915            Box::new(stage.clone()),
1916            vec![CallArg::Positional(input.clone())],
1917            span,
1918        ),
1919    }
1920}
1921
1922fn collect_pattern_call_requirements(
1923    pat: &Pattern,
1924    table: &SymbolTable,
1925    out: &mut Vec<CallRequirement>,
1926) {
1927    match pat {
1928        Pattern::Variant(_, Some(inner), _) => {
1929            collect_pattern_call_requirements(inner, table, out);
1930        }
1931        Pattern::Guard {
1932            inner, condition, ..
1933        } => {
1934            collect_pattern_call_requirements(inner, table, out);
1935            collect_expr_call_requirements(condition, table, out);
1936        }
1937        Pattern::Or { patterns, .. } => {
1938            for p in patterns {
1939                collect_pattern_call_requirements(p, table, out);
1940            }
1941        }
1942        Pattern::ListDestructure { elements, .. } | Pattern::TupleDestructure { elements, .. } => {
1943            for p in elements {
1944                collect_pattern_call_requirements(p, table, out);
1945            }
1946        }
1947        Pattern::RecordDestructure { fields, .. } => {
1948            for (_, p) in fields {
1949                if let Some(p) = p {
1950                    collect_pattern_call_requirements(p, table, out);
1951                }
1952            }
1953        }
1954        _ => {}
1955    }
1956}
1957
1958fn collect_stmt_call_requirements(
1959    stmt: &Stmt,
1960    table: &SymbolTable,
1961    out: &mut Vec<CallRequirement>,
1962) {
1963    match stmt {
1964        Stmt::Let(s) => collect_expr_call_requirements(&s.value, table, out),
1965        Stmt::If(s) => {
1966            collect_expr_call_requirements(&s.condition, table, out);
1967            for st in &s.then_body {
1968                collect_stmt_call_requirements(st, table, out);
1969            }
1970            if let Some(else_body) = &s.else_body {
1971                for st in else_body {
1972                    collect_stmt_call_requirements(st, table, out);
1973                }
1974            }
1975        }
1976        Stmt::For(s) => {
1977            collect_expr_call_requirements(&s.iter, table, out);
1978            if let Some(filter) = &s.filter {
1979                collect_expr_call_requirements(filter, table, out);
1980            }
1981            for st in &s.body {
1982                collect_stmt_call_requirements(st, table, out);
1983            }
1984        }
1985        Stmt::Match(s) => {
1986            collect_expr_call_requirements(&s.subject, table, out);
1987            for arm in &s.arms {
1988                collect_pattern_call_requirements(&arm.pattern, table, out);
1989                for st in &arm.body {
1990                    collect_stmt_call_requirements(st, table, out);
1991                }
1992            }
1993        }
1994        Stmt::Return(s) => collect_expr_call_requirements(&s.value, table, out),
1995        Stmt::Halt(s) => collect_expr_call_requirements(&s.message, table, out),
1996        Stmt::Assign(s) => collect_expr_call_requirements(&s.value, table, out),
1997        Stmt::Expr(s) => collect_expr_call_requirements(&s.expr, table, out),
1998        Stmt::While(s) => {
1999            collect_expr_call_requirements(&s.condition, table, out);
2000            for st in &s.body {
2001                collect_stmt_call_requirements(st, table, out);
2002            }
2003        }
2004        Stmt::Loop(s) => {
2005            for st in &s.body {
2006                collect_stmt_call_requirements(st, table, out);
2007            }
2008        }
2009        Stmt::Emit(s) => collect_expr_call_requirements(&s.value, table, out),
2010        Stmt::CompoundAssign(s) => collect_expr_call_requirements(&s.value, table, out),
2011        Stmt::Break(_) | Stmt::Continue(_) => {}
2012        Stmt::Defer(s) => {
2013            for stmt in &s.body {
2014                collect_stmt_call_requirements(stmt, table, out);
2015            }
2016        }
2017    }
2018}
2019
2020fn collect_expr_call_requirements(
2021    expr: &Expr,
2022    table: &SymbolTable,
2023    out: &mut Vec<CallRequirement>,
2024) {
2025    match expr {
2026        Expr::BinOp(lhs, _, rhs, _) | Expr::NullCoalesce(lhs, rhs, _) => {
2027            collect_expr_call_requirements(lhs, table, out);
2028            collect_expr_call_requirements(rhs, table, out);
2029        }
2030        Expr::Pipe { left, right, span } => {
2031            let call_expr = desugar_pipe_application(left, right, *span);
2032            collect_expr_call_requirements(&call_expr, table, out);
2033        }
2034        Expr::Illuminate {
2035            input,
2036            transform,
2037            span,
2038        } => {
2039            let call_expr = desugar_pipe_application(input, transform, *span);
2040            collect_expr_call_requirements(&call_expr, table, out);
2041        }
2042        Expr::UnaryOp(_, inner, _)
2043        | Expr::ExpectSchema(inner, _, _)
2044        | Expr::TryExpr(inner, _)
2045        | Expr::AwaitExpr(inner, _)
2046        | Expr::NullAssert(inner, _)
2047        | Expr::SpreadExpr(inner, _)
2048        | Expr::IsType { expr: inner, .. }
2049        | Expr::TypeCast { expr: inner, .. } => collect_expr_call_requirements(inner, table, out),
2050        Expr::Call(callee, args, span) => {
2051            collect_expr_call_requirements(callee, table, out);
2052            for a in args {
2053                match a {
2054                    CallArg::Positional(e) | CallArg::Named(_, e, _) | CallArg::Role(_, e, _) => {
2055                        collect_expr_call_requirements(e, table, out)
2056                    }
2057                }
2058            }
2059            if let Some((target, effects)) = resolve_call_target_effects(callee, table) {
2060                if !effects.is_empty() {
2061                    out.push(CallRequirement {
2062                        callee: target,
2063                        effects,
2064                        line: span.line,
2065                    });
2066                }
2067            }
2068        }
2069        Expr::ToolCall(callee, args, span) => {
2070            for a in args {
2071                match a {
2072                    CallArg::Positional(e) | CallArg::Named(_, e, _) | CallArg::Role(_, e, _) => {
2073                        collect_expr_call_requirements(e, table, out)
2074                    }
2075                }
2076            }
2077            let (callee_name, effect) = resolve_tool_call_effect(callee, table);
2078            let mut effects = BTreeSet::new();
2079            effects.insert(normalize_effect(&effect));
2080            out.push(CallRequirement {
2081                callee: callee_name,
2082                effects,
2083                line: span.line,
2084            });
2085        }
2086        Expr::ListLit(items, _) | Expr::TupleLit(items, _) | Expr::SetLit(items, _) => {
2087            for e in items {
2088                collect_expr_call_requirements(e, table, out);
2089            }
2090        }
2091        Expr::MapLit(items, _) => {
2092            for (k, v) in items {
2093                collect_expr_call_requirements(k, table, out);
2094                collect_expr_call_requirements(v, table, out);
2095            }
2096        }
2097        Expr::RecordLit(_, fields, _) => {
2098            for (_, e) in fields {
2099                collect_expr_call_requirements(e, table, out);
2100            }
2101        }
2102        Expr::DotAccess(obj, _, _) | Expr::NullSafeAccess(obj, _, _) => {
2103            collect_expr_call_requirements(obj, table, out);
2104        }
2105        Expr::IndexAccess(obj, idx, _) | Expr::NullSafeIndex(obj, idx, _) => {
2106            collect_expr_call_requirements(obj, table, out);
2107            collect_expr_call_requirements(idx, table, out);
2108        }
2109        Expr::RoleBlock(_, inner, _) => collect_expr_call_requirements(inner, table, out),
2110        Expr::Lambda { body, .. } => match body {
2111            LambdaBody::Expr(e) => collect_expr_call_requirements(e, table, out),
2112            LambdaBody::Block(stmts) => {
2113                for s in stmts {
2114                    collect_stmt_call_requirements(s, table, out);
2115                }
2116            }
2117        },
2118        Expr::IfExpr {
2119            cond,
2120            then_val,
2121            else_val,
2122            ..
2123        } => {
2124            collect_expr_call_requirements(cond, table, out);
2125            collect_expr_call_requirements(then_val, table, out);
2126            collect_expr_call_requirements(else_val, table, out);
2127        }
2128        Expr::Comprehension {
2129            body,
2130            iter,
2131            condition,
2132            ..
2133        } => {
2134            collect_expr_call_requirements(iter, table, out);
2135            if let Some(c) = condition {
2136                collect_expr_call_requirements(c, table, out);
2137            }
2138            collect_expr_call_requirements(body, table, out);
2139        }
2140        Expr::RangeExpr {
2141            start, end, step, ..
2142        } => {
2143            if let Some(s) = start {
2144                collect_expr_call_requirements(s, table, out);
2145            }
2146            if let Some(e) = end {
2147                collect_expr_call_requirements(e, table, out);
2148            }
2149            if let Some(st) = step {
2150                collect_expr_call_requirements(st, table, out);
2151            }
2152        }
2153        Expr::MatchExpr { subject, arms, .. } => {
2154            collect_expr_call_requirements(subject, table, out);
2155            for arm in arms {
2156                for s in &arm.body {
2157                    collect_stmt_call_requirements(s, table, out);
2158                }
2159            }
2160        }
2161        Expr::BlockExpr(stmts, _) => {
2162            for s in stmts {
2163                collect_stmt_call_requirements(s, table, out);
2164            }
2165        }
2166        Expr::IntLit(_, _)
2167        | Expr::FloatLit(_, _)
2168        | Expr::StringLit(_, _)
2169        | Expr::StringInterp(_, _)
2170        | Expr::BoolLit(_, _)
2171        | Expr::NullLit(_)
2172        | Expr::Ident(_, _)
2173        | Expr::RawStringLit(_, _)
2174        | Expr::BytesLit(_, _) => {}
2175    }
2176}
2177
2178fn collect_pattern_effect_evidence(
2179    pat: &Pattern,
2180    table: &SymbolTable,
2181    current: &HashMap<String, BTreeSet<String>>,
2182    out: &mut Vec<EffectEvidence>,
2183) {
2184    match pat {
2185        Pattern::Variant(_, Some(inner), _) => {
2186            collect_pattern_effect_evidence(inner, table, current, out);
2187        }
2188        Pattern::Guard {
2189            inner, condition, ..
2190        } => {
2191            collect_pattern_effect_evidence(inner, table, current, out);
2192            collect_expr_effect_evidence(condition, table, current, out);
2193        }
2194        Pattern::Or { patterns, .. } => {
2195            for p in patterns {
2196                collect_pattern_effect_evidence(p, table, current, out);
2197            }
2198        }
2199        Pattern::ListDestructure { elements, .. } | Pattern::TupleDestructure { elements, .. } => {
2200            for p in elements {
2201                collect_pattern_effect_evidence(p, table, current, out);
2202            }
2203        }
2204        Pattern::RecordDestructure { fields, .. } => {
2205            for (_, p) in fields {
2206                if let Some(p) = p {
2207                    collect_pattern_effect_evidence(p, table, current, out);
2208                }
2209            }
2210        }
2211        _ => {}
2212    }
2213}
2214
2215fn collect_stmt_effect_evidence(
2216    stmt: &Stmt,
2217    table: &SymbolTable,
2218    current: &HashMap<String, BTreeSet<String>>,
2219    out: &mut Vec<EffectEvidence>,
2220) {
2221    match stmt {
2222        Stmt::Let(s) => collect_expr_effect_evidence(&s.value, table, current, out),
2223        Stmt::If(s) => {
2224            collect_expr_effect_evidence(&s.condition, table, current, out);
2225            for st in &s.then_body {
2226                collect_stmt_effect_evidence(st, table, current, out);
2227            }
2228            if let Some(else_body) = &s.else_body {
2229                for st in else_body {
2230                    collect_stmt_effect_evidence(st, table, current, out);
2231                }
2232            }
2233        }
2234        Stmt::For(s) => {
2235            collect_expr_effect_evidence(&s.iter, table, current, out);
2236            if let Some(filter) = &s.filter {
2237                collect_expr_effect_evidence(filter, table, current, out);
2238            }
2239            for st in &s.body {
2240                collect_stmt_effect_evidence(st, table, current, out);
2241            }
2242        }
2243        Stmt::Match(s) => {
2244            collect_expr_effect_evidence(&s.subject, table, current, out);
2245            for arm in &s.arms {
2246                collect_pattern_effect_evidence(&arm.pattern, table, current, out);
2247                for st in &arm.body {
2248                    collect_stmt_effect_evidence(st, table, current, out);
2249                }
2250            }
2251        }
2252        Stmt::Return(s) => collect_expr_effect_evidence(&s.value, table, current, out),
2253        Stmt::Halt(s) => collect_expr_effect_evidence(&s.message, table, current, out),
2254        Stmt::Assign(s) => collect_expr_effect_evidence(&s.value, table, current, out),
2255        Stmt::Expr(s) => collect_expr_effect_evidence(&s.expr, table, current, out),
2256        Stmt::While(s) => {
2257            collect_expr_effect_evidence(&s.condition, table, current, out);
2258            for st in &s.body {
2259                collect_stmt_effect_evidence(st, table, current, out);
2260            }
2261        }
2262        Stmt::Loop(s) => {
2263            for st in &s.body {
2264                collect_stmt_effect_evidence(st, table, current, out);
2265            }
2266        }
2267        Stmt::Emit(s) => {
2268            collect_expr_effect_evidence(&s.value, table, current, out);
2269            push_effect_evidence(out, "emit", s.span.line, "emit statement".to_string());
2270        }
2271        Stmt::CompoundAssign(s) => collect_expr_effect_evidence(&s.value, table, current, out),
2272        Stmt::Break(_) | Stmt::Continue(_) => {}
2273        Stmt::Defer(s) => {
2274            for stmt in &s.body {
2275                collect_stmt_effect_evidence(stmt, table, current, out);
2276            }
2277        }
2278    }
2279}
2280
2281fn collect_expr_effect_evidence(
2282    expr: &Expr,
2283    table: &SymbolTable,
2284    current: &HashMap<String, BTreeSet<String>>,
2285    out: &mut Vec<EffectEvidence>,
2286) {
2287    match expr {
2288        Expr::BinOp(lhs, _, rhs, _) | Expr::NullCoalesce(lhs, rhs, _) => {
2289            collect_expr_effect_evidence(lhs, table, current, out);
2290            collect_expr_effect_evidence(rhs, table, current, out);
2291        }
2292        Expr::Pipe { left, right, span } => {
2293            let call_expr = desugar_pipe_application(left, right, *span);
2294            collect_expr_effect_evidence(&call_expr, table, current, out);
2295        }
2296        Expr::Illuminate {
2297            input,
2298            transform,
2299            span,
2300        } => {
2301            let call_expr = desugar_pipe_application(input, transform, *span);
2302            collect_expr_effect_evidence(&call_expr, table, current, out);
2303        }
2304        Expr::UnaryOp(_, inner, _)
2305        | Expr::ExpectSchema(inner, _, _)
2306        | Expr::TryExpr(inner, _)
2307        | Expr::NullAssert(inner, _)
2308        | Expr::SpreadExpr(inner, _)
2309        | Expr::IsType { expr: inner, .. }
2310        | Expr::TypeCast { expr: inner, .. } => {
2311            collect_expr_effect_evidence(inner, table, current, out);
2312        }
2313        Expr::AwaitExpr(inner, span) => {
2314            collect_expr_effect_evidence(inner, table, current, out);
2315            push_effect_evidence(out, "async", span.line, "await expression".to_string());
2316        }
2317        Expr::Call(callee, args, span) => {
2318            collect_expr_effect_evidence(callee, table, current, out);
2319            for a in args {
2320                match a {
2321                    CallArg::Positional(e) | CallArg::Named(_, e, _) | CallArg::Role(_, e, _) => {
2322                        collect_expr_effect_evidence(e, table, current, out)
2323                    }
2324                }
2325            }
2326            match callee.as_ref() {
2327                Expr::Ident(name, _) => {
2328                    if let Some(effects) = current.get(name) {
2329                        for effect in effects {
2330                            push_effect_evidence(
2331                                out,
2332                                effect,
2333                                span.line,
2334                                format!("call to '{}'", name),
2335                            );
2336                        }
2337                    }
2338                    if table.tools.contains_key(name) {
2339                        let effect =
2340                            effect_from_tool(name, table).unwrap_or_else(|| "external".into());
2341                        push_effect_evidence(
2342                            out,
2343                            &effect,
2344                            span.line,
2345                            format!("tool call '{}'", name),
2346                        );
2347                    }
2348                    if name == "emit" || name == "print" {
2349                        push_effect_evidence(out, "emit", span.line, format!("call to '{}'", name));
2350                    }
2351                    if matches!(
2352                        name.as_str(),
2353                        "parallel" | "race" | "vote" | "select" | "timeout" | "spawn"
2354                    ) {
2355                        push_effect_evidence(
2356                            out,
2357                            "async",
2358                            span.line,
2359                            format!("call to '{}'", name),
2360                        );
2361                    }
2362                    if matches!(name.as_str(), "uuid" | "uuid_v4") {
2363                        push_effect_evidence(
2364                            out,
2365                            "random",
2366                            span.line,
2367                            format!("call to '{}'", name),
2368                        );
2369                    }
2370                    if matches!(name.as_str(), "timestamp") {
2371                        push_effect_evidence(out, "time", span.line, format!("call to '{}'", name));
2372                    }
2373                }
2374                Expr::DotAccess(obj, field, _) => {
2375                    if let Expr::Ident(owner, _) = obj.as_ref() {
2376                        let fq = format!("{}.{}", owner, field);
2377                        if let Some(effects) = current.get(&fq) {
2378                            for effect in effects {
2379                                push_effect_evidence(
2380                                    out,
2381                                    effect,
2382                                    span.line,
2383                                    format!("call to '{}'", fq),
2384                                );
2385                            }
2386                        }
2387                        if let Some(process) = table.processes.values().find(|p| p.name == *owner) {
2388                            match process.kind.as_str() {
2389                                "memory" => {
2390                                    if matches!(
2391                                        field.as_str(),
2392                                        "append"
2393                                            | "remember"
2394                                            | "upsert"
2395                                            | "store"
2396                                            | "recent"
2397                                            | "recall"
2398                                            | "query"
2399                                            | "get"
2400                                    ) {
2401                                        push_effect_evidence(
2402                                            out,
2403                                            "state",
2404                                            span.line,
2405                                            format!("process call '{}'", fq),
2406                                        );
2407                                    }
2408                                }
2409                                "machine" => {
2410                                    if matches!(
2411                                        field.as_str(),
2412                                        "run"
2413                                            | "start"
2414                                            | "step"
2415                                            | "is_terminal"
2416                                            | "current_state"
2417                                            | "resume_from"
2418                                    ) {
2419                                        push_effect_evidence(
2420                                            out,
2421                                            "state",
2422                                            span.line,
2423                                            format!("process call '{}'", fq),
2424                                        );
2425                                    }
2426                                }
2427                                _ => {}
2428                            }
2429                        }
2430                    }
2431                }
2432                _ => {}
2433            }
2434        }
2435        Expr::ToolCall(callee, args, span) => {
2436            for a in args {
2437                match a {
2438                    CallArg::Positional(e) | CallArg::Named(_, e, _) | CallArg::Role(_, e, _) => {
2439                        collect_expr_effect_evidence(e, table, current, out)
2440                    }
2441                }
2442            }
2443            match callee.as_ref() {
2444                Expr::Ident(alias, _) => {
2445                    let effect =
2446                        effect_from_tool(alias, table).unwrap_or_else(|| "external".into());
2447                    push_effect_evidence(out, &effect, span.line, format!("tool call '{}'", alias));
2448                }
2449                _ => push_effect_evidence(
2450                    out,
2451                    "external",
2452                    span.line,
2453                    "dynamic tool call".to_string(),
2454                ),
2455            }
2456        }
2457        Expr::ListLit(items, _) | Expr::TupleLit(items, _) | Expr::SetLit(items, _) => {
2458            for e in items {
2459                collect_expr_effect_evidence(e, table, current, out);
2460            }
2461        }
2462        Expr::MapLit(items, _) => {
2463            for (k, v) in items {
2464                collect_expr_effect_evidence(k, table, current, out);
2465                collect_expr_effect_evidence(v, table, current, out);
2466            }
2467        }
2468        Expr::RecordLit(_, fields, _) => {
2469            for (_, e) in fields {
2470                collect_expr_effect_evidence(e, table, current, out);
2471            }
2472        }
2473        Expr::DotAccess(obj, _, _) | Expr::NullSafeAccess(obj, _, _) => {
2474            collect_expr_effect_evidence(obj, table, current, out);
2475        }
2476        Expr::IndexAccess(obj, idx, _) | Expr::NullSafeIndex(obj, idx, _) => {
2477            collect_expr_effect_evidence(obj, table, current, out);
2478            collect_expr_effect_evidence(idx, table, current, out);
2479        }
2480        Expr::RoleBlock(_, inner, _) => collect_expr_effect_evidence(inner, table, current, out),
2481        Expr::Lambda { body, .. } => match body {
2482            LambdaBody::Expr(e) => collect_expr_effect_evidence(e, table, current, out),
2483            LambdaBody::Block(stmts) => {
2484                for s in stmts {
2485                    collect_stmt_effect_evidence(s, table, current, out);
2486                }
2487            }
2488        },
2489        Expr::IfExpr {
2490            cond,
2491            then_val,
2492            else_val,
2493            ..
2494        } => {
2495            collect_expr_effect_evidence(cond, table, current, out);
2496            collect_expr_effect_evidence(then_val, table, current, out);
2497            collect_expr_effect_evidence(else_val, table, current, out);
2498        }
2499        Expr::Comprehension {
2500            body,
2501            iter,
2502            condition,
2503            ..
2504        } => {
2505            collect_expr_effect_evidence(iter, table, current, out);
2506            if let Some(c) = condition {
2507                collect_expr_effect_evidence(c, table, current, out);
2508            }
2509            collect_expr_effect_evidence(body, table, current, out);
2510        }
2511        Expr::RangeExpr {
2512            start, end, step, ..
2513        } => {
2514            if let Some(s) = start {
2515                collect_expr_effect_evidence(s, table, current, out);
2516            }
2517            if let Some(e) = end {
2518                collect_expr_effect_evidence(e, table, current, out);
2519            }
2520            if let Some(st) = step {
2521                collect_expr_effect_evidence(st, table, current, out);
2522            }
2523        }
2524        Expr::MatchExpr { subject, arms, .. } => {
2525            collect_expr_effect_evidence(subject, table, current, out);
2526            for arm in arms {
2527                for s in &arm.body {
2528                    collect_stmt_effect_evidence(s, table, current, out);
2529                }
2530            }
2531        }
2532        Expr::BlockExpr(stmts, _) => {
2533            for s in stmts {
2534                collect_stmt_effect_evidence(s, table, current, out);
2535            }
2536        }
2537        Expr::IntLit(_, _)
2538        | Expr::FloatLit(_, _)
2539        | Expr::StringLit(_, _)
2540        | Expr::StringInterp(_, _)
2541        | Expr::BoolLit(_, _)
2542        | Expr::NullLit(_)
2543        | Expr::Ident(_, _)
2544        | Expr::RawStringLit(_, _)
2545        | Expr::BytesLit(_, _) => {}
2546    }
2547}
2548
2549fn collect_cell_effect_evidence(
2550    cell: &EffectCell,
2551    table: &SymbolTable,
2552    current: &HashMap<String, BTreeSet<String>>,
2553) -> HashMap<String, EffectEvidence> {
2554    let mut raw = Vec::new();
2555    for stmt in &cell.body {
2556        collect_stmt_effect_evidence(stmt, table, current, &mut raw);
2557    }
2558
2559    let mut by_effect: HashMap<String, EffectEvidence> = HashMap::new();
2560    for ev in raw {
2561        match by_effect.get(&ev.effect) {
2562            Some(existing) if existing.line <= ev.line => {}
2563            _ => {
2564                by_effect.insert(ev.effect.clone(), ev);
2565            }
2566        }
2567    }
2568    by_effect
2569}
2570
2571fn infer_stmt_effects(
2572    stmt: &Stmt,
2573    table: &SymbolTable,
2574    current: &HashMap<String, BTreeSet<String>>,
2575    out: &mut BTreeSet<String>,
2576) {
2577    match stmt {
2578        Stmt::Let(s) => infer_expr_effects(&s.value, table, current, out),
2579        Stmt::If(s) => {
2580            infer_expr_effects(&s.condition, table, current, out);
2581            for st in &s.then_body {
2582                infer_stmt_effects(st, table, current, out);
2583            }
2584            if let Some(else_body) = &s.else_body {
2585                for st in else_body {
2586                    infer_stmt_effects(st, table, current, out);
2587                }
2588            }
2589        }
2590        Stmt::For(s) => {
2591            infer_expr_effects(&s.iter, table, current, out);
2592            if let Some(filter) = &s.filter {
2593                infer_expr_effects(filter, table, current, out);
2594            }
2595            for st in &s.body {
2596                infer_stmt_effects(st, table, current, out);
2597            }
2598        }
2599        Stmt::Match(s) => {
2600            infer_expr_effects(&s.subject, table, current, out);
2601            for arm in &s.arms {
2602                infer_pattern_effects(&arm.pattern, table, current, out);
2603                for st in &arm.body {
2604                    infer_stmt_effects(st, table, current, out);
2605                }
2606            }
2607        }
2608        Stmt::Return(s) => infer_expr_effects(&s.value, table, current, out),
2609        Stmt::Halt(s) => infer_expr_effects(&s.message, table, current, out),
2610        Stmt::Assign(s) => infer_expr_effects(&s.value, table, current, out),
2611        Stmt::Expr(s) => infer_expr_effects(&s.expr, table, current, out),
2612        Stmt::While(s) => {
2613            infer_expr_effects(&s.condition, table, current, out);
2614            for st in &s.body {
2615                infer_stmt_effects(st, table, current, out);
2616            }
2617        }
2618        Stmt::Loop(s) => {
2619            for st in &s.body {
2620                infer_stmt_effects(st, table, current, out);
2621            }
2622        }
2623        Stmt::Emit(s) => {
2624            infer_expr_effects(&s.value, table, current, out);
2625            out.insert("emit".into());
2626        }
2627        Stmt::CompoundAssign(s) => infer_expr_effects(&s.value, table, current, out),
2628        Stmt::Break(_) | Stmt::Continue(_) => {}
2629        Stmt::Defer(s) => {
2630            for stmt in &s.body {
2631                infer_stmt_effects(stmt, table, current, out);
2632            }
2633        }
2634    }
2635}
2636
2637fn infer_expr_effects(
2638    expr: &Expr,
2639    table: &SymbolTable,
2640    current: &HashMap<String, BTreeSet<String>>,
2641    out: &mut BTreeSet<String>,
2642) {
2643    match expr {
2644        Expr::BinOp(lhs, _, rhs, _) | Expr::NullCoalesce(lhs, rhs, _) => {
2645            infer_expr_effects(lhs, table, current, out);
2646            infer_expr_effects(rhs, table, current, out);
2647        }
2648        Expr::Pipe { left, right, span } => {
2649            let call_expr = desugar_pipe_application(left, right, *span);
2650            infer_expr_effects(&call_expr, table, current, out);
2651        }
2652        Expr::Illuminate {
2653            input,
2654            transform,
2655            span,
2656        } => {
2657            let call_expr = desugar_pipe_application(input, transform, *span);
2658            infer_expr_effects(&call_expr, table, current, out);
2659        }
2660        Expr::UnaryOp(_, inner, _)
2661        | Expr::ExpectSchema(inner, _, _)
2662        | Expr::TryExpr(inner, _)
2663        | Expr::AwaitExpr(inner, _)
2664        | Expr::NullAssert(inner, _)
2665        | Expr::SpreadExpr(inner, _)
2666        | Expr::IsType { expr: inner, .. }
2667        | Expr::TypeCast { expr: inner, .. } => {
2668            infer_expr_effects(inner, table, current, out);
2669            if matches!(expr, Expr::AwaitExpr(_, _)) {
2670                out.insert("async".into());
2671            }
2672        }
2673        Expr::Call(callee, args, _) => {
2674            infer_expr_effects(callee, table, current, out);
2675            for a in args {
2676                match a {
2677                    CallArg::Positional(e) | CallArg::Named(_, e, _) | CallArg::Role(_, e, _) => {
2678                        infer_expr_effects(e, table, current, out)
2679                    }
2680                }
2681            }
2682            match callee.as_ref() {
2683                Expr::Ident(name, _) => {
2684                    if let Some(effects) = current.get(name) {
2685                        out.extend(effects.iter().cloned());
2686                    }
2687                    if table.tools.contains_key(name) {
2688                        if let Some(effect) = effect_from_tool(name, table) {
2689                            out.insert(effect);
2690                        } else {
2691                            out.insert("external".into());
2692                        }
2693                    }
2694                    if name == "emit" || name == "print" {
2695                        out.insert("emit".into());
2696                    }
2697                    if matches!(
2698                        name.as_str(),
2699                        "parallel" | "race" | "vote" | "select" | "timeout" | "spawn"
2700                    ) {
2701                        out.insert("async".into());
2702                    }
2703                    if matches!(name.as_str(), "uuid" | "uuid_v4") {
2704                        out.insert("random".into());
2705                    }
2706                    if matches!(name.as_str(), "timestamp") {
2707                        out.insert("time".into());
2708                    }
2709                }
2710                Expr::DotAccess(obj, field, _) => {
2711                    if let Expr::Ident(owner, _) = obj.as_ref() {
2712                        let fq = format!("{}.{}", owner, field);
2713                        if let Some(effects) = current.get(&fq) {
2714                            out.extend(effects.iter().cloned());
2715                        }
2716                        if let Some(process) = table.processes.values().find(|p| p.name == *owner) {
2717                            match process.kind.as_str() {
2718                                "memory" => {
2719                                    if matches!(
2720                                        field.as_str(),
2721                                        "append"
2722                                            | "remember"
2723                                            | "upsert"
2724                                            | "store"
2725                                            | "recent"
2726                                            | "recall"
2727                                            | "query"
2728                                            | "get"
2729                                    ) {
2730                                        out.insert("state".into());
2731                                    }
2732                                }
2733                                "machine" => {
2734                                    if matches!(
2735                                        field.as_str(),
2736                                        "run"
2737                                            | "start"
2738                                            | "step"
2739                                            | "is_terminal"
2740                                            | "current_state"
2741                                            | "resume_from"
2742                                    ) {
2743                                        out.insert("state".into());
2744                                    }
2745                                }
2746                                _ => {}
2747                            }
2748                        }
2749                    }
2750                }
2751                _ => {}
2752            }
2753        }
2754        Expr::ToolCall(callee, args, _) => {
2755            for a in args {
2756                match a {
2757                    CallArg::Positional(e) | CallArg::Named(_, e, _) | CallArg::Role(_, e, _) => {
2758                        infer_expr_effects(e, table, current, out)
2759                    }
2760                }
2761            }
2762            if let Expr::Ident(alias, _) = callee.as_ref() {
2763                if let Some(effect) = effect_from_tool(alias, table) {
2764                    out.insert(effect);
2765                } else {
2766                    out.insert("external".into());
2767                }
2768            } else {
2769                out.insert("external".into());
2770            }
2771        }
2772        Expr::ListLit(items, _) | Expr::TupleLit(items, _) | Expr::SetLit(items, _) => {
2773            for e in items {
2774                infer_expr_effects(e, table, current, out);
2775            }
2776        }
2777        Expr::MapLit(items, _) => {
2778            for (k, v) in items {
2779                infer_expr_effects(k, table, current, out);
2780                infer_expr_effects(v, table, current, out);
2781            }
2782        }
2783        Expr::RecordLit(_, fields, _) => {
2784            for (_, e) in fields {
2785                infer_expr_effects(e, table, current, out);
2786            }
2787        }
2788        Expr::DotAccess(obj, _, _) | Expr::NullSafeAccess(obj, _, _) => {
2789            infer_expr_effects(obj, table, current, out);
2790        }
2791        Expr::IndexAccess(obj, idx, _) | Expr::NullSafeIndex(obj, idx, _) => {
2792            infer_expr_effects(obj, table, current, out);
2793            infer_expr_effects(idx, table, current, out);
2794        }
2795        Expr::RoleBlock(_, inner, _) => infer_expr_effects(inner, table, current, out),
2796        Expr::Lambda { body, .. } => match body {
2797            LambdaBody::Expr(e) => infer_expr_effects(e, table, current, out),
2798            LambdaBody::Block(stmts) => {
2799                for s in stmts {
2800                    infer_stmt_effects(s, table, current, out);
2801                }
2802            }
2803        },
2804        Expr::IfExpr {
2805            cond,
2806            then_val,
2807            else_val,
2808            ..
2809        } => {
2810            infer_expr_effects(cond, table, current, out);
2811            infer_expr_effects(then_val, table, current, out);
2812            infer_expr_effects(else_val, table, current, out);
2813        }
2814        Expr::Comprehension {
2815            body,
2816            iter,
2817            condition,
2818            ..
2819        } => {
2820            infer_expr_effects(iter, table, current, out);
2821            if let Some(c) = condition {
2822                infer_expr_effects(c, table, current, out);
2823            }
2824            infer_expr_effects(body, table, current, out);
2825        }
2826        Expr::MatchExpr { subject, arms, .. } => {
2827            infer_expr_effects(subject, table, current, out);
2828            for arm in arms {
2829                for s in &arm.body {
2830                    infer_stmt_effects(s, table, current, out);
2831                }
2832            }
2833        }
2834        Expr::BlockExpr(stmts, _) => {
2835            for s in stmts {
2836                infer_stmt_effects(s, table, current, out);
2837            }
2838        }
2839        Expr::IntLit(_, _)
2840        | Expr::FloatLit(_, _)
2841        | Expr::StringLit(_, _)
2842        | Expr::StringInterp(_, _)
2843        | Expr::BoolLit(_, _)
2844        | Expr::NullLit(_)
2845        | Expr::Ident(_, _)
2846        | Expr::RawStringLit(_, _)
2847        | Expr::BytesLit(_, _)
2848        | Expr::RangeExpr { .. } => {}
2849    }
2850}
2851
2852fn infer_cell_effects(
2853    cell: &EffectCell,
2854    table: &SymbolTable,
2855    current: &HashMap<String, BTreeSet<String>>,
2856) -> BTreeSet<String> {
2857    let mut out = BTreeSet::new();
2858    for s in &cell.body {
2859        infer_stmt_effects(s, table, current, &mut out);
2860    }
2861    out
2862}
2863
2864fn apply_effect_inference(
2865    program: &Program,
2866    table: &mut SymbolTable,
2867    errors: &mut Vec<ResolveError>,
2868) {
2869    let strict = parse_directive_bool(program, "strict").unwrap_or(true);
2870    let doc_mode = parse_directive_bool(program, "doc_mode").unwrap_or(false);
2871    let enforce_declared_effect_rows = strict && !doc_mode;
2872    let cells = collect_effect_cells(program);
2873    if cells.is_empty() {
2874        return;
2875    }
2876
2877    let mut effective: HashMap<String, BTreeSet<String>> = HashMap::new();
2878    for cell in &cells {
2879        let declared: BTreeSet<String> =
2880            cell.declared.iter().map(|e| normalize_effect(e)).collect();
2881        effective.insert(
2882            cell.name.clone(),
2883            if declared.is_empty() {
2884                BTreeSet::new()
2885            } else {
2886                declared
2887            },
2888        );
2889    }
2890
2891    for _ in 0..32 {
2892        let mut changed = false;
2893        for cell in &cells {
2894            if !cell.declared.is_empty() {
2895                continue;
2896            }
2897            let inferred = infer_cell_effects(cell, table, &effective);
2898            let entry = effective.entry(cell.name.clone()).or_default();
2899            if *entry != inferred {
2900                *entry = inferred;
2901                changed = true;
2902            }
2903        }
2904        if !changed {
2905            break;
2906        }
2907    }
2908
2909    for cell in &cells {
2910        let inferred = infer_cell_effects(cell, table, &effective);
2911        let evidence = collect_cell_effect_evidence(cell, table, &effective);
2912        let declared: BTreeSet<String> =
2913            cell.declared.iter().map(|e| normalize_effect(e)).collect();
2914        let final_effects = if declared.is_empty() {
2915            inferred.clone()
2916        } else {
2917            if enforce_declared_effect_rows {
2918                for missing in inferred.difference(&declared) {
2919                    let (line, cause) = if let Some(ev) = evidence.get(missing) {
2920                        (ev.line, format!("; cause: {}", ev.cause))
2921                    } else {
2922                        (cell.line, String::new())
2923                    };
2924                    errors.push(ResolveError::UndeclaredEffect {
2925                        cell: cell.name.clone(),
2926                        effect: missing.clone(),
2927                        line,
2928                        cause,
2929                    });
2930                }
2931            }
2932            declared
2933        };
2934
2935        if cell.declared.is_empty() && !doc_mode {
2936            let inferred_vec: Vec<String> = final_effects.iter().cloned().collect();
2937            check_effect_grants_for(&cell.name, cell.line, &inferred_vec, table, errors);
2938        }
2939
2940        if let Some(info) = table.cells.get_mut(&cell.name) {
2941            info.effects = final_effects.iter().cloned().collect();
2942        }
2943    }
2944
2945    enforce_effect_call_compatibility(program, table, &cells, errors);
2946    enforce_deterministic_profile(program, table, &cells, errors);
2947}
2948
2949fn enforce_effect_call_compatibility(
2950    program: &Program,
2951    table: &SymbolTable,
2952    cells: &[EffectCell],
2953    errors: &mut Vec<ResolveError>,
2954) {
2955    let strict = parse_directive_bool(program, "strict").unwrap_or(true);
2956    let doc_mode = parse_directive_bool(program, "doc_mode").unwrap_or(false);
2957    if !strict || doc_mode {
2958        return;
2959    }
2960
2961    for cell in cells {
2962        let Some(info) = table.cells.get(&cell.name) else {
2963            continue;
2964        };
2965        let caller_effects = normalized_non_pure_effects(&info.effects);
2966
2967        let mut reqs = Vec::new();
2968        for stmt in &cell.body {
2969            collect_stmt_call_requirements(stmt, table, &mut reqs);
2970        }
2971
2972        let mut seen = BTreeSet::new();
2973        for req in reqs {
2974            for effect in req.effects {
2975                if caller_effects.contains(&effect) {
2976                    continue;
2977                }
2978                if seen.insert((req.callee.clone(), effect.clone(), req.line)) {
2979                    errors.push(ResolveError::EffectContractViolation {
2980                        caller: cell.name.clone(),
2981                        callee: req.callee.clone(),
2982                        effect,
2983                        line: req.line,
2984                    });
2985                }
2986            }
2987        }
2988    }
2989}
2990
2991fn enforce_deterministic_profile(
2992    program: &Program,
2993    table: &SymbolTable,
2994    cells: &[EffectCell],
2995    errors: &mut Vec<ResolveError>,
2996) {
2997    let deterministic = parse_directive_bool(program, "deterministic").unwrap_or(false);
2998    let doc_mode = parse_directive_bool(program, "doc_mode").unwrap_or(false);
2999    if !deterministic || doc_mode {
3000        return;
3001    }
3002
3003    // Effects that represent real I/O and are therefore nondeterministic.
3004    // "external" is the fallback for any tool without an explicit `bind effect`
3005    // declaration.  The rest are well-known effect names that users may bind
3006    // via `bind effect <name> to <tool>`.
3007    const NONDETERMINISTIC_EFFECTS: &[&str] = &[
3008        "database", "email", "external", "fs", "http", "llm", "mcp", "random", "time",
3009    ];
3010
3011    for cell in cells {
3012        let Some(info) = table.cells.get(&cell.name) else {
3013            continue;
3014        };
3015        let mut seen = BTreeSet::new();
3016        for effect in &info.effects {
3017            let effect = normalize_effect(effect);
3018            if NONDETERMINISTIC_EFFECTS.contains(&effect.as_str()) && seen.insert(effect.clone()) {
3019                errors.push(ResolveError::NondeterministicOperation {
3020                    cell: cell.name.clone(),
3021                    operation: effect,
3022                    line: cell.line,
3023                });
3024            }
3025        }
3026    }
3027}
3028
3029/// Compute Levenshtein edit distance between two strings
3030fn edit_distance(a: &str, b: &str) -> usize {
3031    let a_chars: Vec<char> = a.chars().collect();
3032    let b_chars: Vec<char> = b.chars().collect();
3033    let a_len = a_chars.len();
3034    let b_len = b_chars.len();
3035
3036    if a_len == 0 {
3037        return b_len;
3038    }
3039    if b_len == 0 {
3040        return a_len;
3041    }
3042
3043    let mut matrix = vec![vec![0; b_len + 1]; a_len + 1];
3044
3045    for (i, row) in matrix.iter_mut().enumerate() {
3046        row[0] = i;
3047    }
3048    #[allow(clippy::needless_range_loop)]
3049    for j in 0..=b_len {
3050        matrix[0][j] = j;
3051    }
3052
3053    for i in 1..=a_len {
3054        for j in 1..=b_len {
3055            let cost = if a_chars[i - 1] == b_chars[j - 1] {
3056                0
3057            } else {
3058                1
3059            };
3060            matrix[i][j] = (matrix[i - 1][j] + 1)
3061                .min(matrix[i][j - 1] + 1)
3062                .min(matrix[i - 1][j - 1] + cost);
3063        }
3064    }
3065
3066    matrix[a_len][b_len]
3067}
3068
3069/// Find similar names for "did you mean?" suggestions
3070fn suggest_similar(name: &str, candidates: &[&str], max_distance: usize) -> Vec<String> {
3071    let mut matches: Vec<(usize, String)> = candidates
3072        .iter()
3073        .filter_map(|c| {
3074            let d = edit_distance(name, c);
3075            if d <= max_distance && d < name.len() {
3076                Some((d, c.to_string()))
3077            } else {
3078                None
3079            }
3080        })
3081        .collect();
3082
3083    matches.sort_by_key(|(d, _)| *d);
3084    matches.into_iter().map(|(_, s)| s).take(3).collect()
3085}
3086
3087fn collect_type_alias_arities(program: &Program) -> HashMap<String, usize> {
3088    program
3089        .items
3090        .iter()
3091        .filter_map(|item| {
3092            if let Item::TypeAlias(alias) = item {
3093                Some((alias.name.clone(), alias.generic_params.len()))
3094            } else {
3095                None
3096            }
3097        })
3098        .collect()
3099}
3100
3101fn expected_type_arity(
3102    name: &str,
3103    table: &SymbolTable,
3104    type_alias_arities: &HashMap<String, usize>,
3105) -> Option<usize> {
3106    if let Some(info) = table.types.get(name) {
3107        return Some(info.generic_params.len());
3108    }
3109
3110    if let Some(arity) = type_alias_arities.get(name) {
3111        return Some(*arity);
3112    }
3113
3114    if table.type_aliases.contains_key(name) {
3115        return Some(0);
3116    }
3117
3118    None
3119}
3120
3121fn collect_required_trait_methods(trait_name: &str, table: &SymbolTable) -> Vec<String> {
3122    fn walk(name: &str, table: &SymbolTable, visited: &mut HashSet<String>, out: &mut Vec<String>) {
3123        if !visited.insert(name.to_string()) {
3124            return;
3125        }
3126        let Some(info) = table.traits.get(name) else {
3127            return;
3128        };
3129        for parent in &info.parent_traits {
3130            walk(parent, table, visited, out);
3131        }
3132        for method in &info.methods {
3133            if !out.contains(method) {
3134                out.push(method.clone());
3135            }
3136        }
3137    }
3138
3139    let mut out = Vec::new();
3140    let mut visited = HashSet::new();
3141    walk(trait_name, table, &mut visited, &mut out);
3142    out
3143}
3144
3145fn collect_trait_defs(program: &Program) -> HashMap<String, &TraitDef> {
3146    let mut defs = HashMap::new();
3147    for item in &program.items {
3148        if let Item::Trait(t) = item {
3149            defs.entry(t.name.clone()).or_insert(t);
3150        }
3151    }
3152    defs
3153}
3154
3155fn collect_required_trait_method_defs<'a>(
3156    trait_name: &str,
3157    trait_defs: &HashMap<String, &'a TraitDef>,
3158) -> Vec<&'a CellDef> {
3159    fn walk<'a>(
3160        name: &str,
3161        trait_defs: &HashMap<String, &'a TraitDef>,
3162        visited: &mut HashSet<String>,
3163        seen_methods: &mut HashSet<String>,
3164        out: &mut Vec<&'a CellDef>,
3165    ) {
3166        if !visited.insert(name.to_string()) {
3167            return;
3168        }
3169        let Some(trait_def) = trait_defs.get(name).copied() else {
3170            return;
3171        };
3172        for parent in &trait_def.parent_traits {
3173            walk(parent, trait_defs, visited, seen_methods, out);
3174        }
3175        for method in &trait_def.methods {
3176            if seen_methods.insert(method.name.clone()) {
3177                out.push(method);
3178            }
3179        }
3180    }
3181
3182    let mut out = Vec::new();
3183    let mut visited = HashSet::new();
3184    let mut seen_methods = HashSet::new();
3185    walk(
3186        trait_name,
3187        trait_defs,
3188        &mut visited,
3189        &mut seen_methods,
3190        &mut out,
3191    );
3192    out
3193}
3194
3195fn trait_method_signature_mismatch_reason(expected: &CellDef, actual: &CellDef) -> Option<String> {
3196    if expected.generic_params.len() != actual.generic_params.len() {
3197        return Some(format!(
3198            "generic parameter count mismatch: expected {}, found {}",
3199            expected.generic_params.len(),
3200            actual.generic_params.len()
3201        ));
3202    }
3203
3204    let expected_generics: Vec<&str> = expected
3205        .generic_params
3206        .iter()
3207        .map(|g| g.name.as_str())
3208        .collect();
3209    let actual_generics: Vec<&str> = actual
3210        .generic_params
3211        .iter()
3212        .map(|g| g.name.as_str())
3213        .collect();
3214
3215    if expected.params.len() != actual.params.len() {
3216        return Some(format!(
3217            "parameter count mismatch: expected {}, found {}",
3218            expected.params.len(),
3219            actual.params.len()
3220        ));
3221    }
3222
3223    for (idx, (expected_param, actual_param)) in
3224        expected.params.iter().zip(&actual.params).enumerate()
3225    {
3226        if !type_expr_compatible(
3227            &expected_param.ty,
3228            &actual_param.ty,
3229            &expected_generics,
3230            &actual_generics,
3231        ) {
3232            return Some(format!(
3233                "parameter {} type mismatch: expected '{}', found '{}'",
3234                idx + 1,
3235                format_type_expr(&expected_param.ty),
3236                format_type_expr(&actual_param.ty)
3237            ));
3238        }
3239    }
3240
3241    if !return_type_compatible(
3242        expected.return_type.as_ref(),
3243        actual.return_type.as_ref(),
3244        &expected_generics,
3245        &actual_generics,
3246    ) {
3247        return Some(format!(
3248            "return type mismatch: expected '{}', found '{}'",
3249            format_optional_type_expr(expected.return_type.as_ref()),
3250            format_optional_type_expr(actual.return_type.as_ref())
3251        ));
3252    }
3253
3254    None
3255}
3256
3257fn return_type_compatible(
3258    expected: Option<&TypeExpr>,
3259    actual: Option<&TypeExpr>,
3260    expected_generics: &[&str],
3261    actual_generics: &[&str],
3262) -> bool {
3263    match (expected, actual) {
3264        (None, None) => true,
3265        (Some(expected_ty), Some(actual_ty)) => {
3266            type_expr_compatible(expected_ty, actual_ty, expected_generics, actual_generics)
3267        }
3268        _ => false,
3269    }
3270}
3271
3272fn type_expr_compatible(
3273    expected: &TypeExpr,
3274    actual: &TypeExpr,
3275    expected_generics: &[&str],
3276    actual_generics: &[&str],
3277) -> bool {
3278    match (expected, actual) {
3279        (TypeExpr::Named(expected_name, _), TypeExpr::Named(actual_name, _)) => names_compatible(
3280            expected_name,
3281            actual_name,
3282            expected_generics,
3283            actual_generics,
3284        ),
3285        (TypeExpr::List(expected_inner, _), TypeExpr::List(actual_inner, _))
3286        | (TypeExpr::Set(expected_inner, _), TypeExpr::Set(actual_inner, _)) => {
3287            type_expr_compatible(
3288                expected_inner,
3289                actual_inner,
3290                expected_generics,
3291                actual_generics,
3292            )
3293        }
3294        (TypeExpr::Map(expected_k, expected_v, _), TypeExpr::Map(actual_k, actual_v, _))
3295        | (TypeExpr::Result(expected_k, expected_v, _), TypeExpr::Result(actual_k, actual_v, _)) => {
3296            type_expr_compatible(expected_k, actual_k, expected_generics, actual_generics)
3297                && type_expr_compatible(expected_v, actual_v, expected_generics, actual_generics)
3298        }
3299        (TypeExpr::Union(expected_types, _), TypeExpr::Union(actual_types, _))
3300        | (TypeExpr::Tuple(expected_types, _), TypeExpr::Tuple(actual_types, _)) => {
3301            expected_types.len() == actual_types.len()
3302                && expected_types
3303                    .iter()
3304                    .zip(actual_types)
3305                    .all(|(expected_ty, actual_ty)| {
3306                        type_expr_compatible(
3307                            expected_ty,
3308                            actual_ty,
3309                            expected_generics,
3310                            actual_generics,
3311                        )
3312                    })
3313        }
3314        (TypeExpr::Null(_), TypeExpr::Null(_)) => true,
3315        (
3316            TypeExpr::Fn(expected_params, expected_ret, expected_effects, _),
3317            TypeExpr::Fn(actual_params, actual_ret, actual_effects, _),
3318        ) => {
3319            if expected_params.len() != actual_params.len() {
3320                return false;
3321            }
3322            let mut expected_effects_sorted = expected_effects.clone();
3323            expected_effects_sorted.sort();
3324            let mut actual_effects_sorted = actual_effects.clone();
3325            actual_effects_sorted.sort();
3326            expected_effects_sorted == actual_effects_sorted
3327                && expected_params
3328                    .iter()
3329                    .zip(actual_params)
3330                    .all(|(expected_ty, actual_ty)| {
3331                        type_expr_compatible(
3332                            expected_ty,
3333                            actual_ty,
3334                            expected_generics,
3335                            actual_generics,
3336                        )
3337                    })
3338                && type_expr_compatible(
3339                    expected_ret,
3340                    actual_ret,
3341                    expected_generics,
3342                    actual_generics,
3343                )
3344        }
3345        (
3346            TypeExpr::Generic(expected_name, expected_args, _),
3347            TypeExpr::Generic(actual_name, actual_args, _),
3348        ) => {
3349            names_compatible(
3350                expected_name,
3351                actual_name,
3352                expected_generics,
3353                actual_generics,
3354            ) && expected_args.len() == actual_args.len()
3355                && expected_args
3356                    .iter()
3357                    .zip(actual_args)
3358                    .all(|(expected_arg, actual_arg)| {
3359                        type_expr_compatible(
3360                            expected_arg,
3361                            actual_arg,
3362                            expected_generics,
3363                            actual_generics,
3364                        )
3365                    })
3366        }
3367        _ => false,
3368    }
3369}
3370
3371fn names_compatible(
3372    expected: &str,
3373    actual: &str,
3374    expected_generics: &[&str],
3375    actual_generics: &[&str],
3376) -> bool {
3377    let expected_generic_idx = expected_generics.iter().position(|name| *name == expected);
3378    let actual_generic_idx = actual_generics.iter().position(|name| *name == actual);
3379    match (expected_generic_idx, actual_generic_idx) {
3380        (Some(expected_idx), Some(actual_idx)) => expected_idx == actual_idx,
3381        (None, None) => expected == actual,
3382        _ => false,
3383    }
3384}
3385
3386fn format_method_signature(method: &CellDef) -> String {
3387    let mut signature = String::new();
3388    signature.push_str("cell ");
3389    signature.push_str(&method.name);
3390    if !method.generic_params.is_empty() {
3391        let generic_names: Vec<&str> = method
3392            .generic_params
3393            .iter()
3394            .map(|generic_param| generic_param.name.as_str())
3395            .collect();
3396        signature.push('[');
3397        signature.push_str(&generic_names.join(", "));
3398        signature.push(']');
3399    }
3400    signature.push('(');
3401    let params = method
3402        .params
3403        .iter()
3404        .map(|param| format!("{}: {}", param.name, format_type_expr(&param.ty)))
3405        .collect::<Vec<_>>();
3406    signature.push_str(&params.join(", "));
3407    signature.push(')');
3408    if let Some(return_type) = &method.return_type {
3409        signature.push_str(" -> ");
3410        signature.push_str(&format_type_expr(return_type));
3411    }
3412    signature
3413}
3414
3415fn format_optional_type_expr(ty: Option<&TypeExpr>) -> String {
3416    match ty {
3417        Some(ty) => format_type_expr(ty),
3418        None => "no return type".to_string(),
3419    }
3420}
3421
3422fn format_type_expr(ty: &TypeExpr) -> String {
3423    match ty {
3424        TypeExpr::Named(name, _) => name.clone(),
3425        TypeExpr::List(inner, _) => format!("list[{}]", format_type_expr(inner)),
3426        TypeExpr::Map(key, value, _) => {
3427            format!(
3428                "map[{}, {}]",
3429                format_type_expr(key),
3430                format_type_expr(value)
3431            )
3432        }
3433        TypeExpr::Result(ok, err, _) => {
3434            format!(
3435                "result[{}, {}]",
3436                format_type_expr(ok),
3437                format_type_expr(err)
3438            )
3439        }
3440        TypeExpr::Union(types, _) => types
3441            .iter()
3442            .map(format_type_expr)
3443            .collect::<Vec<_>>()
3444            .join(" | "),
3445        TypeExpr::Null(_) => "Null".to_string(),
3446        TypeExpr::Tuple(types, _) => {
3447            let rendered = types.iter().map(format_type_expr).collect::<Vec<_>>();
3448            format!("({})", rendered.join(", "))
3449        }
3450        TypeExpr::Set(inner, _) => format!("set[{}]", format_type_expr(inner)),
3451        TypeExpr::Fn(params, ret, effects, _) => {
3452            let rendered_params = params.iter().map(format_type_expr).collect::<Vec<_>>();
3453            if effects.is_empty() {
3454                format!(
3455                    "fn({}) -> {}",
3456                    rendered_params.join(", "),
3457                    format_type_expr(ret)
3458                )
3459            } else {
3460                format!(
3461                    "fn({}) -> {} / {{{}}}",
3462                    rendered_params.join(", "),
3463                    format_type_expr(ret),
3464                    effects.join(", ")
3465                )
3466            }
3467        }
3468        TypeExpr::Generic(name, args, _) => {
3469            let rendered_args = args.iter().map(format_type_expr).collect::<Vec<_>>();
3470            format!("{}[{}]", name, rendered_args.join(", "))
3471        }
3472    }
3473}
3474
3475fn check_type_refs_with_generics(
3476    ty: &TypeExpr,
3477    table: &SymbolTable,
3478    type_alias_arities: &HashMap<String, usize>,
3479    errors: &mut Vec<ResolveError>,
3480    generics: &[String],
3481) {
3482    match ty {
3483        TypeExpr::Named(name, span) => {
3484            if generics.iter().any(|g| g == name) {
3485                return;
3486            }
3487            if !table.types.contains_key(name) && !table.type_aliases.contains_key(name) {
3488                let mut candidates: Vec<&str> = table.types.keys().map(|s| s.as_str()).collect();
3489                candidates.extend(table.type_aliases.keys().map(|s| s.as_str()));
3490                let suggestions = suggest_similar(name, &candidates, 2);
3491                errors.push(ResolveError::UndefinedType {
3492                    name: name.clone(),
3493                    line: span.line,
3494                    suggestions,
3495                });
3496            } else if expected_type_arity(name, table, type_alias_arities).is_some_and(|n| n > 0) {
3497                let expected = expected_type_arity(name, table, type_alias_arities).unwrap_or(0);
3498                errors.push(ResolveError::GenericArityMismatch {
3499                    name: name.clone(),
3500                    expected,
3501                    actual: 0,
3502                    line: span.line,
3503                });
3504            }
3505        }
3506        TypeExpr::List(inner, _) => {
3507            check_type_refs_with_generics(inner, table, type_alias_arities, errors, generics)
3508        }
3509        TypeExpr::Map(k, v, _) => {
3510            check_type_refs_with_generics(k, table, type_alias_arities, errors, generics);
3511            check_type_refs_with_generics(v, table, type_alias_arities, errors, generics);
3512        }
3513        TypeExpr::Result(ok, err, _) => {
3514            check_type_refs_with_generics(ok, table, type_alias_arities, errors, generics);
3515            check_type_refs_with_generics(err, table, type_alias_arities, errors, generics);
3516        }
3517        TypeExpr::Union(types, _) => {
3518            for t in types {
3519                check_type_refs_with_generics(t, table, type_alias_arities, errors, generics);
3520            }
3521        }
3522        TypeExpr::Null(_) => {}
3523        TypeExpr::Tuple(types, _) => {
3524            for t in types {
3525                check_type_refs_with_generics(t, table, type_alias_arities, errors, generics);
3526            }
3527        }
3528        TypeExpr::Set(inner, _) => {
3529            check_type_refs_with_generics(inner, table, type_alias_arities, errors, generics)
3530        }
3531        TypeExpr::Fn(params, ret, _, _) => {
3532            for t in params {
3533                check_type_refs_with_generics(t, table, type_alias_arities, errors, generics);
3534            }
3535            check_type_refs_with_generics(ret, table, type_alias_arities, errors, generics);
3536        }
3537        TypeExpr::Generic(name, args, span) => {
3538            if generics.iter().any(|g| g == name) {
3539                if !args.is_empty() {
3540                    errors.push(ResolveError::GenericArityMismatch {
3541                        name: name.clone(),
3542                        expected: 0,
3543                        actual: args.len(),
3544                        line: span.line,
3545                    });
3546                }
3547            } else if !table.types.contains_key(name) && !table.type_aliases.contains_key(name) {
3548                let mut candidates: Vec<&str> = table.types.keys().map(|s| s.as_str()).collect();
3549                candidates.extend(table.type_aliases.keys().map(|s| s.as_str()));
3550                let suggestions = suggest_similar(name, &candidates, 2);
3551                errors.push(ResolveError::UndefinedType {
3552                    name: name.clone(),
3553                    line: span.line,
3554                    suggestions,
3555                });
3556            } else if let Some(expected) = expected_type_arity(name, table, type_alias_arities) {
3557                if expected != args.len() {
3558                    errors.push(ResolveError::GenericArityMismatch {
3559                        name: name.clone(),
3560                        expected,
3561                        actual: args.len(),
3562                        line: span.line,
3563                    });
3564                }
3565            }
3566            for t in args {
3567                check_type_refs_with_generics(t, table, type_alias_arities, errors, generics);
3568            }
3569        }
3570    }
3571}
3572
3573#[cfg(test)]
3574mod tests {
3575    use super::*;
3576    use crate::compiler::lexer::Lexer;
3577    use crate::compiler::parser::Parser;
3578    use crate::compiler::tokens::Span;
3579
3580    fn resolve_src(src: &str) -> Result<SymbolTable, Vec<ResolveError>> {
3581        let mut lexer = Lexer::new(src, 1, 0);
3582        let tokens = lexer.tokenize().unwrap();
3583        let mut parser = Parser::new(tokens);
3584        let prog = parser.parse_program(vec![]).unwrap();
3585        resolve(&prog)
3586    }
3587
3588    fn s() -> Span {
3589        Span {
3590            start: 0,
3591            end: 0,
3592            line: 1,
3593            col: 1,
3594        }
3595    }
3596
3597    #[test]
3598    fn test_resolve_basic() {
3599        let table =
3600            resolve_src("record Foo\n  x: Int\nend\n\ncell main() -> Foo\n  return Foo(x: 1)\nend")
3601                .unwrap();
3602        assert!(table.types.contains_key("Foo"));
3603        assert!(table.cells.contains_key("main"));
3604    }
3605
3606    #[test]
3607    fn test_resolve_undefined_type() {
3608        let err = resolve_src("record Bar\n  x: Unknown\nend").unwrap_err();
3609        assert!(!err.is_empty());
3610    }
3611
3612    #[test]
3613    fn test_effect_inference_for_implicit_row() {
3614        let table = resolve_src("cell main() -> Int\n  emit(\"x\")\n  return 1\nend").unwrap();
3615        let effects = &table.cells.get("main").unwrap().effects;
3616        assert!(effects.contains(&"emit".to_string()));
3617    }
3618
3619    #[test]
3620    fn test_effect_inference_transitive_cell_call() {
3621        let table = resolve_src(
3622            "cell a() -> Int / {emit}\n  emit(\"x\")\n  return 1\nend\n\ncell b() -> Int\n  return a()\nend",
3623        )
3624        .unwrap();
3625        let effects = &table.cells.get("b").unwrap().effects;
3626        assert!(effects.contains(&"emit".to_string()));
3627    }
3628
3629    #[test]
3630    fn test_undeclared_effect_error_in_strict_mode() {
3631        let sp = s();
3632        let program = Program {
3633            directives: vec![],
3634            items: vec![Item::Cell(CellDef {
3635                name: "main".into(),
3636                generic_params: vec![],
3637                params: vec![],
3638                return_type: Some(TypeExpr::Named("Int".into(), sp)),
3639                effects: vec!["emit".into()],
3640                body: vec![Stmt::Expr(ExprStmt {
3641                    expr: Expr::Call(
3642                        Box::new(Expr::Ident("parallel".into(), sp)),
3643                        vec![CallArg::Positional(Expr::IntLit(1, sp))],
3644                        sp,
3645                    ),
3646                    span: sp,
3647                })],
3648                is_pub: false,
3649                is_async: false,
3650                where_clauses: vec![],
3651                span: sp,
3652            })],
3653            span: sp,
3654        };
3655        let err = resolve(&program).unwrap_err();
3656        assert!(err.iter().any(|e| matches!(
3657            e,
3658            ResolveError::UndeclaredEffect { cell, effect, .. } if cell == "main" && effect == "async"
3659        )));
3660    }
3661
3662    #[test]
3663    fn test_doc_mode_allows_undeclared_effects() {
3664        let sp = s();
3665        let program = Program {
3666            directives: vec![Directive {
3667                name: "doc_mode".into(),
3668                value: Some("true".into()),
3669                span: sp,
3670            }],
3671            items: vec![Item::Cell(CellDef {
3672                name: "main".into(),
3673                generic_params: vec![],
3674                params: vec![],
3675                return_type: Some(TypeExpr::Named("Int".into(), sp)),
3676                effects: vec!["emit".into()],
3677                body: vec![Stmt::Expr(ExprStmt {
3678                    expr: Expr::Call(
3679                        Box::new(Expr::Ident("parallel".into(), sp)),
3680                        vec![CallArg::Positional(Expr::IntLit(1, sp))],
3681                        sp,
3682                    ),
3683                    span: sp,
3684                })],
3685                is_pub: false,
3686                is_async: false,
3687                where_clauses: vec![],
3688                span: sp,
3689            })],
3690            span: sp,
3691        };
3692        let table = resolve(&program).unwrap();
3693        assert!(table.cells.contains_key("main"));
3694    }
3695
3696    #[test]
3697    fn test_effect_inference_marks_uuid_and_timestamp() {
3698        let table = resolve_src(
3699            "cell main() -> String\n  let id = uuid()\n  let ts = timestamp()\n  return to_string(ts) + id\nend",
3700        )
3701        .unwrap();
3702        let effects = &table.cells.get("main").unwrap().effects;
3703        assert!(effects.contains(&"random".to_string()));
3704        assert!(effects.contains(&"time".to_string()));
3705    }
3706
3707    #[test]
3708    fn test_effect_inference_marks_async_orchestration_builtins() {
3709        let table = resolve_src(
3710            "cell main() -> Int\n  let f = spawn(fn() => 1)\n  let a = parallel(1, 2)\n  let b = race(1, 2)\n  let c = vote(1, 1, 2)\n  let d = select(null, 1)\n  return timeout(d, 10)\nend",
3711        )
3712        .unwrap();
3713        let effects = &table.cells.get("main").unwrap().effects;
3714        assert!(effects.contains(&"async".to_string()));
3715    }
3716
3717    #[test]
3718    fn test_deterministic_profile_rejects_nondeterminism() {
3719        let err = resolve_src("@deterministic true\n\ncell main() -> String\n  return uuid()\nend")
3720            .unwrap_err();
3721        assert!(err.iter().any(|e| matches!(
3722            e,
3723            ResolveError::NondeterministicOperation { cell, operation, .. }
3724            if cell == "main" && operation == "random"
3725        )));
3726    }
3727
3728    #[test]
3729    fn test_effect_contract_violation_on_cell_call() {
3730        let err = resolve_src(
3731            "use tool http.get as HttpGet\ngrant HttpGet\n\ncell fetch() -> Int / {http}\n  return 1\nend\n\ncell main() -> Int / {emit}\n  return fetch()\nend",
3732        )
3733        .unwrap_err();
3734        assert!(err.iter().any(|e| matches!(
3735            e,
3736            ResolveError::EffectContractViolation { caller, callee, effect, .. }
3737            if caller == "main" && callee == "fetch" && effect == "http"
3738        )));
3739    }
3740
3741    #[test]
3742    fn test_effect_contract_violation_on_tool_call() {
3743        let err = resolve_src(
3744            "use tool http.get as HttpGet\nbind effect http to HttpGet\n\ngrant HttpGet\n\ncell main() -> String / {emit}\n  return string(HttpGet(url: \"https://example.com\"))\nend",
3745        )
3746        .unwrap_err();
3747        assert!(err.iter().any(|e| matches!(
3748            e,
3749            ResolveError::EffectContractViolation { caller, callee, effect, .. }
3750            if caller == "main" && callee == "tool HttpGet" && effect == "http"
3751        )));
3752    }
3753
3754    #[test]
3755    fn test_effect_contract_allows_declared_callee_effects() {
3756        let table = resolve_src(
3757            "use tool http.get as HttpGet\ngrant HttpGet\n\ncell fetch() -> Int / {http}\n  return 1\nend\n\ncell main() -> Int / {http}\n  return fetch()\nend",
3758        )
3759        .unwrap();
3760        let effects = &table.cells.get("main").unwrap().effects;
3761        assert!(effects.contains(&"http".to_string()));
3762    }
3763
3764    #[test]
3765    fn test_undeclared_effect_includes_call_cause() {
3766        let err = resolve_src(
3767            "use tool http.get as HttpGet\ngrant HttpGet\n\ncell fetch() -> Int / {http}\n  return 1\nend\n\ncell main() -> Int / {emit}\n  return fetch()\nend",
3768        )
3769        .unwrap_err();
3770        assert!(err.iter().any(|e| matches!(
3771            e,
3772            ResolveError::UndeclaredEffect { cell, effect, cause, .. }
3773            if cell == "main" && effect == "http" && cause.contains("call to 'fetch'")
3774        )));
3775    }
3776
3777    #[test]
3778    fn test_undeclared_effect_includes_tool_cause() {
3779        let err = resolve_src(
3780            "use tool http.get as HttpGet\nbind effect http to HttpGet\ngrant HttpGet\n\ncell main() -> String / {emit}\n  return string(HttpGet(url: \"https://example.com\"))\nend",
3781        )
3782        .unwrap_err();
3783        assert!(err.iter().any(|e| matches!(
3784            e,
3785            ResolveError::UndeclaredEffect { cell, effect, cause, .. }
3786            if cell == "main" && effect == "http" && cause.contains("tool call 'HttpGet'")
3787        )));
3788    }
3789
3790    #[test]
3791    fn test_grant_policy_effect_clause_restricts_effects() {
3792        let err = resolve_src(
3793            "use tool http.get as HttpGet\ngrant HttpGet\n  effect http\n\ncell main() -> Int / {llm}\n  return 1\nend",
3794        )
3795        .unwrap_err();
3796        assert!(err.iter().any(|e| matches!(
3797            e,
3798            ResolveError::MissingEffectGrant { cell, effect, .. }
3799            if cell == "main" && effect == "llm"
3800        )));
3801    }
3802
3803    #[test]
3804    fn test_grant_policy_effects_list_allows_effect() {
3805        let table = resolve_src(
3806            "use tool http.get as HttpGet\ngrant HttpGet\n  effects [\"http\", \"llm\"]\n\ncell main() -> Int / {llm}\n  return 1\nend",
3807        )
3808        .unwrap();
3809        let effects = &table.cells.get("main").unwrap().effects;
3810        assert!(effects.contains(&"llm".to_string()));
3811    }
3812
3813    #[test]
3814    fn test_machine_graph_validation_accepts_reachable_terminal_graph() {
3815        let table = resolve_src(
3816            "machine TicketFlow\n  initial: Start\n  state Start\n    transition Done()\n  end\n  state Done\n    terminal: true\n  end\nend",
3817        )
3818        .unwrap();
3819        let process = table
3820            .processes
3821            .get("machine:TicketFlow")
3822            .expect("machine should be registered");
3823        assert_eq!(process.machine_initial.as_deref(), Some("Start"));
3824        assert_eq!(process.machine_states.len(), 2);
3825    }
3826
3827    #[test]
3828    fn test_machine_graph_validation_reports_transition_and_reachability_errors() {
3829        let err = resolve_src(
3830            "machine Broken\n  initial: Start\n  state Start\n    transition Missing()\n  end\n  state DeadEnd\n    terminal: false\n  end\nend",
3831        )
3832        .unwrap_err();
3833        assert!(err.iter().any(|e| matches!(
3834            e,
3835            ResolveError::MachineUnknownTransition { machine, state, target, .. }
3836            if machine == "Broken" && state == "Start" && target == "Missing"
3837        )));
3838        assert!(err.iter().any(|e| matches!(
3839            e,
3840            ResolveError::MachineUnreachableState { machine, state, .. }
3841            if machine == "Broken" && state == "DeadEnd"
3842        )));
3843        assert!(err.iter().any(|e| matches!(
3844            e,
3845            ResolveError::MachineMissingTerminal { machine, .. }
3846            if machine == "Broken"
3847        )));
3848    }
3849
3850    #[test]
3851    fn test_machine_graph_validation_checks_transition_arg_count_and_type() {
3852        let err = resolve_src(
3853            "machine Typed\n  initial: Start\n  state Start(x: Int)\n    transition Done(x, \"bad\")\n  end\n  state Done(v: Int)\n    terminal: true\n  end\nend",
3854        )
3855        .unwrap_err();
3856        assert!(err.iter().any(|e| matches!(
3857            e,
3858            ResolveError::MachineTransitionArgCount { machine, state, target, expected, actual, .. }
3859            if machine == "Typed" && state == "Start" && target == "Done" && *expected == 1 && *actual == 2
3860        )));
3861
3862        let err = resolve_src(
3863            "machine Typed\n  initial: Start\n  state Start(x: String)\n    transition Done(x)\n  end\n  state Done(v: Int)\n    terminal: true\n  end\nend",
3864        )
3865        .unwrap_err();
3866        assert!(err.iter().any(|e| matches!(
3867            e,
3868            ResolveError::MachineTransitionArgType { machine, state, target, expected, actual, .. }
3869            if machine == "Typed" && state == "Start" && target == "Done" && expected == "Int" && actual == "String"
3870        )));
3871    }
3872
3873    #[test]
3874    fn test_machine_graph_validation_checks_guard_type() {
3875        let err = resolve_src(
3876            "machine Guarded\n  initial: Start\n  state Start(x: Int)\n    guard: x + 1\n    transition Done(x)\n  end\n  state Done(v: Int)\n    terminal: true\n  end\nend",
3877        )
3878        .unwrap_err();
3879        assert!(err.iter().any(|e| matches!(
3880            e,
3881            ResolveError::MachineGuardType { machine, state, actual, .. }
3882            if machine == "Guarded" && state == "Start" && actual == "Int"
3883        )));
3884    }
3885
3886    #[test]
3887    fn test_pipeline_stage_validation_rejects_unknown_stage() {
3888        let err = resolve_src("pipeline P\n  stages:\n    UnknownStage\n  end\nend").unwrap_err();
3889        assert!(err.iter().any(|e| matches!(
3890            e,
3891            ResolveError::PipelineUnknownStage { pipeline, stage, .. }
3892            if pipeline == "P" && stage == "UnknownStage"
3893        )));
3894    }
3895
3896    #[test]
3897    fn test_pipeline_stage_validation_rejects_type_mismatch() {
3898        let err = resolve_src(
3899            "cell one(x: Int) -> String\n  return \"x\"\nend\n\ncell two(y: Int) -> Int\n  return y\nend\n\npipeline P\n  stages:\n    one\n      -> two\n  end\nend",
3900        )
3901        .unwrap_err();
3902        assert!(err.iter().any(|e| matches!(
3903            e,
3904            ResolveError::PipelineStageTypeMismatch { pipeline, from_stage, to_stage, expected, actual, .. }
3905            if pipeline == "P" && from_stage == "one" && to_stage == "two" && expected == "Int" && actual == "String"
3906        )));
3907    }
3908
3909    #[test]
3910    fn test_duplicate_record_detection() {
3911        let err =
3912            resolve_src("record Foo\n  x: Int\nend\n\nrecord Foo\n  y: String\nend").unwrap_err();
3913        assert!(err.iter().any(|e| matches!(
3914            e,
3915            ResolveError::Duplicate { name, .. } if name == "Foo"
3916        )));
3917    }
3918
3919    #[test]
3920    fn test_duplicate_cell_detection() {
3921        let err =
3922            resolve_src("cell foo() -> Int\n  return 1\nend\n\ncell foo() -> Int\n  return 2\nend")
3923                .unwrap_err();
3924        assert!(err.iter().any(|e| matches!(
3925            e,
3926            ResolveError::Duplicate { name, .. } if name == "foo"
3927        )));
3928    }
3929
3930    #[test]
3931    fn test_type_alias_not_undefined() {
3932        // A type alias should not produce an UndefinedType error
3933        let table = resolve_src(
3934            "type UserId = String\n\ncell greet(id: UserId) -> String\n  return id\nend",
3935        )
3936        .unwrap();
3937        assert!(table.type_aliases.contains_key("UserId"));
3938    }
3939
3940    #[test]
3941    fn test_duplicate_enum_detection() {
3942        let err =
3943            resolve_src("enum Color\n  Red\n  Blue\nend\n\nenum Color\n  Green\nend").unwrap_err();
3944        assert!(err.iter().any(|e| matches!(
3945            e,
3946            ResolveError::Duplicate { name, .. } if name == "Color"
3947        )));
3948    }
3949
3950    #[test]
3951    fn test_duplicate_effect_detection() {
3952        let err = resolve_src("effect http\n  cell get(url: String) -> String\nend\n\neffect http\n  cell post(url: String) -> String\nend").unwrap_err();
3953        assert!(err.iter().any(|e| matches!(
3954            e,
3955            ResolveError::Duplicate { name, .. } if name == "http"
3956        )));
3957    }
3958
3959    #[test]
3960    fn test_builtin_types_are_minimal() {
3961        let table = SymbolTable::new();
3962        // Core builtins should be present
3963        assert!(table.types.contains_key("String"));
3964        assert!(table.types.contains_key("Int"));
3965        assert!(table.types.contains_key("Float"));
3966        assert!(table.types.contains_key("Bool"));
3967        assert!(table.types.contains_key("Bytes"));
3968        assert!(table.types.contains_key("Json"));
3969        assert!(table.types.contains_key("Null"));
3970        // Generic placeholders should not be implicitly accepted.
3971        assert!(!table.types.contains_key("A"));
3972        assert!(!table.types.contains_key("T"));
3973        // App-specific types should NOT be present
3974        assert!(!table.types.contains_key("Invoice"));
3975        assert!(!table.types.contains_key("MyRecord"));
3976        assert!(!table.types.contains_key("Report"));
3977        assert!(!table.types.contains_key("Response"));
3978    }
3979
3980    #[test]
3981    fn test_tool_without_binding_gets_external_effect() {
3982        // A tool with no explicit `bind effect` should produce "external" effect,
3983        // not a heuristic guess based on tool name or path.
3984        let err = resolve_src(
3985            "use tool http.get as HttpGet\ngrant HttpGet\n\ncell main() -> String / {http}\n  return string(HttpGet(url: \"https://example.com\"))\nend",
3986        )
3987        .unwrap_err();
3988        // The tool call should produce "external" (not "http"), so declaring {http}
3989        // should cause an UndeclaredEffect for "external".
3990        assert!(err.iter().any(|e| matches!(
3991            e,
3992            ResolveError::UndeclaredEffect { cell, effect, .. }
3993            if cell == "main" && effect == "external"
3994        )));
3995    }
3996
3997    #[test]
3998    fn test_explicit_bind_effect_maps_tool_to_effect() {
3999        // With an explicit `bind effect http to HttpGet`, the tool should
4000        // produce "http" effect.
4001        let table = resolve_src(
4002            "use tool http.get as HttpGet\nbind effect http to HttpGet\ngrant HttpGet\n\ncell main() -> String / {http}\n  return string(HttpGet(url: \"https://example.com\"))\nend",
4003        )
4004        .unwrap();
4005        let effects = &table.cells.get("main").unwrap().effects;
4006        assert!(effects.contains(&"http".to_string()));
4007    }
4008
4009    #[test]
4010    fn test_generic_type_alias_resolves_without_placeholder_builtins() {
4011        let table = resolve_src(
4012            "type Box[T] = map[String, T]\n\ncell main() -> Box[Int]\n  return {\"ok\": 1}\nend",
4013        )
4014        .unwrap();
4015        assert!(table.type_aliases.contains_key("Box"));
4016    }
4017
4018    #[test]
4019    fn test_trait_impl_signature_reports_parameter_count_mismatch() {
4020        let err = resolve_src(
4021            "trait Greeter\n  cell greet(name: String) -> String\n    return name\n  end\nend\n\nimpl Greeter for String\n  cell greet(name: String, suffix: String) -> String\n    return name\n  end\nend",
4022        )
4023        .unwrap_err();
4024        assert!(err.iter().any(|e| matches!(
4025            e,
4026            ResolveError::TraitMethodSignatureMismatch { method, reason, .. }
4027            if method == "greet" && reason.contains("parameter count mismatch")
4028        )));
4029    }
4030
4031    #[test]
4032    fn test_trait_impl_signature_reports_parameter_type_mismatch() {
4033        let err = resolve_src(
4034            "trait Greeter\n  cell greet(name: String) -> String\n    return name\n  end\nend\n\nimpl Greeter for String\n  cell greet(name: Int) -> String\n    return \"x\"\n  end\nend",
4035        )
4036        .unwrap_err();
4037        assert!(err.iter().any(|e| matches!(
4038            e,
4039            ResolveError::TraitMethodSignatureMismatch { method, reason, .. }
4040            if method == "greet" && reason.contains("parameter 1 type mismatch")
4041        )));
4042    }
4043
4044    #[test]
4045    fn test_trait_impl_signature_reports_return_type_mismatch() {
4046        let err = resolve_src(
4047            "trait Greeter\n  cell greet(name: String) -> String\n    return name\n  end\nend\n\nimpl Greeter for String\n  cell greet(name: String) -> Int\n    return 1\n  end\nend",
4048        )
4049        .unwrap_err();
4050        assert!(err.iter().any(|e| matches!(
4051            e,
4052            ResolveError::TraitMethodSignatureMismatch { method, reason, .. }
4053            if method == "greet" && reason.contains("return type mismatch")
4054        )));
4055    }
4056
4057    #[test]
4058    fn test_trait_impl_signature_accepts_compatible_method() {
4059        let table = resolve_src(
4060            "trait Greeter\n  cell greet(name: String) -> String\n    return name\n  end\nend\n\nimpl Greeter for String\n  cell greet(name: String) -> String\n    return name\n  end\nend",
4061        )
4062        .unwrap();
4063        assert_eq!(table.impls.len(), 1);
4064    }
4065}