aver/types/checker/mod.rs
1/// Aver static type checker.
2///
3/// Two-phase analysis:
4/// Phase 1 — build a signature table from all FnDef nodes and builtins.
5/// Phase 2 — check top-level statements, then each FnDef for call-site
6/// argument types, return type, BinOp compatibility, and effects.
7///
8/// The checker resolves named generic variables at call sites. Error recovery
9/// uses `Type::Invalid`, which matches anything to suppress cascading diagnostics
10/// (Iron — A4).
11use std::collections::{HashMap, HashSet};
12
13use super::{Type, parse_type_str_strict};
14use crate::ast::{
15 BinOp, Expr, FnDef, Literal, Module, Pattern, Spanned, Stmt, TailCallData, TopLevel, TypeDef,
16};
17use crate::ir::{FnId, FnKey, SymbolTable, TypeId, TypeKey};
18
19mod builtins;
20mod check;
21pub mod effect_classification;
22pub mod effect_lifting;
23mod exhaustiveness;
24mod flow;
25pub mod hostile_effects;
26pub mod hostile_values;
27mod infer;
28mod modules;
29pub mod oracle_subtypes;
30pub mod proof_trust_header;
31
32#[cfg(test)]
33mod tests;
34
35// ---------------------------------------------------------------------------
36// Public API
37// ---------------------------------------------------------------------------
38
39#[derive(Debug, Clone)]
40pub struct TypeError {
41 pub message: String,
42 pub line: usize,
43 pub col: usize,
44 /// Optional secondary span for multi-region diagnostics (e.g. declared type vs actual return).
45 pub secondary: Option<TypeErrorSpan>,
46}
47
48#[derive(Debug, Clone)]
49pub struct TypeErrorSpan {
50 pub line: usize,
51 pub col: usize,
52 pub label: String,
53}
54
55/// Result of type-checking.
56#[derive(Debug)]
57pub struct TypeCheckResult {
58 pub errors: Vec<TypeError>,
59 /// For each user-defined fn: (param_types, return_type, effects).
60 pub fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)>,
61 /// Unused binding warnings: (binding_name, fn_name, line).
62 pub unused_bindings: Vec<(String, String, usize)>,
63}
64
65pub fn run_type_check(items: &[TopLevel]) -> Vec<TypeError> {
66 run_type_check_with_base(items, None)
67}
68
69pub fn run_type_check_with_base(items: &[TopLevel], base_dir: Option<&str>) -> Vec<TypeError> {
70 run_type_check_full(items, base_dir).errors
71}
72
73pub fn run_type_check_full(items: &[TopLevel], base_dir: Option<&str>) -> TypeCheckResult {
74 let mut checker = TypeChecker::new_with_symbols(build_symbols_for_items(items, base_dir));
75 checker.check(items, base_dir);
76 finalize_check_result(checker, items)
77}
78
79/// Build the `SymbolTable` for a typecheck entry point. Mirrors the
80/// dep-resolution path inside [`TypeChecker::check`] so the table
81/// covers both entry items and any modules the entry file depends on.
82/// Filesystem errors are silently dropped here — the checker rebuilds
83/// its own diagnostics during the actual check, so duplicating them at
84/// the symbol-table layer would only produce noise.
85fn build_symbols_for_items(items: &[TopLevel], base_dir: Option<&str>) -> SymbolTable {
86 let dep_modules = base_dir
87 .and_then(|base| {
88 TypeChecker::module_decl(items).and_then(|m| {
89 crate::source::load_module_tree(&m.depends, base)
90 .ok()
91 .map(|loaded| symbols_dep_modules_from_loaded(&loaded))
92 })
93 })
94 .unwrap_or_default();
95 SymbolTable::build(items, &dep_modules)
96}
97
98/// Pre-loaded variant of [`build_symbols_for_items`] for the
99/// `WithLoaded` typecheck driver (playground virtual FS).
100fn build_symbols_with_loaded(
101 items: &[TopLevel],
102 loaded: &[crate::source::LoadedModule],
103) -> SymbolTable {
104 let dep_modules = symbols_dep_modules_from_loaded(loaded);
105 SymbolTable::build(items, &dep_modules)
106}
107
108fn symbols_dep_modules_from_loaded(
109 loaded: &[crate::source::LoadedModule],
110) -> Vec<crate::codegen::ModuleInfo> {
111 loaded
112 .iter()
113 .map(|m| crate::codegen::ModuleInfo {
114 prefix: m.dep_name.clone(),
115 depends: Vec::new(),
116 type_defs: m
117 .items
118 .iter()
119 .filter_map(|i| match i {
120 TopLevel::TypeDef(td) => Some(td.clone()),
121 _ => None,
122 })
123 .collect(),
124 fn_defs: m
125 .items
126 .iter()
127 .filter_map(|i| match i {
128 TopLevel::FnDef(fd) => Some(fd.clone()),
129 _ => None,
130 })
131 .collect(),
132 // Symbol-table build only — the typechecker never reads
133 // verify_laws (it is a proof-emit-only field).
134 verify_laws: Vec::new(),
135 analysis: None,
136 })
137 .collect()
138}
139
140/// Variant of [`run_type_check_full`] that uses pre-loaded dependency
141/// modules instead of resolving them from disk. The playground feeds
142/// this from its in-memory virtual fs so multi-file projects type-
143/// check without any filesystem access.
144pub fn run_type_check_with_loaded(
145 items: &[TopLevel],
146 loaded: &[crate::source::LoadedModule],
147) -> TypeCheckResult {
148 let mut checker = TypeChecker::new_with_symbols(build_symbols_with_loaded(items, loaded));
149 checker.check_with_loaded(items, loaded);
150 finalize_check_result(checker, items)
151}
152
153/// Self-host variant of [`run_type_check_full`]: bypasses the
154/// opaque-type checks (construction, field access, pattern match).
155/// Used exclusively by `aver compile --with-self-host-support` so
156/// `self_hosted/domain/builtins.av` can round-trip opaque host
157/// types (e.g. `Tcp.Connection`) through the replay JSON contract.
158/// User code outside the self-host always goes through the regular
159/// [`run_type_check_full`] and stays bound by the opaque rules.
160pub fn run_type_check_full_self_host(
161 items: &[TopLevel],
162 base_dir: Option<&str>,
163) -> TypeCheckResult {
164 let mut checker = TypeChecker::new_with_symbols(build_symbols_for_items(items, base_dir));
165 checker.self_host_mode = true;
166 checker.check(items, base_dir);
167 finalize_check_result(checker, items)
168}
169
170/// Self-host variant of [`run_type_check_with_loaded`]. See
171/// [`run_type_check_full_self_host`] for the opaque-bypass rationale.
172pub fn run_type_check_with_loaded_self_host(
173 items: &[TopLevel],
174 loaded: &[crate::source::LoadedModule],
175) -> TypeCheckResult {
176 let mut checker = TypeChecker::new_with_symbols(build_symbols_with_loaded(items, loaded));
177 checker.self_host_mode = true;
178 checker.check_with_loaded(items, loaded);
179 finalize_check_result(checker, items)
180}
181
182fn finalize_check_result(mut checker: TypeChecker, items: &[TopLevel]) -> TypeCheckResult {
183 // Phase B (post peer-review #148): flatten the internal split
184 // (`fn_sigs` keyed by `FnId`, `extra_sigs` keyed by canonical
185 // string) into the exported `HashMap<String, _>` external
186 // consumers expect. The old bare-alias mirror was last-write-wins
187 // across modules: when two distinct dep modules each exposed `foo`,
188 // one would silently win the global `"foo"` slot and Rust codegen's
189 // `ctx.fn_sigs.get(&fd.name)` could pick up the wrong signature.
190 //
191 // Now bare aliases land only when unambiguous. Each canonical key
192 // is always present; bare-name keys land only when no other fn in
193 // the program shares that bare name. Consumers that previously
194 // relied on a non-deterministic global "foo" entry will see a miss
195 // and surface a clear "qualify the reference" error instead of
196 // mismatched parameters.
197 let entry_prefix = checker.current_module_prefix.clone();
198 let mut fn_sigs: HashMap<String, (Vec<Type>, Type, Vec<String>)> = HashMap::new();
199
200 // Iterate in `FnId` order for deterministic output (HashMap
201 // iteration order would otherwise leak non-determinism into the
202 // exported map and downstream diagnostics).
203 let mut ordered_user: Vec<(FnId, &FnSig)> =
204 checker.fn_sigs.iter().map(|(id, sig)| (*id, sig)).collect();
205 ordered_user.sort_by_key(|(id, _)| id.0);
206
207 // Tally bare-name owners. Phase B (peer review round 2): an
208 // entry-scope fn shadows any dep-module fn sharing the same bare
209 // name — source-level `doit()` inside `Main` unambiguously means
210 // `Main.doit`, even when a dep also exposes `doit`. We therefore
211 // suppress the bare alias only for dep-dep ambiguity (multiple
212 // dep modules share a bare name *and* the entry doesn't define
213 // one). When the entry owns the bare name, the bare alias points
214 // at the entry FnId; dep fns stay reachable only by qualified
215 // name.
216 let mut bare_entry_owner: HashMap<String, FnId> = HashMap::new();
217 let mut bare_dep_owners: HashMap<String, FnId> = HashMap::new();
218 let mut bare_dep_ambiguous: HashSet<String> = HashSet::new();
219 for (id, _) in &ordered_user {
220 let entry = checker.symbol_table.fn_entry(*id);
221 let bare = entry.key.name.as_str();
222 if entry.module.is_entry() {
223 bare_entry_owner.insert(bare.to_string(), *id);
224 continue;
225 }
226 match bare_dep_owners.get(bare) {
227 None => {
228 bare_dep_owners.insert(bare.to_string(), *id);
229 }
230 Some(prior) if prior == id => {}
231 Some(_) => {
232 bare_dep_ambiguous.insert(bare.to_string());
233 }
234 }
235 }
236
237 for (id, sig) in &ordered_user {
238 let entry = checker.symbol_table.fn_entry(*id);
239 let canonical = if entry.module.is_entry() {
240 match entry_prefix.as_deref() {
241 Some(prefix) => crate::visibility::qualified_name(prefix, &entry.key.name),
242 None => entry.key.name.clone(),
243 }
244 } else {
245 entry.key.canonical()
246 };
247 let triple = (sig.params.clone(), sig.ret.clone(), sig.effects.clone());
248 fn_sigs.insert(canonical.clone(), triple.clone());
249 // Bare alias rules:
250 // - entry-scope owner always wins (shadows any dep with the
251 // same bare name);
252 // - dep-scope fn gets the bare alias only when (a) the entry
253 // doesn't own it and (b) no other dep module conflicts.
254 if entry.key.name == canonical {
255 continue;
256 }
257 let is_entry_owner = bare_entry_owner.get(&entry.key.name) == Some(id);
258 let mut emit_bare = false;
259 if entry.module.is_entry() {
260 emit_bare = is_entry_owner;
261 } else if !bare_entry_owner.contains_key(&entry.key.name)
262 && !bare_dep_ambiguous.contains(&entry.key.name)
263 {
264 emit_bare = true;
265 }
266 if emit_bare {
267 fn_sigs.entry(entry.key.name.clone()).or_insert(triple);
268 }
269 }
270 for (k, sig) in &checker.extra_sigs {
271 fn_sigs
272 .entry(k.clone())
273 .or_insert_with(|| (sig.params.clone(), sig.ret.clone(), sig.effects.clone()));
274 }
275
276 check_module_effect_boundary(items, &mut checker.errors);
277
278 TypeCheckResult {
279 errors: checker.errors,
280 fn_sigs,
281 unused_bindings: checker.unused_warnings,
282 }
283}
284
285/// Enforce module-level `effects [...]` declaration against per-fn effect
286/// usage. The rule:
287///
288/// - Module without `effects [...]` → legacy/mixed, no enforcement (0.13
289/// migration shim; 0.14+ may upgrade to soft warning).
290/// - Module with `effects [...]` (including `effects []` for explicit pure)
291/// → every function's `! [...]` must be covered by the module's declared
292/// surface. A namespace-level entry like `Disk` admits any `Disk.*`
293/// method; a method-level entry like `Time.now` admits only that one.
294fn check_module_effect_boundary(items: &[TopLevel], errors: &mut Vec<TypeError>) {
295 let Some(allowed) = items.iter().find_map(|i| match i {
296 TopLevel::Module(m) => m.effects.as_ref().map(|e| (e, m)),
297 _ => None,
298 }) else {
299 return;
300 };
301 let (allowed_list, module) = allowed;
302
303 let allowed_namespaces: HashSet<&str> = allowed_list
304 .iter()
305 .filter(|e| !e.contains('.'))
306 .map(|e| e.as_str())
307 .collect();
308 let allowed_methods: HashSet<&str> = allowed_list.iter().map(|e| e.as_str()).collect();
309
310 for item in items {
311 let TopLevel::FnDef(fd) = item else { continue };
312 for eff in &fd.effects {
313 let method = eff.node.as_str();
314 if allowed_methods.contains(method) {
315 continue;
316 }
317 if let Some((ns, _)) = method.split_once('.')
318 && allowed_namespaces.contains(ns)
319 {
320 continue;
321 }
322 errors.push(TypeError {
323 message: format!(
324 "module '{}' declared `effects [{}]` but '{}' uses '{}' which is not in the declared boundary",
325 module.name,
326 allowed_list.join(", "),
327 fd.name,
328 method
329 ),
330 line: eff.line,
331 col: 1,
332 secondary: module.effects_line.map(|l| TypeErrorSpan {
333 line: l,
334 col: 1,
335 label: "module effects declared here".to_string(),
336 }),
337 });
338 }
339 }
340}
341
342// ---------------------------------------------------------------------------
343// Internal structures
344// ---------------------------------------------------------------------------
345
346#[derive(Debug, Clone)]
347struct FnSig {
348 params: Vec<Type>,
349 ret: Type,
350 effects: Vec<String>,
351}
352
353/// Iron — A5: typed key for `record_field_types`. Pre-A5 the map
354/// was keyed by `"TypeName.fieldName"` stringifications, which
355/// forced every reader to `strip_prefix(format!("{type}."))` and
356/// then re-check that the remainder didn't itself contain a dot
357/// (because the post-A3 dual-keying mirrored each entry under both
358/// the canonical `"Module.Type.field"` form and the bare alias
359/// `"Type.field"` — and the canonical form spuriously matched the
360/// `"Module."` prefix-strip when the read came from a module
361/// looking up its own fields). The struct key separates the two
362/// dimensions, so the canonical resolution happens once at
363/// insert/lookup (via `sig_aliases`) and iteration filters on
364/// `key.type_name == canonical` with no string-shape gymnastics.
365#[derive(Debug, Clone, PartialEq, Eq, Hash)]
366pub(crate) struct RecordFieldKey {
367 pub(crate) type_name: String,
368 pub(crate) field_name: String,
369}
370
371impl RecordFieldKey {
372 pub(crate) fn new(type_name: impl Into<String>, field_name: impl Into<String>) -> Self {
373 Self {
374 type_name: type_name.into(),
375 field_name: field_name.into(),
376 }
377 }
378}
379
380/// Bare-name resolution result. Tracks ambiguity explicitly so
381/// `resolve_fn_id` / `resolve_type_id` can refuse the look-up when
382/// two distinct identities surface the same source name. The
383/// `Ambiguous` variant carries the actual candidate IDs (only those
384/// that made it through visibility, since that's the population
385/// path) so diagnostics can suggest exactly the names the user can
386/// actually pick from — never a private dep type that happens to
387/// share the bare name.
388#[derive(Debug, Clone)]
389enum Resolution<T> {
390 Single(T),
391 Ambiguous(Vec<T>),
392}
393
394impl<T: Copy + PartialEq> Resolution<T> {
395 /// Merge another candidate identity into an alias entry. Two
396 /// distinct identities for the same bare name produce
397 /// `Ambiguous`; a duplicate registration of the same identity is
398 /// a no-op (same module re-traversed by another path).
399 fn merge(&mut self, candidate: T) {
400 match self {
401 Resolution::Single(existing) if *existing == candidate => {}
402 Resolution::Single(existing) => {
403 let prior = *existing;
404 *self = Resolution::Ambiguous(vec![prior, candidate]);
405 }
406 Resolution::Ambiguous(seen) => {
407 if !seen.contains(&candidate) {
408 seen.push(candidate);
409 }
410 }
411 }
412 }
413
414 fn unambiguous(&self) -> Option<T> {
415 match self {
416 Resolution::Single(v) => Some(*v),
417 Resolution::Ambiguous(_) => None,
418 }
419 }
420}
421
422struct TypeChecker {
423 /// Resolved-identity table — phase B (#138). Populated by
424 /// [`TypeChecker::new_with_symbols`] from the program's `entry_items
425 /// + dep_modules` before any signature registration. Carries opaque
426 /// `FnId` / `TypeId` for every user-defined fn and type. The
427 /// checker resolves bare-name references through it instead of the
428 /// pre-phase-B `sig_aliases` string→string map.
429 symbol_table: SymbolTable,
430 /// User-defined function signatures, keyed by the opaque `FnId`
431 /// from `symbol_table`. Phase B (#138) migrated this away from
432 /// `HashMap<String, FnSig>` so that two modules each declaring `foo`
433 /// can't silently collide through bare-name keying.
434 ///
435 /// Built-in fn signatures (namespace methods like `Int.add`,
436 /// `Console.print`) and user constructors (variants of sum types,
437 /// product type constructors keyed by `"Module.Type.Variant"`)
438 /// don't have `FnId`s in the program symbol table — those live in
439 /// `extra_sigs` keyed by canonical string. `find_fn_sig` chains
440 /// the two for a unified bare-name → signature lookup.
441 fn_sigs: HashMap<FnId, FnSig>,
442 /// Builtin fn signatures + constructor signatures keyed by
443 /// canonical string. The non-`FnId` half of the split — entries
444 /// here either come from `register_builtins` (namespace methods)
445 /// or `register_type_def_sigs` (sum-type variant constructors,
446 /// product type "callable name" entries). Lookups against this
447 /// map use the canonical key directly; bare→canonical resolution
448 /// goes through `symbol_table.type_id_of` for type-derived keys.
449 extra_sigs: HashMap<String, FnSig>,
450 /// Bare → `FnId` aliases for cross-module imports. Populated
451 /// during `integrate_registry` from visibility-exposed aliases
452 /// (and during `build_signatures` for the current module's own
453 /// fns). The typed replacement for the pre-phase-B
454 /// `sig_aliases: HashMap<String, String>`: same bare-name routing
455 /// role, but the value side now carries opaque identity instead
456 /// of a string that needed re-resolution downstream.
457 ///
458 /// When two distinct modules each expose the same bare name
459 /// (`Pricing.percent` *and* `Math.percent` both surface a bare
460 /// `percent`), the entry switches to [`Resolution::Ambiguous`]
461 /// and `resolve_fn_id` refuses to silently pick one — the user
462 /// must qualify the reference. Avoids the
463 /// "global bare alias last-wins" bug class peer review #148
464 /// flagged.
465 bare_fn_aliases: HashMap<String, Resolution<FnId>>,
466 /// Bare → `TypeId` aliases. Same role as `bare_fn_aliases`
467 /// for the type-name dimension; consumed by `resolve_type_id`
468 /// and `canonical_type_name`.
469 bare_type_aliases: HashMap<String, Resolution<TypeId>>,
470 /// `FnId`s the current checker may legitimately resolve to.
471 /// Populated from `build_signatures` (the current module's own
472 /// fns — always visible from within the module) and from
473 /// `integrate_registry` (visibility-exposed entries from each
474 /// dep module — already filtered against the `exposes` contract
475 /// by `crate::visibility::SymbolRegistry::from_modules`).
476 ///
477 /// `resolve_fn_id` consults this set so a qualified
478 /// `C.helper()` reference fails when `helper` isn't exposed even
479 /// though the symbol table — which carries every fn from every
480 /// dep module regardless of visibility — has its `FnId`.
481 visible_fn_ids: HashSet<FnId>,
482 /// `TypeId`s the current checker may legitimately resolve to.
483 /// Same role as `visible_fn_ids` for the type-name dimension —
484 /// closes the qualified-private-import leak peer review round 4
485 /// flagged on `fn takes(s: C.Shape)` references against a `C`
486 /// whose `exposes` list didn't include `Shape`.
487 visible_type_ids: HashSet<TypeId>,
488 /// Per-module `depends` list, keyed by the dep module's
489 /// `dep_name`. Populated by `integrate_loaded_modules` so the
490 /// per-owner type resolver (`canonicalize_named_in_module`) can
491 /// walk an owner module's *own* depends when canonicalising its
492 /// exported signatures — not the entry module's or arbitrary
493 /// loaded siblings'. Round-6 peer review caught the leak: B
494 /// depends on A; Main depends on B, C; B's bare `Shape` was
495 /// resolving against `[A, C]` (Main's loaded tree) instead of
496 /// `[A]` (B's own depends), and `Shape` came back ambiguous.
497 module_depends: HashMap<String, Vec<String>>,
498 value_members: HashMap<String, Type>,
499 /// Field types for record types, keyed by `(type_name, field_name)`.
500 /// Populated for both user-defined `record` types and built-in records
501 /// (HttpResponse, Header). Single entry per (canonical type name, field);
502 /// lookup canonicalises `type_name` through `SymbolTable` at read time.
503 /// Enables checked dot-access on Named types.
504 record_field_types: HashMap<RecordFieldKey, Type>,
505 /// Variant names for sum types: "Shape" → ["Circle", "Rect", "Point"].
506 /// Pre-populated for Result and Option; extended by user-defined sum types.
507 type_variants: HashMap<String, Vec<String>>,
508 /// Module prefix of the items currently being checked. `None`
509 /// while checking entry-scope items. Per-module sub-checkers
510 /// (`check_loaded_module_bodies`) set this to the dep module's
511 /// prefix so bare-name resolution finds the local type/fn first.
512 current_module_prefix: Option<String>,
513 /// Top-level bindings visible from function bodies.
514 globals: HashMap<String, Type>,
515 /// Local bindings in the current function/scope.
516 locals: HashMap<String, Type>,
517 errors: Vec<TypeError>,
518 /// Return type of the function currently being checked; None at top level.
519 current_fn_ret: Option<Type>,
520 /// Line number of the function currently being checked; None at top level.
521 current_fn_line: Option<usize>,
522 /// Type names that are opaque in this module's context (imported via `exposes opaque`).
523 opaque_types: HashSet<String>,
524 /// When `true`, opaque-type construction + field-access + pattern-match
525 /// checks are bypassed. Used only by the self-host compile path
526 /// (`aver compile --with-self-host-support`) where
527 /// `self_hosted/domain/builtins.av` round-trips opaque host types
528 /// (e.g. `Tcp.Connection`) through the replay `Val` representation:
529 /// it serialises by reading `.id` / `.host` / `.port`, and
530 /// reconstructs by `Tcp.Connection(id = …, host = …, port = …)` on
531 /// replay deserialise. Both operations are forbidden in user code by
532 /// design (Phase 4.7+ fix #11), but the self-host has to read +
533 /// write the underlying record shape because that's the contract
534 /// with the replay JSON format. The flag is set by
535 /// [`run_type_check_full_self_host`] / [`run_type_check_with_loaded_self_host`]
536 /// and never user-toggleable from source.
537 self_host_mode: bool,
538 /// Names referenced during type checking of current function body (for unused detection).
539 used_names: HashSet<String>,
540 /// Bindings defined in the current function body: (name, line).
541 fn_bindings: Vec<(String, usize)>,
542 /// Unused binding warnings collected during checking: (binding_name, fn_name, line).
543 unused_warnings: Vec<(String, String, usize)>,
544 /// Oracle v1: `.result` / `.trace` / `.trace.*` projections are
545 /// only meaningful inside `verify <fn> trace` cases. This flag is
546 /// set true while checking such a case's LHS / RHS, false
547 /// otherwise. Outside verify-trace the projections are rejected at
548 /// check time — otherwise user code would type-check then crash
549 /// at runtime with "namespace has no member 'trace'".
550 in_verify_trace_context: bool,
551}
552
553impl TypeChecker {
554 fn new_with_symbols(symbol_table: SymbolTable) -> Self {
555 let mut type_variants = HashMap::new();
556 type_variants.insert(
557 "Result".to_string(),
558 vec!["Ok".to_string(), "Err".to_string()],
559 );
560 type_variants.insert(
561 "Option".to_string(),
562 vec!["Some".to_string(), "None".to_string()],
563 );
564
565 let mut tc = TypeChecker {
566 symbol_table,
567 fn_sigs: HashMap::new(),
568 extra_sigs: HashMap::new(),
569 bare_fn_aliases: HashMap::new(),
570 bare_type_aliases: HashMap::new(),
571 visible_fn_ids: HashSet::new(),
572 visible_type_ids: HashSet::new(),
573 module_depends: HashMap::new(),
574 value_members: HashMap::new(),
575 record_field_types: HashMap::new(),
576 type_variants,
577 current_module_prefix: None,
578 globals: HashMap::new(),
579 locals: HashMap::new(),
580 errors: Vec::new(),
581 current_fn_ret: None,
582 current_fn_line: None,
583 opaque_types: HashSet::new(),
584 self_host_mode: false,
585 used_names: HashSet::new(),
586 fn_bindings: Vec::new(),
587 unused_warnings: Vec::new(),
588 in_verify_trace_context: false,
589 };
590 tc.register_builtins();
591 tc
592 }
593
594 // -- Identity resolution (phase B) -------------------------------------
595
596 /// Resolve a source-faithful function reference (`"foo"`,
597 /// `"Module.foo"`, `"Tcp.send"`) to a `FnId` via the symbol table.
598 /// Tries, in order: literal-as-qualified (split `"Module.foo"`
599 /// into `(Module, foo)`), current-module-scoped bare name, then
600 /// entry-scope bare name, then the typed bare-alias map.
601 ///
602 /// Misses for builtin namespace methods and constructors — those
603 /// don't live in the program symbol table; callers fall back to
604 /// the `extra_sigs` string-keyed half.
605 pub(crate) fn resolve_fn_id(&self, name: &str) -> Option<FnId> {
606 // Phase B (peer review round 4): every `SymbolTable`
607 // resolution path filters through `visible_fn_ids` so a
608 // qualified `C.helper()` reference can't reach into a dep
609 // module's private fn just because the symbol table
610 // unconditionally stores every dep entry. Bare-name lookup
611 // already went through `bare_fn_aliases`, which is itself
612 // populated only from visibility-exposed entries — that
613 // branch stays as-is.
614 if let Some((prefix, n)) = name.rsplit_once('.') {
615 if let Some(id) = self.symbol_table.fn_id_of(&FnKey::in_module(prefix, n))
616 && self.visible_fn_ids.contains(&id)
617 {
618 return Some(id);
619 }
620 if self.current_module_prefix.as_deref() == Some(prefix)
621 && let Some(id) = self.symbol_table.fn_id_of(&FnKey::entry(n))
622 && self.visible_fn_ids.contains(&id)
623 {
624 return Some(id);
625 }
626 }
627 if let Some(prefix) = self.current_module_prefix.as_deref()
628 && let Some(id) = self.symbol_table.fn_id_of(&FnKey::in_module(prefix, name))
629 && self.visible_fn_ids.contains(&id)
630 {
631 return Some(id);
632 }
633 if let Some(id) = self.symbol_table.fn_id_of(&FnKey::entry(name))
634 && self.visible_fn_ids.contains(&id)
635 {
636 return Some(id);
637 }
638 self.bare_fn_aliases
639 .get(name)
640 .and_then(Resolution::unambiguous)
641 }
642
643 /// `true` when the bare alias map has recorded multiple distinct
644 /// `TypeId`s for `name` (cross-module same-bare-name import).
645 /// Distinct from "name doesn't resolve at all" — used by the
646 /// matcher to decide whether a mixed (Some, None) typed/raw
647 /// comparison should fall back to name equality (only when the
648 /// `None` side is genuinely a builtin / external name, not when
649 /// it's ambiguous bare reference whose typed identity we
650 /// deliberately suppressed).
651 pub(crate) fn type_name_is_ambiguous(&self, name: &str) -> bool {
652 matches!(
653 self.bare_type_aliases.get(name),
654 Some(Resolution::Ambiguous(_))
655 )
656 }
657
658 /// List the canonical names of every type that the bare alias map
659 /// recorded as a candidate for `bare`. The `Resolution::Ambiguous`
660 /// variant carries the actual conflicting `TypeId`s populated
661 /// through visibility-exposed aliases — never scans the full
662 /// `symbol_table.types`, so a private (non-exposed) dep type that
663 /// happens to share a bare name never appears in the diagnostic.
664 pub(crate) fn ambiguous_type_candidates(&self, bare: &str) -> Vec<String> {
665 let Some(Resolution::Ambiguous(ids)) = self.bare_type_aliases.get(bare) else {
666 return Vec::new();
667 };
668 let mut out: Vec<String> = ids
669 .iter()
670 .map(|id| self.symbol_table.type_entry(*id).key.canonical())
671 .collect();
672 out.sort();
673 out
674 }
675
676 /// Walk `ty` and emit diagnostics for every distinct unresolved
677 /// reason the typechecker deliberately blocked resolution.
678 /// Called from the signature-registration boundary
679 /// (`build_signatures`, `register_type_def_sigs`, flow's binding
680 /// annotations) so the user gets a clean explanation instead of
681 /// downstream `expected X, got X` cascades. Two reasons are
682 /// surfaced:
683 ///
684 /// - Ambiguous bare reference: `Foo` matches multiple
685 /// visibility-exposed `TypeId`s. Diagnostic suggests the
686 /// qualified forms.
687 /// - Private qualified import: `Module.Foo` exists in the
688 /// symbol table but isn't on `Module`'s `exposes` list.
689 /// Diagnostic names the dep + asks for the export.
690 pub(super) fn report_ambiguous_named(&mut self, ty: &Type, line: usize, source_ctx: &str) {
691 let mut seen_ambig: HashSet<String> = HashSet::new();
692 let mut seen_private: HashSet<String> = HashSet::new();
693 self.collect_unresolved_into(ty, &mut seen_ambig, &mut seen_private);
694 for name in seen_ambig {
695 let candidates = self.ambiguous_type_candidates(&name);
696 if candidates.is_empty() {
697 continue;
698 }
699 let suggestion = match candidates.as_slice() {
700 [a] => a.clone(),
701 [a, b] => format!("`{}` or `{}`", a, b),
702 more => {
703 let last = more.last().expect("non-empty");
704 let head = &more[..more.len() - 1];
705 let joined = head
706 .iter()
707 .map(|c| format!("`{}`", c))
708 .collect::<Vec<_>>()
709 .join(", ");
710 format!("{}, or `{}`", joined, last)
711 }
712 };
713 self.error_at_line(
714 line,
715 format!(
716 "{source_ctx}: Ambiguous type name '{name}'; use {suggestion} to disambiguate"
717 ),
718 );
719 }
720 for qualified in seen_private {
721 let (module, type_name) = qualified
722 .rsplit_once('.')
723 .map(|(m, t)| (m.to_string(), t.to_string()))
724 .expect("private qualified name always has a `.`");
725 self.error_at_line(
726 line,
727 format!(
728 "{source_ctx}: Type '{qualified}' is not exposed by module '{module}' — add '{type_name}' to its `exposes` list to import it",
729 ),
730 );
731 }
732 }
733
734 fn collect_unresolved_into(
735 &self,
736 ty: &Type,
737 ambig: &mut HashSet<String>,
738 private: &mut HashSet<String>,
739 ) {
740 match ty {
741 Type::Named { id: None, name } => {
742 if self.type_name_is_ambiguous(name) {
743 ambig.insert(name.clone());
744 } else if self.type_name_is_private_import(name) {
745 private.insert(name.clone());
746 }
747 }
748 Type::Named { .. }
749 | Type::Int
750 | Type::Float
751 | Type::Str
752 | Type::Bool
753 | Type::Unit
754 | Type::Var(_)
755 | Type::Invalid => {}
756 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
757 self.collect_unresolved_into(inner, ambig, private);
758 }
759 Type::Result(ok, err) => {
760 self.collect_unresolved_into(ok, ambig, private);
761 self.collect_unresolved_into(err, ambig, private);
762 }
763 Type::Map(k, v) => {
764 self.collect_unresolved_into(k, ambig, private);
765 self.collect_unresolved_into(v, ambig, private);
766 }
767 Type::Tuple(items) => {
768 for item in items {
769 self.collect_unresolved_into(item, ambig, private);
770 }
771 }
772 Type::Fn(params, ret, _) => {
773 for p in params {
774 self.collect_unresolved_into(p, ambig, private);
775 }
776 self.collect_unresolved_into(ret, ambig, private);
777 }
778 }
779 }
780
781 /// Narrowing: a function TYPE (`Fn(...)`) may appear only as a *direct
782 /// function parameter type*. Reject it in every other declared-type
783 /// position — return types, record / sum-variant fields, collection /
784 /// map-value / tuple elements, binding annotations, and nested inside
785 /// another `Fn`. Aver functions are first-class values only in
786 /// call-argument position (`HttpServer.listen(port, handler)`); letting a
787 /// function value be returned, stored, or otherwise escape would make the
788 /// concrete callee — and therefore its effects — runtime-determined,
789 /// which is exactly what the static effect / Oracle / verify guarantees
790 /// rely on NOT happening.
791 ///
792 /// `allow_top_level_param` is true only at the parameter site: a parameter
793 /// may itself be a `Fn(...)` callback, but a `Fn` nested inside that
794 /// callback's own params/return is still rejected. Emits at most one error
795 /// per offending position (stops descending past a rejected `Fn`), so a
796 /// `-> Fn(A) -> Fn(B) -> C` return yields one diagnostic, not a cascade.
797 pub(super) fn reject_fn_in_type(
798 &mut self,
799 ty: &Type,
800 allow_top_level_param: bool,
801 line: usize,
802 source_ctx: &str,
803 ) {
804 match ty {
805 Type::Fn(params, ret, _) => {
806 if !allow_top_level_param {
807 self.error_at_line(
808 line,
809 format!(
810 "{source_ctx}: a function type `{}` is not allowed here. Aver permits `Fn(...)` only as a direct function parameter type \
811 (e.g. `fn run(step: Fn(Int) -> Int) -> Int`); functions are first-class values only in call-argument position \
812 (`HttpServer.listen(port, handler)`). Return a concrete value and call the function at its use site.",
813 ty.display()
814 ),
815 );
816 return;
817 }
818 // A callback parameter may not itself take or return a fn.
819 for p in params {
820 self.reject_fn_in_type(p, false, line, source_ctx);
821 }
822 self.reject_fn_in_type(ret, false, line, source_ctx);
823 }
824 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
825 self.reject_fn_in_type(inner, false, line, source_ctx);
826 }
827 Type::Result(ok, err) => {
828 self.reject_fn_in_type(ok, false, line, source_ctx);
829 self.reject_fn_in_type(err, false, line, source_ctx);
830 }
831 Type::Map(k, v) => {
832 self.reject_fn_in_type(k, false, line, source_ctx);
833 self.reject_fn_in_type(v, false, line, source_ctx);
834 }
835 Type::Tuple(items) => {
836 for item in items {
837 self.reject_fn_in_type(item, false, line, source_ctx);
838 }
839 }
840 Type::Named { .. }
841 | Type::Int
842 | Type::Float
843 | Type::Str
844 | Type::Bool
845 | Type::Unit
846 | Type::Var(_)
847 | Type::Invalid => {}
848 }
849 }
850
851 /// `true` if `ty` is a `Fn(...)` or structurally contains one (in a
852 /// collection element, tuple slot, etc.). Used to reject binding a
853 /// function value in any shape (`g = double`, `gs = [double, inc]`).
854 pub(super) fn type_contains_fn(&self, ty: &Type) -> bool {
855 match ty {
856 Type::Fn(..) => true,
857 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
858 self.type_contains_fn(inner)
859 }
860 Type::Result(ok, err) => self.type_contains_fn(ok) || self.type_contains_fn(err),
861 Type::Map(k, v) => self.type_contains_fn(k) || self.type_contains_fn(v),
862 Type::Tuple(items) => items.iter().any(|i| self.type_contains_fn(i)),
863 Type::Named { .. }
864 | Type::Int
865 | Type::Float
866 | Type::Str
867 | Type::Bool
868 | Type::Unit
869 | Type::Var(_)
870 | Type::Invalid => false,
871 }
872 }
873
874 /// Register a bare → `FnId` alias, marking it `Ambiguous` if a
875 /// different identity is already registered under the same bare
876 /// name. Duplicate registration of the same identity (e.g. an
877 /// item walked twice by `integrate_registry` + `build_signatures`)
878 /// is a no-op.
879 pub(super) fn merge_bare_fn_alias(&mut self, alias: String, id: FnId) {
880 self.bare_fn_aliases
881 .entry(alias)
882 .and_modify(|r| r.merge(id))
883 .or_insert(Resolution::Single(id));
884 }
885
886 pub(super) fn merge_bare_type_alias(&mut self, alias: String, id: TypeId) {
887 self.bare_type_aliases
888 .entry(alias)
889 .and_modify(|r| r.merge(id))
890 .or_insert(Resolution::Single(id));
891 }
892
893 /// Type-side equivalent of [`Self::resolve_fn_id`]. Same
894 /// visibility gating: `SymbolTable` look-ups are filtered through
895 /// `visible_type_ids` so a qualified `C.Shape` reference can't
896 /// resolve to a private (non-exposed) type even though the symbol
897 /// table holds every dep type unconditionally.
898 pub(crate) fn resolve_type_id(&self, name: &str) -> Option<TypeId> {
899 if let Some((prefix, n)) = name.rsplit_once('.') {
900 if let Some(id) = self.symbol_table.type_id_of(&TypeKey::in_module(prefix, n))
901 && self.visible_type_ids.contains(&id)
902 {
903 return Some(id);
904 }
905 if self.current_module_prefix.as_deref() == Some(prefix)
906 && let Some(id) = self.symbol_table.type_id_of(&TypeKey::entry(n))
907 && self.visible_type_ids.contains(&id)
908 {
909 return Some(id);
910 }
911 }
912 if let Some(prefix) = self.current_module_prefix.as_deref()
913 && let Some(id) = self
914 .symbol_table
915 .type_id_of(&TypeKey::in_module(prefix, name))
916 && self.visible_type_ids.contains(&id)
917 {
918 return Some(id);
919 }
920 if let Some(id) = self.symbol_table.type_id_of(&TypeKey::entry(name))
921 && self.visible_type_ids.contains(&id)
922 {
923 return Some(id);
924 }
925 self.bare_type_aliases
926 .get(name)
927 .and_then(Resolution::unambiguous)
928 }
929
930 /// `true` when `name` (qualified `Module.Type` form) resolves to
931 /// an existing `TypeId` in the symbol table but the typechecker
932 /// hasn't registered that ID as visible to the current scope.
933 /// Distinguishes "type doesn't exist anywhere" (silently miss →
934 /// downstream "unknown type" error) from "type exists but its
935 /// dep module doesn't expose it" (explicit private-import
936 /// diagnostic emitted by `report_named_visibility_errors`).
937 pub(crate) fn type_name_is_private_import(&self, name: &str) -> bool {
938 let Some((prefix, n)) = name.rsplit_once('.') else {
939 return false;
940 };
941 if self.current_module_prefix.as_deref() == Some(prefix) {
942 // Self-references like `Main.foo` inside `Main` always
943 // resolve through the entry-scope alias; never a privacy
944 // failure.
945 return false;
946 }
947 let Some(id) = self.symbol_table.type_id_of(&TypeKey::in_module(prefix, n)) else {
948 return false;
949 };
950 !self.visible_type_ids.contains(&id)
951 }
952
953 /// Canonical name (`"Module.Type"` or bare entry name) for a
954 /// source-faithful type reference. Resolves through the symbol
955 /// table; falls back to the input string for references the table
956 /// doesn't know about (builtins, opaque host types, in-flight
957 /// recovery from earlier errors).
958 ///
959 /// For entry-scope types in a checker that's currently processing
960 /// items with a `module X` declaration, the returned name includes
961 /// the `X.` prefix even though the symbol table itself stores
962 /// entry items without one. This preserves the pre-phase-B
963 /// canonical view the typechecker's internal maps
964 /// (`type_variants`, `record_field_types`, …) are keyed against.
965 pub(crate) fn canonical_type_name(&self, name: &str) -> String {
966 match self.resolve_type_id(name) {
967 Some(id) => {
968 let entry = self.symbol_table.type_entry(id);
969 if entry.module.is_entry()
970 && let Some(prefix) = self.current_module_prefix.as_deref()
971 {
972 crate::visibility::qualified_name(prefix, &entry.key.name)
973 } else {
974 entry.key.canonical()
975 }
976 }
977 None => name.to_string(),
978 }
979 }
980
981 // -- Unified lookups ---------------------------------------------------
982
983 fn find_fn_sig(&self, key: &str) -> Option<&FnSig> {
984 // Phase B: user fns live in `fn_sigs` keyed by `FnId`; everything
985 // else (builtins + sum-type variant constructors) stays in
986 // `extra_sigs`. Direct hit on `extra_sigs` covers references that
987 // came in already-canonicalised; `resolve_fn_id` chains the
988 // symbol-table lookups for bare/qualified user-fn references.
989 if let Some(id) = self.resolve_fn_id(key)
990 && let Some(sig) = self.fn_sigs.get(&id)
991 {
992 return Some(sig);
993 }
994 if let Some(sig) = self.extra_sigs.get(key) {
995 return Some(sig);
996 }
997 // Try canonicalised form for type-derived keys
998 // (`"Module.Type.Variant"`).
999 let canonical = self.canonical_extra_key(key);
1000 if canonical != key {
1001 return self.extra_sigs.get(&canonical);
1002 }
1003 None
1004 }
1005
1006 /// Take a bare-or-qualified key that may name a constructor or a
1007 /// per-type member (`"Shape.Circle"`, `"Status.Open"`) and resolve
1008 /// the leading type segment through `canonical_type_name`. Uses
1009 /// the typechecker view of canonical names (which include the
1010 /// entry module's prefix), matching what `register_type_def_sigs`
1011 /// inserts into `extra_sigs`.
1012 fn canonical_extra_key(&self, key: &str) -> String {
1013 if let Some((head, tail)) = key.split_once('.') {
1014 let canonical_type = self.canonical_type_name(head);
1015 if canonical_type != head {
1016 return format!("{}.{}", canonical_type, tail);
1017 }
1018 }
1019 key.to_string()
1020 }
1021
1022 fn find_value_member(&self, key: &str) -> Option<&Type> {
1023 if let Some(v) = self.value_members.get(key) {
1024 return Some(v);
1025 }
1026 let canonical = self.canonical_extra_key(key);
1027 if canonical != key {
1028 return self.value_members.get(&canonical);
1029 }
1030 None
1031 }
1032
1033 fn find_record_field_type(&self, type_name: &str, field_name: &str) -> Option<&Type> {
1034 let direct = RecordFieldKey::new(type_name, field_name);
1035 if let Some(ty) = self.record_field_types.get(&direct) {
1036 return Some(ty);
1037 }
1038 let canonical_type = self.canonical_type_name(type_name);
1039 if canonical_type != type_name {
1040 let canonical = RecordFieldKey::new(canonical_type, field_name);
1041 return self.record_field_types.get(&canonical);
1042 }
1043 None
1044 }
1045
1046 fn fields_for_type(&self, type_name: &str) -> Vec<(String, Type)> {
1047 let canonical = self.canonical_type_name(type_name);
1048 let canonical_ref: &str = canonical.as_str();
1049 self.record_field_types
1050 .iter()
1051 .filter(|(k, _)| k.type_name == canonical_ref || k.type_name == type_name)
1052 .map(|(k, v)| (k.field_name.clone(), v.clone()))
1053 .collect()
1054 }
1055
1056 fn has_record_schema(&self, type_name: &str) -> bool {
1057 let canonical = self.canonical_type_name(type_name);
1058 let canonical_ref: &str = canonical.as_str();
1059 self.record_field_types
1060 .keys()
1061 .any(|k| k.type_name == canonical_ref || k.type_name == type_name)
1062 }
1063
1064 /// Look up the variant list for a named sum type. Resolves
1065 /// `name` through `canonical_type_name` so bare references find
1066 /// the canonical "Module.Type" entry registered by
1067 /// `register_type_def_sigs` / `integrate_registry`.
1068 pub(crate) fn variants_for(&self, name: &str) -> Option<&Vec<String>> {
1069 if let Some(v) = self.type_variants.get(name) {
1070 return Some(v);
1071 }
1072 let canonical = self.canonical_type_name(name);
1073 if canonical != name {
1074 return self.type_variants.get(&canonical);
1075 }
1076 None
1077 }
1078
1079 pub(crate) fn has_variants_for(&self, name: &str) -> bool {
1080 self.variants_for(name).is_some()
1081 }
1082
1083 /// Iterate every fn signature regardless of which storage half
1084 /// holds it. Used by the namespace-prefix check and by
1085 /// `finalize_check_result` to flatten for external export.
1086 fn all_fn_sigs(&self) -> impl Iterator<Item = (String, &FnSig)> + '_ {
1087 let from_user = self.fn_sigs.iter().map(|(id, sig)| {
1088 let name = self.symbol_table.fn_entry(*id).key.canonical();
1089 (name, sig)
1090 });
1091 let from_extra = self.extra_sigs.iter().map(|(k, sig)| (k.clone(), sig));
1092 from_user.chain(from_extra)
1093 }
1094
1095 fn fn_sig_contains_canonical(&self, canonical: &str) -> bool {
1096 if let Some(id) = self.resolve_fn_id(canonical)
1097 && self.fn_sigs.contains_key(&id)
1098 {
1099 return true;
1100 }
1101 if self.extra_sigs.contains_key(canonical) {
1102 return true;
1103 }
1104 let canonical_form = self.canonical_extra_key(canonical);
1105 canonical_form != canonical && self.extra_sigs.contains_key(&canonical_form)
1106 }
1107
1108 /// Insert a fn signature under its canonical form. Routes to the
1109 /// `FnId`-keyed user map when the name resolves through the
1110 /// symbol table (i.e. it names a user fn declared in `items` or a
1111 /// dep module); otherwise it lands in the `extra_sigs` half.
1112 ///
1113 /// Also marks the `FnId` as visible to the current scope — every
1114 /// fn the checker's own `build_signatures` / `integrate_registry`
1115 /// path inserts is by definition reachable from here (own module
1116 /// items or visibility-exposed dep entries). The visibility
1117 /// gating in `resolve_fn_id` then refuses look-ups against any
1118 /// `FnId` that landed in the symbol table but not in this set —
1119 /// a qualified `C.helper()` reference whose `helper` isn't on
1120 /// `C`'s `exposes` list never gets inserted here and so never
1121 /// resolves.
1122 fn insert_fn_sig(&mut self, canonical: &str, sig: FnSig) {
1123 match self.fn_id_for_canonical(canonical) {
1124 Some(id) => {
1125 self.fn_sigs.insert(id, sig);
1126 self.visible_fn_ids.insert(id);
1127 }
1128 None => {
1129 self.extra_sigs.insert(canonical.to_string(), sig);
1130 }
1131 }
1132 }
1133
1134 /// `resolve_fn_id` minus the visibility filter — used by
1135 /// `insert_fn_sig` (where the very point of the insert is to
1136 /// register visibility) and by other boundary points that build
1137 /// the visible set itself. Production look-ups must go through
1138 /// `resolve_fn_id`.
1139 fn fn_id_for_canonical(&self, name: &str) -> Option<FnId> {
1140 if let Some((prefix, n)) = name.rsplit_once('.') {
1141 if let Some(id) = self.symbol_table.fn_id_of(&FnKey::in_module(prefix, n)) {
1142 return Some(id);
1143 }
1144 if self.current_module_prefix.as_deref() == Some(prefix)
1145 && let Some(id) = self.symbol_table.fn_id_of(&FnKey::entry(n))
1146 {
1147 return Some(id);
1148 }
1149 }
1150 if let Some(prefix) = self.current_module_prefix.as_deref()
1151 && let Some(id) = self.symbol_table.fn_id_of(&FnKey::in_module(prefix, name))
1152 {
1153 return Some(id);
1154 }
1155 self.symbol_table.fn_id_of(&FnKey::entry(name))
1156 }
1157
1158 /// Type-side equivalent of [`Self::fn_id_for_canonical`].
1159 fn type_id_for_canonical(&self, name: &str) -> Option<TypeId> {
1160 if let Some((prefix, n)) = name.rsplit_once('.') {
1161 if let Some(id) = self.symbol_table.type_id_of(&TypeKey::in_module(prefix, n)) {
1162 return Some(id);
1163 }
1164 if self.current_module_prefix.as_deref() == Some(prefix)
1165 && let Some(id) = self.symbol_table.type_id_of(&TypeKey::entry(n))
1166 {
1167 return Some(id);
1168 }
1169 }
1170 if let Some(prefix) = self.current_module_prefix.as_deref()
1171 && let Some(id) = self
1172 .symbol_table
1173 .type_id_of(&TypeKey::in_module(prefix, name))
1174 {
1175 return Some(id);
1176 }
1177 self.symbol_table.type_id_of(&TypeKey::entry(name))
1178 }
1179
1180 /// Mark a `TypeId` as visible to the current scope. Called from
1181 /// `register_type_def_sigs` (own module types) and
1182 /// `integrate_registry` (visibility-exposed dep types).
1183 fn mark_type_visible(&mut self, id: TypeId) {
1184 self.visible_type_ids.insert(id);
1185 }
1186
1187 // -- Helpers -----------------------------------------------------------
1188
1189 /// Check whether `required_effect` is satisfied by `caller_effects`.
1190 fn caller_has_effect(&self, caller_effects: &[String], required_effect: &str) -> bool {
1191 caller_effects
1192 .iter()
1193 .any(|declared| crate::effects::effect_satisfies(declared, required_effect))
1194 }
1195
1196 fn error(&mut self, msg: impl Into<String>) {
1197 let line = self.current_fn_line.unwrap_or(1);
1198 self.errors.push(TypeError {
1199 message: msg.into(),
1200 line,
1201 col: 0,
1202 secondary: None,
1203 });
1204 }
1205
1206 fn error_at_line(&mut self, line: usize, msg: impl Into<String>) {
1207 self.errors.push(TypeError {
1208 message: msg.into(),
1209 line,
1210 col: 0,
1211 secondary: None,
1212 });
1213 }
1214
1215 fn insert_sig(&mut self, name: &str, params: &[Type], ret: Type, effects: &[&str]) {
1216 // Builtins (Int.add, Console.print, …) are not part of the
1217 // user-program symbol table, so they always land in
1218 // `extra_sigs`. The `insert_fn_sig` router would normally
1219 // resolve user fns into `fn_sigs` — but no `register_builtins`
1220 // caller could ever name a user fn, so we short-circuit here
1221 // to avoid pointless symbol-table probes.
1222 self.extra_sigs.insert(
1223 name.to_string(),
1224 FnSig {
1225 params: params.to_vec(),
1226 ret,
1227 effects: effects.iter().map(|s| s.to_string()).collect(),
1228 },
1229 );
1230 }
1231
1232 fn fn_type_from_sig(sig: &FnSig) -> Type {
1233 Type::Fn(
1234 sig.params.clone(),
1235 Box::new(sig.ret.clone()),
1236 sig.effects.clone(),
1237 )
1238 }
1239
1240 fn sig_from_callable_type(ty: &Type) -> Option<FnSig> {
1241 match ty {
1242 Type::Fn(params, ret, effects) => Some(FnSig {
1243 params: params.clone(),
1244 ret: *ret.clone(),
1245 effects: effects.clone(),
1246 }),
1247 _ => None,
1248 }
1249 }
1250
1251 fn binding_type(&self, name: &str) -> Option<Type> {
1252 self.locals
1253 .get(name)
1254 .or_else(|| self.globals.get(name))
1255 .cloned()
1256 }
1257
1258 /// Phase B: `&self`-bearing constraint check. Resolves bare named
1259 /// types against the `SymbolTable` (carried on `self`) so
1260 /// source-faithful Spanned stamps still match against canonical fn
1261 /// signatures. Replaces the pre-phase-B `sig_aliases` string→string
1262 /// alias map with typed `TypeId` resolution.
1263 pub(super) fn compatible(&self, actual: &Type, expected: &Type) -> bool {
1264 let mut subst = HashMap::new();
1265 Self::match_expected_type_inner(actual, expected, &mut subst, Some(self))
1266 }
1267
1268 /// Static-form matcher (no symbol-table resolution). Tests use
1269 /// this directly; production code should reach for `compatible`
1270 /// instead.
1271 pub(super) fn match_expected_type(
1272 actual: &Type,
1273 expected: &Type,
1274 subst: &mut HashMap<String, Type>,
1275 ) -> bool {
1276 Self::match_expected_type_inner(actual, expected, subst, None)
1277 }
1278
1279 /// `&self` matcher that lets the caller carry a substitution
1280 /// (poly fn arg inference). The pure `compatible` helper above
1281 /// hides `subst` for the common "no `Type::Var` involved" callers;
1282 /// this method exposes it for the FnCall arg loop in `infer/expr.rs`.
1283 pub(super) fn match_with(
1284 &self,
1285 actual: &Type,
1286 expected: &Type,
1287 subst: &mut HashMap<String, Type>,
1288 ) -> bool {
1289 Self::match_expected_type_inner(actual, expected, subst, Some(self))
1290 }
1291
1292 fn match_expected_type_inner(
1293 actual: &Type,
1294 expected: &Type,
1295 subst: &mut HashMap<String, Type>,
1296 checker: Option<&TypeChecker>,
1297 ) -> bool {
1298 // Iron — A4: `Type::Invalid` is the checker's "we already
1299 // reported an error here, don't compound it" sentinel.
1300 // Returning `false` for it turned every downstream use site
1301 // into a fresh `expected X, got Invalid` diagnostic — a single
1302 // unknown-fn call could fan out to N + 1 errors (the unknown
1303 // fn plus one per downstream consumer). Treat Invalid as a
1304 // wildcard on either side so the original error stands alone.
1305 // Per-callsite guards like `!matches!(ty, Type::Invalid)`
1306 // around `self.compatible(...)` are now redundant but harmless;
1307 // sweeping them is deliberately out of scope here.
1308 if matches!(actual, Type::Invalid) || matches!(expected, Type::Invalid) {
1309 return true;
1310 }
1311 match expected {
1312 Type::Var(name) => Self::bind_expected_var(name, actual, subst),
1313 Type::Invalid => unreachable!("Type::Invalid handled by the early guard above"),
1314 Type::Int => matches!(actual, Type::Int),
1315 Type::Float => matches!(actual, Type::Float),
1316 Type::Str => matches!(actual, Type::Str),
1317 Type::Bool => matches!(actual, Type::Bool),
1318 Type::Unit => matches!(actual, Type::Unit),
1319 Type::Named {
1320 id: expected_id,
1321 name: expected_name,
1322 } => match actual {
1323 // Phase B: typed-identity comparison. When both sides
1324 // carry a `TypeId` (resolved against the symbol table)
1325 // we compare IDs directly — two unrelated modules
1326 // declaring `Shape` get distinct `TypeId`s by
1327 // construction and so stay incompatible. When the
1328 // checker is available we resolve either side's bare
1329 // string through `resolve_type_id` to bring it into
1330 // the typed identity domain; without the checker (or
1331 // for references that don't resolve, like builtin
1332 // `HttpResponse`) we fall back to canonical-name
1333 // equality.
1334 Type::Named {
1335 id: actual_id,
1336 name: actual_name,
1337 } => {
1338 // Peer review round 6: do NOT auto-resolve an
1339 // unresolved side in the matcher's
1340 // (importer-context) symbol table. Upstream
1341 // signature/binding boundaries
1342 // (`canonicalize_named`,
1343 // `canonicalize_named_in_module`) are
1344 // responsible for stamping `id` in the correct
1345 // owner context. If a `Type::Named` reaches the
1346 // matcher with `id = None`, that's a deliberate
1347 // unresolved state — either a genuine builtin
1348 // (HttpResponse) or a resolution gap the matcher
1349 // must surface, not silently paper over by
1350 // re-resolving in the wrong scope.
1351 let exp_id = *expected_id;
1352 let act_id = *actual_id;
1353 // Phase B (peer review round 2): the typed-identity
1354 // comparison must reject mixed (Some, None) cases
1355 // for user-defined types — otherwise an ambiguous
1356 // bare reference (`Shape` when both `A.Shape` and
1357 // `B.Shape` are exposed) silently matches against
1358 // any specific `A.Shape` / `B.Shape` via the
1359 // string fallback below. Distinguish "ambiguous
1360 // bare reference, identity deliberately
1361 // suppressed" from "builtin name that has no
1362 // typed identity by design (`HttpResponse`,
1363 // `Buffer`, …)" by asking the checker whether the
1364 // unresolved side's name is recorded as
1365 // ambiguous; reject in that case, allow name
1366 // fallback otherwise.
1367 match (exp_id, act_id) {
1368 (Some(e), Some(a)) => e == a,
1369 // Peer review round 6 entry-fallback bug:
1370 // a dep module's unresolved bare `Shape` was
1371 // silently binding to the entry module's
1372 // `Shape` via name equality here. Reject all
1373 // mixed (Some, None) cases. Builtins always
1374 // exercise (None, None) below; user-source
1375 // typed/raw mixes are by definition a
1376 // resolution gap and must surface.
1377 (Some(_), None) | (None, Some(_)) => false,
1378 (None, None) => {
1379 let exp = checker
1380 .map(|c| c.canonical_type_name(expected_name))
1381 .unwrap_or_else(|| expected_name.clone());
1382 let act = checker
1383 .map(|c| c.canonical_type_name(actual_name))
1384 .unwrap_or_else(|| actual_name.clone());
1385 exp == act
1386 }
1387 }
1388 }
1389 _ => false,
1390 },
1391 Type::Option(expected_inner) => match actual {
1392 Type::Option(actual_inner) => {
1393 Self::match_expected_type_inner(actual_inner, expected_inner, subst, checker)
1394 }
1395 _ => false,
1396 },
1397 Type::List(expected_inner) => match actual {
1398 Type::List(actual_inner) => {
1399 Self::match_expected_type_inner(actual_inner, expected_inner, subst, checker)
1400 }
1401 _ => false,
1402 },
1403 Type::Vector(expected_inner) => match actual {
1404 Type::Vector(actual_inner) => {
1405 Self::match_expected_type_inner(actual_inner, expected_inner, subst, checker)
1406 }
1407 _ => false,
1408 },
1409 Type::Result(expected_ok, expected_err) => match actual {
1410 Type::Result(actual_ok, actual_err) => {
1411 Self::match_expected_type_inner(actual_ok, expected_ok, subst, checker)
1412 && Self::match_expected_type_inner(actual_err, expected_err, subst, checker)
1413 }
1414 _ => false,
1415 },
1416 Type::Map(expected_k, expected_v) => match actual {
1417 Type::Map(actual_k, actual_v) => {
1418 Self::match_expected_type_inner(actual_k, expected_k, subst, checker)
1419 && Self::match_expected_type_inner(actual_v, expected_v, subst, checker)
1420 }
1421 _ => false,
1422 },
1423 Type::Tuple(expected_items) => match actual {
1424 Type::Tuple(actual_items) if actual_items.len() == expected_items.len() => {
1425 actual_items.iter().zip(expected_items.iter()).all(
1426 |(actual_item, expected_item)| {
1427 Self::match_expected_type_inner(
1428 actual_item,
1429 expected_item,
1430 subst,
1431 checker,
1432 )
1433 },
1434 )
1435 }
1436 _ => false,
1437 },
1438 Type::Fn(expected_params, expected_ret, expected_effects) => match actual {
1439 Type::Fn(actual_params, actual_ret, actual_effects)
1440 if actual_params.len() == expected_params.len() =>
1441 {
1442 actual_params.iter().zip(expected_params.iter()).all(
1443 |(actual_param, expected_param)| {
1444 Self::match_expected_type_inner(
1445 actual_param,
1446 expected_param,
1447 subst,
1448 checker,
1449 )
1450 },
1451 ) && Self::match_expected_type_inner(actual_ret, expected_ret, subst, checker)
1452 && actual_effects.iter().all(|actual| {
1453 expected_effects
1454 .iter()
1455 .any(|expected| crate::effects::effect_satisfies(expected, actual))
1456 })
1457 }
1458 _ => false,
1459 },
1460 }
1461 }
1462
1463 fn bind_expected_var(name: &str, actual: &Type, subst: &mut HashMap<String, Type>) -> bool {
1464 match actual {
1465 Type::Var(actual_name) => return actual_name == name,
1466 // Iron — A4: matches the wildcard in `match_expected_type_inner`.
1467 // An already-errored actual binds vacuously instead of
1468 // refusing the unification and triggering a cascade.
1469 Type::Invalid => return true,
1470 _ => {}
1471 }
1472 if let Some(bound) = subst.get(name).cloned() {
1473 return Self::match_expected_type(actual, &bound, subst)
1474 && Self::match_expected_type(&bound, actual, subst);
1475 // bind_expected_var is alias-agnostic — Var bindings
1476 // never compare Named types against `sig_aliases` since
1477 // the binding rule already accepts whatever concrete
1478 // type the caller hands in.
1479 }
1480 // Occurs check — refuse `T := F<…T…>` style circular bindings.
1481 // Without this, polymorphic recursion patterns like `fn nest(v:
1482 // A) -> Unit; nest([v])` would insert `A → List<A>` into `subst`
1483 // and rely on downstream structural mismatch to terminate
1484 // matching. The HashMap entry itself is still a cycle that
1485 // later `instantiate_type` walks would have to skip; rejecting
1486 // the bind at source keeps the substitution map well-formed
1487 // and surfaces the constraint failure to the caller as a
1488 // normal type-incompatibility error.
1489 if Self::type_contains_var(actual, name) {
1490 return false;
1491 }
1492 subst.insert(name.to_string(), actual.clone());
1493 true
1494 }
1495
1496 /// Structural recursion over `ty` looking for any `Type::Var(name)`.
1497 /// Used by the occurs check in [`bind_expected_var`]; not exposed
1498 /// elsewhere because it's a one-step deep walk over a finite Type
1499 /// AST (no shared subterms, no cycles in the AST itself — the cycle
1500 /// would only exist in the substitution map, which the bind path
1501 /// is what guards).
1502 fn type_contains_var(ty: &Type, name: &str) -> bool {
1503 match ty {
1504 Type::Var(other) => other == name,
1505 Type::Int
1506 | Type::Float
1507 | Type::Str
1508 | Type::Bool
1509 | Type::Unit
1510 | Type::Invalid
1511 | Type::Named { .. } => false,
1512 Type::Option(inner) | Type::List(inner) | Type::Vector(inner) => {
1513 Self::type_contains_var(inner, name)
1514 }
1515 Type::Result(ok, err) => {
1516 Self::type_contains_var(ok, name) || Self::type_contains_var(err, name)
1517 }
1518 Type::Map(k, v) => Self::type_contains_var(k, name) || Self::type_contains_var(v, name),
1519 Type::Tuple(items) => items.iter().any(|t| Self::type_contains_var(t, name)),
1520 Type::Fn(params, ret, _effects) => {
1521 params.iter().any(|p| Self::type_contains_var(p, name))
1522 || Self::type_contains_var(ret, name)
1523 }
1524 }
1525 }
1526
1527 pub(super) fn instantiate_type(ty: &Type, subst: &HashMap<String, Type>) -> Type {
1528 match ty {
1529 Type::Var(name) => subst.get(name).cloned().unwrap_or_else(|| ty.clone()),
1530 Type::Result(ok, err) => Type::Result(
1531 Box::new(Self::instantiate_type(ok, subst)),
1532 Box::new(Self::instantiate_type(err, subst)),
1533 ),
1534 Type::Option(inner) => Type::Option(Box::new(Self::instantiate_type(inner, subst))),
1535 Type::List(inner) => Type::List(Box::new(Self::instantiate_type(inner, subst))),
1536 Type::Vector(inner) => Type::Vector(Box::new(Self::instantiate_type(inner, subst))),
1537 Type::Map(k, v) => Type::Map(
1538 Box::new(Self::instantiate_type(k, subst)),
1539 Box::new(Self::instantiate_type(v, subst)),
1540 ),
1541 Type::Tuple(items) => Type::Tuple(
1542 items
1543 .iter()
1544 .map(|item| Self::instantiate_type(item, subst))
1545 .collect(),
1546 ),
1547 Type::Fn(params, ret, effects) => Type::Fn(
1548 params
1549 .iter()
1550 .map(|param| Self::instantiate_type(param, subst))
1551 .collect(),
1552 Box::new(Self::instantiate_type(ret, subst)),
1553 effects.clone(),
1554 ),
1555 Type::Int
1556 | Type::Float
1557 | Type::Str
1558 | Type::Bool
1559 | Type::Unit
1560 | Type::Invalid
1561 | Type::Named { .. } => ty.clone(),
1562 }
1563 }
1564}