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            // 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}