1use 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#[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 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 pub fn import_cell(&mut self, name: String, info: CellInfo) {
335 self.cells.insert(name, info);
336 }
337
338 pub fn import_type(&mut self, name: String, info: TypeInfo) {
340 self.types.insert(name, info);
341 }
342
343 pub fn import_type_alias(&mut self, name: String, type_expr: TypeExpr) {
345 self.type_aliases.insert(name, type_expr);
346 }
347}
348
349pub fn resolve(program: &Program) -> Result<SymbolTable, Vec<ResolveError>> {
351 resolve_with_base(program, SymbolTable::new())
352}
353
354pub 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 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(_) => {} 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 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 ¶m.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 ¶m.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 ¶m.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 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 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 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 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 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 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
3029fn 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
3069fn 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(¶m.ty)))
3405 .collect::<Vec<_>>();
3406 signature.push_str(¶ms.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 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 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 assert!(!table.types.contains_key("A"));
3972 assert!(!table.types.contains_key("T"));
3973 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 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 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 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}