Skip to main content

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