Skip to main content

lex_types/checker/
mod.rs

1//! M3: type checker. Walks the canonical AST, infers types via unification,
2//! and checks declared signatures and effects.
3
4use crate::builtins::{module_for_import, module_scope};
5use crate::env::{TypeDefKind, TypeEnv, ty_from_canon_env};
6use crate::error::{PositionedError, TypeError};
7use crate::position::Position;
8use crate::types::*;
9use crate::unifier::{UnifyError, Unifier};
10use indexmap::IndexMap;
11use lex_ast as a;
12use std::collections::{BTreeMap, HashMap};
13
14mod exhaustive;
15mod parse_strict;
16
17pub use parse_strict::{rewrite_parse_calls, ParseSite};
18use parse_strict::*;
19
20/// Result of checking a whole program.
21pub struct ProgramTypes {
22    pub fn_signatures: IndexMap<String, Scheme>,
23    pub type_env: TypeEnv,
24    /// For #168: per-call required-fields map for `module.parse(s)`
25    /// calls whose inferred result type is `Result[Record{...}, _]`.
26    /// Keyed by the call's [`ParseSite`] (stage index + NodeId), so
27    /// the table stays valid for any structurally identical copy of
28    /// the checked stages (#777). Empty unless any matching call
29    /// sites were found.
30    ///
31    /// See [`check_and_rewrite_program`] for the function that
32    /// populates this and applies the rewrite in one step, and
33    /// [`rewrite_parse_calls`] to apply it to a separate copy.
34    pub parse_required_fields: HashMap<ParseSite, Vec<String>>,
35    /// For #322: per-call type schema alongside the field names.
36    /// Each entry is a `Vec<(field_name, type_tag)>` parallel to
37    /// `parse_required_fields`. Used by the rewrite pass to inject
38    /// the third argument to `parse_strict`.
39    pub parse_type_schemas: HashMap<ParseSite, Vec<(String, String)>>,
40}
41
42/// Variant of [`check_program`] that stamps a source [`Position`]
43/// onto every emitted error (#306 slice 1).
44///
45/// `positions` is keyed by function name and supplies the position
46/// of each `fn` declaration in the source. Errors from a given
47/// function are tagged with that function's position; errors that
48/// don't map to a single function (e.g. type-decl-level errors)
49/// keep `position = None`.
50///
51/// Slice 1 ships function-level granularity. Slice 1.5 will plumb
52/// per-expression spans through canonicalize so deep-body errors
53/// land on the offending sub-expression rather than its enclosing
54/// function.
55pub fn check_program_with_positions(
56    stages: &[a::Stage],
57    positions: &BTreeMap<String, Position>,
58) -> Result<ProgramTypes, Vec<PositionedError>> {
59    check_program_inner(stages, Some(positions), &BTreeMap::new())
60        .map_err(|errs| errs.into_iter().map(|(e, fn_name)| {
61            let pos = fn_name.as_deref().and_then(|n| positions.get(n)).cloned();
62            PositionedError::new(e, pos)
63        }).collect())
64}
65
66pub fn check_program(stages: &[a::Stage]) -> Result<ProgramTypes, Vec<TypeError>> {
67    check_program_inner(stages, None, &BTreeMap::new())
68        .map_err(|errs| errs.into_iter().map(|(e, _)| e).collect())
69}
70
71/// Like [`check_program`], but with a set of already-resolved dependency
72/// modules the head may import by *reference* (e.g. `"lex-nt/lib"`). Each
73/// value is that module's type — a [`Ty::Record`] of its exported
74/// functions, the same shape [`crate::builtins::module_scope`] produces for
75/// stdlib (build one with [`module_record_from_fields`]). Registry/git
76/// dependencies resolve through this map instead of being inlined into
77/// `stages` (#930): the op-log keeps the `import` edge and the write-time
78/// gate supplies the dependency's signatures here, so the head still
79/// type-checks against them without carrying their bodies.
80///
81/// An empty map reproduces [`check_program`] exactly — only stdlib imports
82/// resolve, and any `<alias>.name` reaching an unsupplied dependency is an
83/// unbound-reference error, as today.
84pub fn check_program_with_modules(
85    stages: &[a::Stage],
86    modules: &BTreeMap<String, Ty>,
87) -> Result<ProgramTypes, Vec<TypeError>> {
88    check_program_inner(stages, None, modules)
89        .map_err(|errs| errs.into_iter().map(|(e, _)| e).collect())
90}
91
92/// Build a dependency module's value type — a record of its exported
93/// functions — from `(name, type)` pairs, for [`check_program_with_modules`]
94/// (#930). Callers never touch the record representation directly.
95///
96/// The record is bound under an import alias and generalized *as a whole*
97/// (Pass 1: `collect_vars`/`collect_eff_vars` over the record, then
98/// `instantiate` per reference). Each export, however, was generalized
99/// independently and so numbers its own variables from zero — two exports
100/// of one dependency both spelling `Var(0)` would be tied together by that
101/// whole-record generalization. So every export is renumbered into a
102/// disjoint block: type variables from `0` up, effect-row variables from
103/// [`EFF_VAR_BASE`] up (the same type/effect split stdlib's
104/// [`crate::stdlib_spec::module_record`] keeps). Monomorphic exports (the
105/// common case, e.g. `gcd(Int, Int) -> Int`) carry no variables and pass
106/// through unchanged.
107pub fn module_record_from_fields(fields: impl IntoIterator<Item = (String, Ty)>) -> Ty {
108    let mut next_ty: u32 = 0;
109    let mut next_eff: u32 = crate::stdlib_spec::EFF_VAR_BASE;
110    let renumbered: IndexMap<String, Ty> = fields
111        .into_iter()
112        .map(|(name, ty)| (name, renumber_field_vars(&ty, &mut next_ty, &mut next_eff)))
113        .collect();
114    Ty::Record(renumbered)
115}
116
117/// Rewrite every type variable and effect-row variable in `ty` to a fresh
118/// id drawn from the running counters, consistently within `ty`: a variable
119/// used more than once stays one variable, but distinct variables get
120/// distinct fresh ids, and no id is reused across separate calls (the
121/// counters advance). See [`module_record_from_fields`].
122fn renumber_field_vars(ty: &Ty, next_ty: &mut u32, next_eff: &mut u32) -> Ty {
123    fn walk(
124        t: &mut Ty,
125        ty_map: &mut HashMap<u32, u32>,
126        eff_map: &mut HashMap<u32, u32>,
127        next_ty: &mut u32,
128        next_eff: &mut u32,
129    ) {
130        match t {
131            Ty::Var(v) => {
132                let nv = *ty_map.entry(*v).or_insert_with(|| {
133                    let x = *next_ty;
134                    *next_ty += 1;
135                    x
136                });
137                *v = nv;
138            }
139            Ty::Prim(_) | Ty::Unit | Ty::Never => {}
140            Ty::List(inner) => walk(inner, ty_map, eff_map, next_ty, next_eff),
141            Ty::Tuple(items) => {
142                for it in items {
143                    walk(it, ty_map, eff_map, next_ty, next_eff);
144                }
145            }
146            Ty::Record(fs) => {
147                for v in fs.values_mut() {
148                    walk(v, ty_map, eff_map, next_ty, next_eff);
149                }
150            }
151            Ty::Con(_, args) => {
152                for a in args {
153                    walk(a, ty_map, eff_map, next_ty, next_eff);
154                }
155            }
156            Ty::Function { params, effects, ret } => {
157                for p in params {
158                    walk(p, ty_map, eff_map, next_ty, next_eff);
159                }
160                if let Some(v) = effects.var {
161                    let nv = *eff_map.entry(v).or_insert_with(|| {
162                        let x = *next_eff;
163                        *next_eff += 1;
164                        x
165                    });
166                    effects.var = Some(nv);
167                }
168                walk(ret, ty_map, eff_map, next_ty, next_eff);
169            }
170        }
171    }
172    let mut out = ty.clone();
173    let mut ty_map: HashMap<u32, u32> = HashMap::new();
174    let mut eff_map: HashMap<u32, u32> = HashMap::new();
175    walk(&mut out, &mut ty_map, &mut eff_map, next_ty, next_eff);
176    out
177}
178
179fn check_program_inner(
180    stages: &[a::Stage],
181    _positions: Option<&BTreeMap<String, Position>>,
182    modules: &BTreeMap<String, Ty>,
183) -> Result<ProgramTypes, Vec<(TypeError, Option<String>)>> {
184    let mut tcx = Checker::new();
185    // Each entry is (error, optional fn name the error came from)
186    // so callers can resolve the error to a source position.
187    let mut errors: Vec<(TypeError, Option<String>)> = Vec::new();
188
189    // Pass 1: gather imports → bring module values into scope.
190    for stage in stages {
191        if let a::Stage::Import(i) = stage {
192            // Stdlib modules resolve to a built-in scope.
193            if let Some(mod_name) = module_for_import(&i.reference) {
194                if let Some(ty) = module_scope(mod_name, &tcx.type_env) {
195                    tcx.globals.insert(i.alias.clone(), Scheme {
196                        // Module-level signatures use Var(0..n) and
197                        // effect-vars on stdlib HOFs (list.map's `[E]`
198                        // etc.); generalize both.
199                        vars: collect_vars(&ty),
200                        eff_vars: collect_eff_vars(&ty),
201                        ty,
202                    });
203                    tcx.module_aliases.insert(i.alias.clone(), mod_name.to_string());
204                    continue;
205                }
206            }
207            // #930: a resolved registry/git dependency, supplied by the
208            // caller (the write-time gate) keyed by import reference,
209            // rather than inlined into `stages`. Bind its record under this
210            // file's alias so `<alias>.name` references type-check with the
211            // dependency's signatures but without its bodies present. The
212            // record is already generalized per export, so generalize it as
213            // a whole the same way a stdlib module scope is bound above.
214            if let Some(ty) = modules.get(&i.reference) {
215                tcx.globals.insert(i.alias.clone(), Scheme {
216                    vars: collect_vars(ty),
217                    eff_vars: collect_eff_vars(ty),
218                    ty: ty.clone(),
219                });
220            }
221        }
222    }
223
224    // Pass 2: register user-declared types.
225    for stage in stages {
226        if let a::Stage::TypeDecl(td) = stage {
227            if let Err(e) = tcx.type_env.add_user_type(&td.name, td.clone()) {
228                errors.push((TypeError::RecursiveTypeWithoutConstructor {
229                    at_node: "n_0".into(),
230                    name: e,
231                }, None));
232            }
233        }
234    }
235
236    // Pass 3: register fn signatures (so mutual recursion works).
237    for stage in stages {
238        if let a::Stage::FnDecl(fd) = stage {
239            let scheme = function_scheme(fd, &tcx.type_env);
240            tcx.globals.insert(fd.name.clone(), scheme);
241            // #209 slice 2: keep the original params so call-site
242            // refinement discharge can see the predicate before it
243            // gets stripped to its base type by `ty_from_canon`.
244            tcx.fn_params.insert(fd.name.clone(), fd.params.clone());
245        }
246    }
247
248    // Pass 4: check each fn body. With #306 slice 1, every emitted
249    // error is paired with the source fn it came from so the public
250    // [`check_program_with_positions`] wrapper can stamp the
251    // function's source position onto a [`PositionedError`].
252    let mut signatures = IndexMap::new();
253    // #777: the parse-call side tables are keyed by (stage index,
254    // NodeId) rather than by expression address, so each FnDecl's
255    // NodeId map is computed up front. The walk is skipped entirely
256    // when no import could produce a rewritable call.
257    let wants_parse_sites = tcx.has_parse_capable_imports();
258    for (stage_idx, stage) in stages.iter().enumerate() {
259        if let a::Stage::FnDecl(fd) = stage {
260            tcx.stage_ids = if wants_parse_sites {
261                Some((stage_idx, a::expr_ids(stage)))
262            } else {
263                None
264            };
265            match tcx.check_fn(fd) {
266                Ok(scheme) => { signatures.insert(fd.name.clone(), scheme); }
267                Err(es) => {
268                    errors.extend(es.into_iter().map(|e| (e, Some(fd.name.clone()))));
269                }
270            }
271        }
272    }
273    tcx.stage_ids = None;
274
275    if errors.is_empty() {
276        // #168: walk pending parse-call records and resolve each
277        // call's return type now that all unification has settled.
278        // A call shows up here only if the call site syntactically
279        // looks like `<alias>.parse(s)` for an alias bound to one
280        // of {json, toml, yaml} via the import pass.
281        let mut parse_required_fields = HashMap::new();
282        let mut parse_type_schemas = HashMap::new();
283        for (site, ret_ty) in &tcx.pending_parse_calls {
284            if let Some((fields, schema)) = extract_record_fields_and_schema(&tcx.u, &tcx.type_env, ret_ty) {
285                parse_required_fields.insert(site.clone(), fields);
286                parse_type_schemas.insert(site.clone(), schema);
287            }
288        }
289        Ok(ProgramTypes {
290            fn_signatures: signatures,
291            type_env: tcx.type_env,
292            parse_required_fields,
293            parse_type_schemas,
294        })
295    } else {
296        Err(errors)
297    }
298}
299
300/// Type-check `stages` and rewrite every `module.parse(s)` call
301/// where the inferred T is a Record into the equivalent
302/// `module.parse_strict(s, [field_names])` (#168). Existing
303/// [`check_program`] keeps the old immutable signature for tests
304/// and tools that don't want the AST rewritten.
305pub fn check_and_rewrite_program(
306    stages: &mut [a::Stage],
307) -> Result<ProgramTypes, Vec<TypeError>> {
308    check_and_rewrite_program_with_modules(stages, &BTreeMap::new())
309}
310
311/// Like [`check_and_rewrite_program`], but resolving external dependency
312/// references through `modules` (#930) — the publish path checks the same
313/// non-inlined head its store gate will, so the two agree.
314pub fn check_and_rewrite_program_with_modules(
315    stages: &mut [a::Stage],
316    modules: &BTreeMap<String, Ty>,
317) -> Result<ProgramTypes, Vec<TypeError>> {
318    let pt = check_program_with_modules(&*stages, modules)?;
319    rewrite_parse_calls(stages, &pt);
320    Ok(pt)
321}
322
323/// `parse` → `parse_strict_typed` / `json_body` → `json_body_typed`
324/// (#168, `parse_strict.rs`) are synthesized onto an AST that has
325/// already been checked once — `rewrite_parse_calls` mutates a
326/// call's callee field name and appends two arguments, producing a
327/// call that the module's literal Lex-level record type has no
328/// field for (the `_typed` variants are native ops dispatched by
329/// name in `lex-runtime`'s `builtins.rs`, not declared Lex members).
330/// Re-checking those already-rewritten stages — as lex-store's
331/// write-time publish gate does on the same stages its caller just
332/// ran `check_and_rewrite_program` over — must accept the field
333/// rather than report it unknown. The synthesized signature is
334/// derived from the original field's rather than hardcoded, so it
335/// stays in sync with any future change to the base op's shape:
336/// same params (plus the two extra `List` arguments the rewrite
337/// always appends) and the same effects and return type.
338fn synthesize_decode_typed_field(field: &str, fields: &IndexMap<String, Ty>) -> Option<Ty> {
339    let base_field = match field {
340        "parse_strict_typed" => "parse",
341        "json_body_typed" => "json_body",
342        _ => return None,
343    };
344    let Ty::Function { params, effects, ret } = fields.get(base_field)? else {
345        return None;
346    };
347    let mut synthesized_params = params.clone();
348    synthesized_params.push(Ty::List(Box::new(Ty::Prim(Prim::Str))));
349    synthesized_params.push(Ty::List(Box::new(Ty::Tuple(vec![
350        Ty::Prim(Prim::Str),
351        Ty::Prim(Prim::Str),
352    ]))));
353    Some(Ty::Function {
354        params: synthesized_params,
355        effects: effects.clone(),
356        ret: ret.clone(),
357    })
358}
359
360fn collect_vars(t: &Ty) -> Vec<TyVarId> {
361    let mut out = Vec::new();
362    fn walk(t: &Ty, out: &mut Vec<TyVarId>) {
363        match t {
364            Ty::Var(v) => { if !out.contains(v) { out.push(*v); } }
365            Ty::Prim(_) | Ty::Unit | Ty::Never => {}
366            Ty::List(inner) => walk(inner, out),
367            Ty::Tuple(items) => for it in items { walk(it, out); },
368            Ty::Record(fs) => for v in fs.values() { walk(v, out); },
369            Ty::Con(_, args) => for a in args { walk(a, out); },
370            Ty::Function { params, ret, .. } => {
371                for p in params { walk(p, out); }
372                walk(ret, out);
373            }
374        }
375    }
376    walk(t, &mut out);
377    out
378}
379
380/// Walk a type and collect every effect-row variable id that appears
381/// inside any function-type's effect set. Used to generalize stdlib
382/// HOF schemes alongside ordinary type vars.
383fn collect_eff_vars(t: &Ty) -> Vec<u32> {
384    let mut out = Vec::new();
385    fn walk(t: &Ty, out: &mut Vec<u32>) {
386        match t {
387            Ty::Var(_) | Ty::Prim(_) | Ty::Unit | Ty::Never => {}
388            Ty::List(inner) => walk(inner, out),
389            Ty::Tuple(items) => for it in items { walk(it, out); },
390            Ty::Record(fs) => for v in fs.values() { walk(v, out); },
391            Ty::Con(_, args) => for a in args { walk(a, out); },
392            Ty::Function { params, effects, ret } => {
393                if let Some(v) = effects.var {
394                    if !out.contains(&v) { out.push(v); }
395                }
396                for p in params { walk(p, out); }
397                walk(ret, out);
398            }
399        }
400    }
401    walk(t, &mut out);
402    out
403}
404
405fn function_scheme(fd: &a::FnDecl, env: &TypeEnv) -> Scheme {
406    // Collect type-param ids in order; map their names to fresh Var(idx).
407    let params: Vec<Ty> = fd.params.iter().map(|p| ty_from_canon_env(&p.ty, &fd.type_params, env)).collect();
408    let ret = ty_from_canon_env(&fd.return_type, &fd.type_params, env);
409    // Plumb effect args (#207). A canonical-AST `EffectDecl` already
410    // carries `Option<EffectArg>`; map it into the type-system kind so
411    // subsumption can honor parameterized effects.
412    let effects = EffectSet {
413        concrete: {
414            let mut s = std::collections::BTreeSet::new();
415            for e in &fd.effects {
416                let arg = e.arg.as_ref().map(|a| match a {
417                    a::EffectArg::Str { value } => crate::types::EffectArg::Str(value.clone()),
418                    a::EffectArg::Int { value } => crate::types::EffectArg::Int(*value),
419                    a::EffectArg::Ident { value } => crate::types::EffectArg::Ident(value.clone()),
420                });
421                s.insert(crate::types::EffectKind { name: e.name.clone(), arg });
422            }
423            s
424        },
425        // Open-row tail on the function's own declared row: `-> [io | E] T`.
426        // Resolve `E` to its `type_params` index (shared id space with the
427        // type-var numbering; read back via the effect-subst map, so no
428        // collision with a same-indexed type param).
429        var: fd.effect_row_var
430            .as_ref()
431            .and_then(|n| fd.type_params.iter().position(|p| p == n))
432            .map(|i| i as u32),
433    };
434    let ty = Ty::Function { params, effects, ret: Box::new(ret) };
435    let vars: Vec<TyVarId> = (0..fd.type_params.len() as u32).collect();
436    // Generalize over any effect-row variable in the signature — a
437    // row-polymorphic parameter (`(Int) -> [io | E] Int`, like the stdlib
438    // HOFs) or the function's own open row (`-> [io | E] T`). Each is
439    // freshened per call site by `instantiate`, then bound to the caller's
440    // actual effects by `unify_effects`. Closed rows collect nothing, so
441    // their checking is unchanged.
442    let eff_vars = collect_eff_vars(&ty);
443    Scheme { vars, eff_vars, ty }
444}
445
446struct Checker {
447    u: Unifier,
448    type_env: TypeEnv,
449    globals: IndexMap<String, Scheme>,
450    /// Imported alias → canonical module name (e.g. `cfg` → `toml`).
451    /// Populated during the import pass; consulted by `check_call`
452    /// to recognise `cfg.parse(...)` as a stdlib parse call.
453    module_aliases: IndexMap<String, String>,
454    /// For #168: every `<alias>.parse(s)` call where alias is in
455    /// `module_aliases` and maps to {json, toml, yaml} (or
456    /// `http.json_body`, #684), recorded here as
457    /// `(call_site, return_type_var)`. After the whole program
458    /// type-checks, we walk this and resolve each return type
459    /// through the unifier — at that point any `Result[Manifest, _]`
460    /// constraints from match patterns or let-annotations have
461    /// settled.
462    pending_parse_calls: Vec<(ParseSite, Ty)>,
463    /// #777: NodeId map for the FnDecl stage currently being checked,
464    /// `(stage index, &CExpr address → NodeId)`. Set by
465    /// `check_program_inner` before each `check_fn` when the program
466    /// imports a decode-capable module, `None` otherwise. Used only to
467    /// translate a parse call's address into a stable [`ParseSite`].
468    stage_ids: Option<(usize, HashMap<*const a::CExpr, a::NodeId>)>,
469    /// Per-function param list, retained so call-site discharge can
470    /// see refinement predicates (#209 slice 2). The main `globals`
471    /// scheme strips refinements (`Refined` unifies as its base);
472    /// this side-table keeps the pre-stripped `TypeExpr` available
473    /// for static discharge of literal arguments.
474    fn_params: IndexMap<String, Vec<a::Param>>,
475    /// Errors recovered from independent sub-expressions within a
476    /// function body (discarded `Block` statements, `Let` binding
477    /// values) so a single `lex check` run surfaces every independent
478    /// error instead of stopping at the first (#566). Drained by
479    /// `check_fn` after each body/example check.
480    recovered_errors: Vec<TypeError>,
481    /// Effect-row variables in scope for the function currently being
482    /// checked: surface name (e.g. `E`) → the instantiated fresh effect-var
483    /// id allocated for it. Lets a row-polymorphic *lambda* inside the body
484    /// (`fn (r) -> [io | E] R { ... }`) resolve its tail `E` to the same id
485    /// as the enclosing function's signature, so effects flow through the
486    /// closure (e.g. into `net.serve_fn`) instead of being silently dropped.
487    /// Empty for closed-row functions.
488    eff_row_scope: IndexMap<String, u32>,
489}
490
491impl Checker {
492    fn new() -> Self {
493        Self {
494            u: Unifier::new(),
495            type_env: TypeEnv::new_with_builtins(),
496            globals: IndexMap::new(),
497            module_aliases: IndexMap::new(),
498            pending_parse_calls: Vec::new(),
499            stage_ids: None,
500            fn_params: IndexMap::new(),
501            recovered_errors: Vec::new(),
502            eff_row_scope: IndexMap::new(),
503        }
504    }
505
506    /// Check an independent sub-expression but, on error, record it and
507    /// continue with a fresh type variable rather than aborting the whole
508    /// body. Used for positions whose result type does not flow into a
509    /// strict constraint — a discarded `Block` statement, or a `Let`
510    /// binding's value — so `check_fn` can surface every independent error
511    /// in one pass (#566). A fresh var unifies with anything, so recovery
512    /// does not manufacture spurious follow-on mismatches.
513    fn check_expr_recover(
514        &mut self,
515        e: &a::CExpr,
516        node_id: &str,
517        locals: &mut IndexMap<String, Ty>,
518        effs: &mut EffectSet,
519    ) -> Ty {
520        match self.check_expr(e, node_id, locals, effs) {
521            Ok(ty) => ty,
522            Err(err) => {
523                self.recovered_errors.push(err);
524                self.u.fresh()
525            }
526        }
527    }
528
529    /// If `ty` is a `Ty::Con(name, args)` whose definition is a type
530    /// alias (record or otherwise), return the aliased type with the
531    /// alias's formal parameters substituted by `args`. For zero-arg
532    /// aliases this is the identity substitution. For parametric
533    /// aliases (#439, e.g. `type Box[T] = { value :: T }`), the
534    /// formal `Ty::Var(i)` for the i-th param is replaced by `args[i]`
535    /// so `Box[Str]` unfolds to `{ value :: Str }` rather than to the
536    /// unsubstituted body. Returns `ty` unchanged when arity doesn't
537    /// match or the name doesn't resolve to an alias.
538    fn unfold_record_alias(&self, ty: Ty) -> Ty {
539        if let Ty::Con(ref n, ref args) = ty {
540            if let Some(td) = self.type_env.types.get(n) {
541                if let TypeDefKind::Alias(inner) = &td.kind {
542                    if td.params.len() != args.len() {
543                        return ty;
544                    }
545                    if td.params.is_empty() {
546                        return inner.clone();
547                    }
548                    let mut subst = IndexMap::new();
549                    for (i, a) in args.iter().enumerate() {
550                        subst.insert(i as u32, a.clone());
551                    }
552                    return subst_vars(inner, &subst, &IndexMap::new());
553                }
554            }
555        }
556        ty
557    }
558
559    /// True iff `ty` is a `Ty::Con(name, args)` whose definition is a
560    /// `TypeDefKind::Alias` and whose arity matches. Used by
561    /// `unify_coerce_inner` to detect the case where both sides are
562    /// nominal aliases and unfolding would collapse the nominal
563    /// distinction (#323 / #439). For parametric aliases the arity
564    /// match guards against `Box[Str]` vs an inconsistent `Box[Str, Int]`.
565    fn is_alias_con(&self, ty: &Ty) -> bool {
566        if let Ty::Con(name, args) = ty {
567            if let Some(td) = self.type_env.types.get(name) {
568                if matches!(td.kind, TypeDefKind::Alias(_))
569                    && td.params.len() == args.len()
570                {
571                    return true;
572                }
573            }
574        }
575        false
576    }
577
578    /// Unify two types, asymmetrically coercing an anonymous record
579    /// against a nominal record alias at any level of nesting. So a
580    /// `{ x: 1, y: 2 }` literal can be passed to a fn taking
581    /// `Inner = { x :: Int, y :: Int }`, even when the literal is the
582    /// inner field of an outer record literal.
583    ///
584    /// We deliberately keep nominal-vs-nominal mismatches strict: two
585    /// distinct `Ty::Con` names won't unify just because their record
586    /// shapes match. The coercion fires only when one side is a bare
587    /// `Ty::Record` and the other is a `Ty::Con` whose alias is a
588    /// record.
589    fn unify_with_record_coercion(&mut self, a: &Ty, b: &Ty) -> Result<(), UnifyError> {
590        let a = self.u.resolve(a);
591        let b = self.u.resolve(b);
592        self.unify_coerce_inner(a, b)
593    }
594
595    fn unify_coerce_inner(&mut self, a: Ty, b: Ty) -> Result<(), UnifyError> {
596        // #323: alias unfolding. If exactly one side is an `alias-Con`
597        // — a 0-arg `Ty::Con(name, [])` whose definition is a type
598        // alias (Record or non-record) — unfold both sides so the
599        // structural cases below can match (`Errors` ↔ `List[…]`,
600        // `Path` ↔ `Tuple(…)`, `Maybe` ↔ `Option[…]`,
601        // `UserId` ↔ `Int`, …).
602        //
603        // Three cases intentionally bypass unfolding:
604        //
605        // - **Same-named Cons** (`Test` vs `Test`): preserve nominal
606        //   identity. The Con-Con same-name case below recurses on
607        //   args; eager unfold here would force the nominal name
608        //   to evaporate, breaking unifications elsewhere that
609        //   still see the nominal `Con`.
610        // - **Var on either side**: don't unfold against an unbound
611        //   variable, because the plain unifier would bind the var
612        //   to the unfolded shape and lose the nominal name. The
613        //   var binds to the nominal `Con` instead, and later
614        //   unifications against concrete shapes re-enter this
615        //   function and unfold then.
616        // - **Two distinct alias-Cons** (`Apple` vs `Box`, both
617        //   declared as record aliases with identical shapes):
618        //   preserve nominal distinction between aliases. Unfolding
619        //   both would collapse the test of "same shape, different
620        //   names" into "same shape" and erase the names.
621        let (a, b) = match (&a, &b) {
622            (Ty::Con(n1, _), Ty::Con(n2, _)) if n1 == n2 => (a, b),
623            (Ty::Var(_), _) | (_, Ty::Var(_)) => (a, b),
624            (Ty::Con(_, _), Ty::Con(_, _))
625                if self.is_alias_con(&a) && self.is_alias_con(&b) =>
626            {
627                (a, b)
628            }
629            _ => {
630                let a_u = if let Ty::Con(_, _) = &a {
631                    self.unfold_record_alias(a.clone())
632                } else {
633                    a
634                };
635                let b_u = if let Ty::Con(_, _) = &b {
636                    self.unfold_record_alias(b.clone())
637                } else {
638                    b
639                };
640                (a_u, b_u)
641            }
642        };
643
644        match (&a, &b) {
645            (Ty::Record(fa), Ty::Record(fb)) => {
646                if fa.len() != fb.len() {
647                    return Err(UnifyError::Mismatch { a: a.clone(), b: b.clone() });
648                }
649                for (k, va) in fa.clone() {
650                    match fb.get(&k) {
651                        Some(vb) => self.unify_coerce_inner(va, vb.clone())?,
652                        None => return Err(UnifyError::Mismatch { a: a.clone(), b: b.clone() }),
653                    }
654                }
655                Ok(())
656            }
657            (Ty::List(ta), Ty::List(tb)) => {
658                self.unify_coerce_inner((**ta).clone(), (**tb).clone())
659            }
660            (Ty::Tuple(xs), Ty::Tuple(ys)) if xs.len() == ys.len() => {
661                for (x, y) in xs.clone().into_iter().zip(ys.clone()) {
662                    self.unify_coerce_inner(x, y)?;
663                }
664                Ok(())
665            }
666            // Recurse into Con-Con pairs so record-alias coercion reaches
667            // arbitrary nesting depth (e.g. Result[T, MyAlias]) (#328).
668            (Ty::Con(n1, a1), Ty::Con(n2, a2)) if n1 == n2 && a1.len() == a2.len() => {
669                for (x, y) in a1.clone().into_iter().zip(a2.clone()) {
670                    self.unify_coerce_inner(x, y)?;
671                }
672                Ok(())
673            }
674            // #345: recurse into Function types so alias coercion fires on
675            // closure params / return types. Without this, a closure annotated
676            // `(Errors, Errors) -> Errors` fails to unify with the expected
677            // `(List[?n], ?m) -> List[?n]` even though `Errors = List[Error]`.
678            (Ty::Function { params: pa, effects: ea, ret: ra },
679             Ty::Function { params: pb, effects: eb, ret: rb })
680            if pa.len() == pb.len() => {
681                for (x, y) in pa.clone().into_iter().zip(pb.clone()) {
682                    self.unify_coerce_inner(x, y)?;
683                }
684                // Propagate the EffectMismatch verbatim (rather than
685                // collapsing it into a whole-type Mismatch) so the
686                // invariant-effect-row case surfaces as its own
687                // rule_tag with the narrow-the-body fix (#565).
688                self.u.unify_effects(ea, eb)?;
689                self.unify_coerce_inner((**ra).clone(), (**rb).clone())
690            }
691            _ => self.u.unify(&a, &b),
692        }
693    }
694
695    fn check_fn(&mut self, fd: &a::FnDecl) -> Result<Scheme, Vec<TypeError>> {
696        // Instantiate fn's signature with fresh vars for its type params.
697        let scheme = function_scheme(fd, &self.type_env);
698        let (inst_ty, eff_subst) = instantiate_with_eff(&scheme, &mut self.u);
699        let (param_tys, declared_effects, ret_ty) = match inst_ty {
700            Ty::Function { params, effects, ret } => (params, effects, *ret),
701            _ => unreachable!(),
702        };
703
704        // Map this function's surface row-variable names to their freshly
705        // instantiated effect-var ids, so a row-polymorphic lambda in the
706        // body can join the enclosing row (see `eff_row_scope`). A type
707        // param at index `i` is a row var iff `i` was generalized as an
708        // effect var (`scheme.eff_vars`) and thus appears in `eff_subst`.
709        let saved_scope = std::mem::take(&mut self.eff_row_scope);
710        for (i, name) in fd.type_params.iter().enumerate() {
711            if let Some(fresh) = eff_subst.get(&(i as u32)) {
712                self.eff_row_scope.insert(name.clone(), *fresh);
713            }
714        }
715
716        let mut locals: IndexMap<String, Ty> = IndexMap::new();
717        for (p, t) in fd.params.iter().zip(param_tys.iter()) {
718            locals.insert(p.name.clone(), t.clone());
719        }
720
721        // Accumulate all errors within this function rather than returning on the
722        // first one (#566). Body errors and example errors are independent — an
723        // agent can fix both in one pass instead of running lex check repeatedly.
724        let mut errors: Vec<TypeError> = Vec::new();
725        let mut inferred_effects = EffectSet::empty();
726
727        // Check body. Save the error but continue to example checking.
728        let body_ok = match self.check_expr(&fd.body, "n_0", &mut locals, &mut inferred_effects) {
729            Ok(body_ty) => {
730                // The body may produce an anonymous record literal where the
731                // signature expects a nominal record alias (and vice-versa,
732                // and at any nested level). `unify_with_record_coercion`
733                // handles that asymmetry while keeping nominal-vs-nominal
734                // mismatches strict.
735                if let Err(e) = self.unify_with_record_coercion(&body_ty, &ret_ty) {
736                    errors.push(mismatch_err("n_0", e, &self.u, vec![format!("in function `{}`", fd.name)]));
737                    false
738                } else {
739                    true
740                }
741            }
742            Err(e) => { errors.push(e); false }
743        };
744
745        // Surface errors recovered from independent positions in the body
746        // (discarded `Block` statements, `Let` values) so every independent
747        // error is reported in one pass (#566), not just the first.
748        let body_had_recovered = !self.recovered_errors.is_empty();
749        errors.append(&mut self.recovered_errors);
750
751        // Skip the effect-not-declared check when the body had recovered
752        // errors: effect inference is incomplete (recovered sub-exprs became
753        // fresh vars that contribute no effects), so a missing/extra effect
754        // would be misleading noise next to the real errors.
755        if body_ok && !body_had_recovered && !inferred_effects.is_subset(&declared_effects) {
756            for e in inferred_effects.concrete.iter() {
757                if !declared_effects.concrete.iter().any(|d| d.subsumes(e)) {
758                    errors.push(TypeError::EffectNotDeclared {
759                        at_node: "n_0".into(),
760                        effect: e.pretty(),
761                    });
762                    break;
763                }
764            }
765        }
766
767        // #369: signature-level examples. Pure-only in v1; arg arity
768        // must match params; each arg type-checks against its param,
769        // each expected type-checks against the return type.
770        // Check all examples regardless of body success (#566).
771        if !fd.examples.is_empty() {
772            if !declared_effects.concrete.is_empty() {
773                errors.push(TypeError::ExamplesOnEffectfulFn {
774                    at_node: "n_0".into(),
775                    fn_name: fd.name.clone(),
776                });
777            } else {
778                for (case_index, ex) in fd.examples.iter().enumerate() {
779                    if ex.args.len() != param_tys.len() {
780                        errors.push(TypeError::ExampleArityMismatch {
781                            at_node: "n_0".into(),
782                            fn_name: fd.name.clone(),
783                            case_index,
784                            expected: param_tys.len(),
785                            got: ex.args.len(),
786                        });
787                        continue;
788                    }
789                    let mut example_locals: IndexMap<String, Ty> = IndexMap::new();
790                    let mut example_effects = EffectSet::empty();
791                    let mut args_ok = true;
792                    for (i, (arg, expected_ty)) in
793                        ex.args.iter().zip(param_tys.iter()).enumerate()
794                    {
795                        match self.check_expr(arg, "n_0", &mut example_locals, &mut example_effects) {
796                            Ok(arg_ty) => {
797                                if let Err(e) = self.unify_with_record_coercion(&arg_ty, expected_ty) {
798                                    errors.push(mismatch_err(
799                                        "n_0", e, &self.u,
800                                        vec![format!("in example #{} for `{}`, argument {}", case_index + 1, fd.name, i + 1)],
801                                    ));
802                                    args_ok = false;
803                                }
804                            }
805                            Err(e) => { errors.push(e); args_ok = false; }
806                        }
807                    }
808                    if args_ok {
809                        match self.check_expr(&ex.expected, "n_0", &mut example_locals, &mut example_effects) {
810                            Ok(expected_ty) => {
811                                if let Err(e) = self.unify_with_record_coercion(&expected_ty, &ret_ty) {
812                                    errors.push(mismatch_err(
813                                        "n_0", e, &self.u,
814                                        vec![format!("in example #{} for `{}`, expected value", case_index + 1, fd.name)],
815                                    ));
816                                }
817                            }
818                            Err(e) => errors.push(e),
819                        }
820                    }
821                    // The example's args/expected are expected to be pure
822                    // by construction (literals in the common case); if
823                    // they invoked effects, they'd break the pure-only
824                    // discipline. Reject the first one via the same effect rule.
825                    if let Some(e) = example_effects.concrete.iter().next() {
826                        errors.push(TypeError::EffectNotDeclared {
827                            at_node: "n_0".into(),
828                            effect: e.pretty(),
829                        });
830                    }
831                }
832            }
833        }
834
835        // Catch any errors recovered while checking example sub-expressions.
836        errors.append(&mut self.recovered_errors);
837        // Restore the enclosing function's row-var scope (functions are
838        // checked one at a time, so this is just defensive symmetry).
839        self.eff_row_scope = saved_scope;
840        if errors.is_empty() { Ok(scheme) } else { Err(errors) }
841    }
842
843    fn check_expr(
844        &mut self,
845        e: &a::CExpr,
846        node_id: &str,
847        locals: &mut IndexMap<String, Ty>,
848        effs: &mut EffectSet,
849    ) -> Result<Ty, TypeError> {
850        match e {
851            a::CExpr::Literal { value } => Ok(lit_type(value)),
852            a::CExpr::Var { name } => {
853                if let Some(t) = locals.get(name) {
854                    return Ok(t.clone());
855                }
856                if let Some(scheme) = self.globals.get(name).cloned() {
857                    return Ok(instantiate(&scheme, &mut self.u));
858                }
859                Err(TypeError::UnknownIdentifier { at_node: node_id.into(), name: name.clone() })
860            }
861            a::CExpr::Constructor { name, args } => self.check_constructor(name, args, node_id, locals, effs),
862            a::CExpr::Call { callee, args } => self.check_call(e, callee, args, node_id, locals, effs),
863            a::CExpr::Let { name, ty, value, body } => {
864                // Recover if the bound value fails to check: record the error
865                // and bind the name to a fresh var so the `let` body (which
866                // may hold further independent errors) is still checked (#566).
867                let v_ty = self.check_expr_recover(value, node_id, locals, effs);
868                if let Some(declared) = ty {
869                    let d = ty_from_canon_env(declared, &[], &self.type_env);
870                    if let Err(err) = self.unify_with_record_coercion(&v_ty, &d) {
871                        return Err(mismatch_err(node_id, err, &self.u, vec![format!("in let `{}`", name)]));
872                    }
873                }
874                let prev = locals.insert(name.clone(), v_ty);
875                let body_ty = self.check_expr(body, node_id, locals, effs)?;
876                match prev {
877                    Some(p) => { locals.insert(name.clone(), p); }
878                    None => { locals.shift_remove(name); }
879                }
880                Ok(body_ty)
881            }
882            a::CExpr::Match { scrutinee, arms } => {
883                let scrut_ty = self.check_expr(scrutinee, node_id, locals, effs)?;
884                if arms.is_empty() {
885                    return Err(TypeError::NonExhaustiveMatch {
886                        at_node: node_id.into(), missing: vec!["_".into()]
887                    });
888                }
889                let result_ty = self.u.fresh();
890                for arm in arms {
891                    let mut arm_locals = locals.clone();
892                    self.bind_pattern(&arm.pattern, &scrut_ty, &mut arm_locals, node_id)?;
893                    let arm_ty = self.check_expr(&arm.body, node_id, &mut arm_locals, effs)?;
894                    if let Err(err) = self.unify_with_record_coercion(&arm_ty, &result_ty) {
895                        return Err(mismatch_err(node_id, err, &self.u, vec!["in match arm".into()]));
896                    }
897                }
898                // Exhaustiveness (#766). Runs after every arm has been
899                // bound so the scrutinee's type is as resolved as it is
900                // going to get (a constructor pattern against a type
901                // variable pins the variable to its union).
902                let rows: Vec<Vec<a::Pattern>> = arms.iter().map(|arm| vec![arm.pattern.clone()]).collect();
903                if let Some(witnesses) = self.missing_patterns(&rows, std::slice::from_ref(&scrut_ty)) {
904                    return Err(TypeError::NonExhaustiveMatch {
905                        at_node: node_id.into(),
906                        missing: witnesses.into_iter().map(|w| w.join(", ")).collect(),
907                    });
908                }
909                Ok(result_ty)
910            }
911            a::CExpr::Block { statements, result } => {
912                // Each statement's value is discarded, so an error in one
913                // doesn't feed a later type — recover and keep checking the
914                // rest so every independent error surfaces in one pass (#566).
915                for s in statements {
916                    let _ = self.check_expr_recover(s, node_id, locals, effs);
917                }
918                self.check_expr(result, node_id, locals, effs)
919            }
920            a::CExpr::RecordLit { fields } => {
921                let mut tys = IndexMap::new();
922                for f in fields {
923                    if tys.contains_key(&f.name) {
924                        return Err(TypeError::DuplicateField {
925                            at_node: node_id.into(), field: f.name.clone()
926                        });
927                    }
928                    let ft = self.check_expr(&f.value, node_id, locals, effs)?;
929                    tys.insert(f.name.clone(), ft);
930                }
931                Ok(Ty::Record(tys))
932            }
933            a::CExpr::TupleLit { items } => {
934                let mut ts = Vec::new();
935                for it in items { ts.push(self.check_expr(it, node_id, locals, effs)?); }
936                Ok(Ty::Tuple(ts))
937            }
938            a::CExpr::ListLit { items } => {
939                let elem = self.u.fresh();
940                for it in items {
941                    let t = self.check_expr(it, node_id, locals, effs)?;
942                    if let Err(err) = self.unify_with_record_coercion(&t, &elem) {
943                        return Err(mismatch_err(node_id, err, &self.u, vec!["in list literal".into()]));
944                    }
945                }
946                Ok(Ty::List(Box::new(elem)))
947            }
948            a::CExpr::FieldAccess { value, field } => {
949                let vt = self.check_expr(value, node_id, locals, effs)?;
950                let resolved = self.u.resolve(&vt);
951                // Unfold a Record-aliased Con (e.g. `type Request = { ... }`
952                // or `type Box[T] = { value :: T }`). For parametric aliases
953                // the helper substitutes the actual args for the formal
954                // params; the post-unfold shape is only a Record when the
955                // alias body was a record, so non-record aliases (e.g.
956                // `type UserId = Int`) fall through to the
957                // "expected record" error below.
958                let resolved = if let Ty::Con(_, _) = &resolved {
959                    let unfolded = self.unfold_record_alias(resolved.clone());
960                    if matches!(unfolded, Ty::Record(_)) {
961                        unfolded
962                    } else {
963                        resolved
964                    }
965                } else {
966                    resolved
967                };
968                match resolved {
969                    Ty::Record(fields) => fields.get(field).cloned()
970                        .or_else(|| synthesize_decode_typed_field(field, &fields))
971                        .ok_or_else(|| TypeError::UnknownField {
972                            at_node: node_id.into(),
973                            record_type: Ty::Record(fields.clone()).pretty(),
974                            field: field.clone(),
975                        }),
976                    other => Err(TypeError::TypeMismatch {
977                        at_node: node_id.into(),
978                        expected: "record".into(),
979                        got: other.pretty(),
980                        context: vec![format!("field access `.{}`", field)],
981                    }),
982                }
983            }
984            a::CExpr::Lambda { params, return_type, effects: l_effects, effect_row_var: l_row_var, body } => {
985                let param_tys: Vec<Ty> = params.iter().map(|p| ty_from_canon_env(&p.ty, &[], &self.type_env)).collect();
986                let ret_ty = ty_from_canon_env(return_type, &[], &self.type_env);
987                // A row-polymorphic lambda (`fn (..) -> [io | E] ..`) resolves
988                // its tail `E` to the enclosing function's instantiated row-var
989                // id (recorded in `eff_row_scope`), so effects produced in the
990                // body — e.g. by calling a row-poly parameter — flow out through
991                // the closure's type (into `net.serve_fn` etc.) rather than
992                // being dropped. An unknown name is a plain error.
993                let row_var = match l_row_var {
994                    Some(name) => match self.eff_row_scope.get(name) {
995                        Some(id) => Some(*id),
996                        None => {
997                            return Err(TypeError::EffectNotDeclared {
998                                at_node: node_id.into(),
999                                effect: format!("unbound effect-row variable `{}`", name),
1000                            });
1001                        }
1002                    },
1003                    None => None,
1004                };
1005                let declared = EffectSet {
1006                    concrete: {
1007                        let mut s = std::collections::BTreeSet::new();
1008                        for e in l_effects {
1009                            let arg = e.arg.as_ref().map(|a| match a {
1010                                a::EffectArg::Str { value } => crate::types::EffectArg::Str(value.clone()),
1011                                a::EffectArg::Int { value } => crate::types::EffectArg::Int(*value),
1012                                a::EffectArg::Ident { value } => crate::types::EffectArg::Ident(value.clone()),
1013                            });
1014                            s.insert(crate::types::EffectKind { name: e.name.clone(), arg });
1015                        }
1016                        s
1017                    },
1018                    var: row_var,
1019                };
1020                let mut inner_locals = locals.clone();
1021                for (p, t) in params.iter().zip(param_tys.iter()) {
1022                    inner_locals.insert(p.name.clone(), t.clone());
1023                }
1024                let mut inner_effs = EffectSet::empty();
1025                let body_ty = self.check_expr(body, node_id, &mut inner_locals, &mut inner_effs)?;
1026                if let Err(err) = self.unify_with_record_coercion(&body_ty, &ret_ty) {
1027                    return Err(mismatch_err(node_id, err, &self.u, vec!["in lambda body".into()]));
1028                }
1029                if !inner_effs.is_subset(&declared) {
1030                    for e in inner_effs.concrete.iter() {
1031                        if !declared.concrete.iter().any(|d| d.subsumes(e)) {
1032                            return Err(TypeError::EffectNotDeclared {
1033                                at_node: node_id.into(),
1034                                effect: e.pretty(),
1035                            });
1036                        }
1037                    }
1038                }
1039                // The body produced an open effect row (e.g. by calling a
1040                // row-polymorphic parameter), but the lambda's declared row
1041                // doesn't carry that same tail — without `| E` the extra
1042                // effects would be silently dropped at the closure boundary.
1043                // Require the lambda to declare the matching open row.
1044                if let Some(iv) = inner_effs.var {
1045                    if declared.var != Some(iv) {
1046                        return Err(TypeError::EffectNotDeclared {
1047                            at_node: node_id.into(),
1048                            effect: "open effect row (annotate the lambda's effects with `| <row-var>`)".into(),
1049                        });
1050                    }
1051                }
1052                Ok(Ty::function(param_tys, declared, ret_ty))
1053            }
1054            a::CExpr::BinOp { op, lhs, rhs } => self.check_binop(op, lhs, rhs, node_id, locals, effs),
1055            a::CExpr::UnaryOp { op, expr } => {
1056                let t = self.check_expr(expr, node_id, locals, effs)?;
1057                match op.as_str() {
1058                    "-" => {
1059                        // Either Int or Float; we pick Int by default if unconstrained.
1060                        let r = self.u.resolve(&t);
1061                        match r {
1062                            Ty::Prim(Prim::Int) | Ty::Prim(Prim::Float) => Ok(t),
1063                            Ty::Var(_) => {
1064                                // default to Int.
1065                                self.u.unify(&t, &Ty::int()).map_err(|e| mismatch_err(node_id, e, &self.u, vec![]))?;
1066                                Ok(Ty::int())
1067                            }
1068                            other => Err(TypeError::TypeMismatch {
1069                                at_node: node_id.into(),
1070                                expected: "Int or Float".into(),
1071                                got: other.pretty(),
1072                                context: vec!["unary `-`".into()],
1073                            }),
1074                        }
1075                    }
1076                    "not" => {
1077                        self.u.unify(&t, &Ty::bool()).map_err(|e| mismatch_err(node_id, e, &self.u, vec!["unary `not`".into()]))?;
1078                        Ok(Ty::bool())
1079                    }
1080                    other => panic!("unknown unary op: {other}"),
1081                }
1082            }
1083            a::CExpr::Return { value } => {
1084                // For now treat Return as having type Never; the surrounding
1085                // context will unify with the actual return type.
1086                self.check_expr(value, node_id, locals, effs)?;
1087                Ok(Ty::Never)
1088            }
1089        }
1090    }
1091
1092    fn check_binop(
1093        &mut self,
1094        op: &str,
1095        lhs: &a::CExpr,
1096        rhs: &a::CExpr,
1097        node_id: &str,
1098        locals: &mut IndexMap<String, Ty>,
1099        effs: &mut EffectSet,
1100    ) -> Result<Ty, TypeError> {
1101        let lt = self.check_expr(lhs, node_id, locals, effs)?;
1102        let rt = self.check_expr(rhs, node_id, locals, effs)?;
1103        match op {
1104            "+" => {
1105                // #308: `+` is overloaded over Int, Float, and Str.
1106                // Str concatenation dispatches at the VM layer
1107                // (Op::NumAdd in bytecode handles all three).
1108                // #323: unfold one-step type aliases on the resolved
1109                // type so `type UserId = Int; id + id` works under
1110                // Option-A transparency. Same below for the other
1111                // numeric operator groups.
1112                self.u.unify(&lt, &rt).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1113                let r = self.unfold_record_alias(self.u.resolve(&lt));
1114                match r {
1115                    Ty::Prim(Prim::Int) | Ty::Prim(Prim::Float) | Ty::Prim(Prim::Str) => Ok(lt),
1116                    Ty::Var(_) => {
1117                        self.u.unify(&lt, &Ty::int()).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1118                        Ok(Ty::int())
1119                    }
1120                    other => Err(TypeError::TypeMismatch {
1121                        at_node: node_id.into(),
1122                        expected: "Int, Float, or Str".into(),
1123                        got: other.pretty(),
1124                        context: vec![format!("operator `{op}`")],
1125                    }),
1126                }
1127            }
1128            "-" | "*" | "/" | "%" => {
1129                self.u.unify(&lt, &rt).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1130                let r = self.unfold_record_alias(self.u.resolve(&lt));
1131                match r {
1132                    Ty::Prim(Prim::Int) | Ty::Prim(Prim::Float) => Ok(lt),
1133                    Ty::Var(_) => {
1134                        self.u.unify(&lt, &Ty::int()).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1135                        Ok(Ty::int())
1136                    }
1137                    other => Err(TypeError::TypeMismatch {
1138                        at_node: node_id.into(),
1139                        expected: "Int or Float".into(),
1140                        got: other.pretty(),
1141                        context: vec![format!("operator `{op}`")],
1142                    }),
1143                }
1144            }
1145            "==" | "!=" => {
1146                self.u.unify(&lt, &rt).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1147                Ok(Ty::bool())
1148            }
1149            "<" | "<=" | ">" | ">=" => {
1150                self.u.unify(&lt, &rt).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1151                let r = self.unfold_record_alias(self.u.resolve(&lt));
1152                match r {
1153                    Ty::Prim(Prim::Int) | Ty::Prim(Prim::Float) | Ty::Prim(Prim::Str) => Ok(Ty::bool()),
1154                    Ty::Var(_) => {
1155                        self.u.unify(&lt, &Ty::int()).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1156                        Ok(Ty::bool())
1157                    }
1158                    other => Err(TypeError::TypeMismatch {
1159                        at_node: node_id.into(),
1160                        expected: "Int, Float, or Str".into(),
1161                        got: other.pretty(),
1162                        context: vec![format!("operator `{op}`")],
1163                    }),
1164                }
1165            }
1166            "and" | "or" => {
1167                self.u.unify(&lt, &Ty::bool()).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1168                self.u.unify(&rt, &Ty::bool()).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("operator `{op}`")]))?;
1169                Ok(Ty::bool())
1170            }
1171            other => panic!("unknown binop: {other}"),
1172        }
1173    }
1174
1175    fn check_call(
1176        &mut self,
1177        call_expr: &a::CExpr,
1178        callee: &a::CExpr,
1179        args: &[a::CExpr],
1180        node_id: &str,
1181        locals: &mut IndexMap<String, Ty>,
1182        effs: &mut EffectSet,
1183    ) -> Result<Ty, TypeError> {
1184        // #168: identify the call before the recursive descent so we
1185        // can later rewrite this exact node. The identity is a stable
1186        // (stage, NodeId) pair rather than the expression's address
1187        // (#777), so the resulting table can be applied to any copy
1188        // of the checked stages. `is_module_parse_call` recognises
1189        // `<alias>.parse` where alias was bound to one of {json,
1190        // toml, yaml} during the import pass.
1191        let parse_site = if self.is_module_parse_call(callee) {
1192            self.parse_site_of(call_expr)
1193        } else {
1194            None
1195        };
1196        let callee_ty = self.check_expr(callee, node_id, locals, effs)?;
1197        let resolved = self.u.resolve(&callee_ty);
1198        match resolved {
1199            Ty::Function { params, effects, ret } => {
1200                if params.len() != args.len() {
1201                    return Err(TypeError::ArityMismatch {
1202                        at_node: node_id.into(),
1203                        expected: params.len(),
1204                        got: args.len(),
1205                    });
1206                }
1207                for (i, (a, p)) in args.iter().zip(params.iter()).enumerate() {
1208                    let at = self.check_expr(a, node_id, locals, effs)?;
1209                    if let Err(err) = self.unify_with_record_coercion(&at, p) {
1210                        return Err(mismatch_err(node_id, err, &self.u, vec![format!("argument {} of call", i + 1)]));
1211                    }
1212                }
1213                // #209 slice 2: refinement discharge for direct named
1214                // calls. Look up the callee's original params (kept
1215                // pre-strip in `fn_params`), and for each refined
1216                // param attempt static discharge against the call
1217                // arg. Refuted = type error; Deferred = pass (slice
1218                // 3 will add a runtime residual check).
1219                if let a::CExpr::Var { name: callee_name } = callee {
1220                    if let Some(callee_params) = self.fn_params.get(callee_name).cloned() {
1221                        for (i, (param, arg)) in callee_params.iter().zip(args.iter()).enumerate() {
1222                            if let a::TypeExpr::Refined { binding, predicate, .. } = &param.ty {
1223                                let outcome = crate::discharge::try_discharge(
1224                                    predicate, binding, arg);
1225                                if let crate::discharge::DischargeOutcome::Refuted { reason } = outcome {
1226                                    return Err(TypeError::RefinementViolation {
1227                                        at_node: node_id.into(),
1228                                        fn_name: callee_name.clone(),
1229                                        param_index: i,
1230                                        binding: binding.clone(),
1231                                        reason,
1232                                    });
1233                                }
1234                            }
1235                        }
1236                    }
1237                }
1238                // Re-resolve effects after unifying args: an effect-row
1239                // variable on the function type may have been bound by
1240                // an argument's closure type, and we want the
1241                // *post-binding* set when propagating to the caller.
1242                let resolved_effects = self.u.resolve_effects(&effects);
1243                effs.extend(&resolved_effects);
1244                // #168: snapshot the post-arg-unification return type
1245                // for stdlib parse calls. Resolution to the eventual
1246                // `Result[Record{...}, _]` shape happens at the end
1247                // of `check_program` once the whole program's
1248                // unification has settled — match-pattern annotations
1249                // and let-type-annotations may bind T after this
1250                // point.
1251                if let Some(site) = parse_site {
1252                    self.pending_parse_calls.push((site, (*ret).clone()));
1253                }
1254                Ok(*ret)
1255            }
1256            Ty::Var(_) => {
1257                // Build a function type and unify.
1258                let mut p_tys = Vec::new();
1259                for a in args { p_tys.push(self.check_expr(a, node_id, locals, effs)?); }
1260                let r = self.u.fresh();
1261                let f = Ty::function(p_tys, EffectSet::empty(), r.clone());
1262                self.u.unify(&callee_ty, &f).map_err(|e| mismatch_err(node_id, e, &self.u, vec!["in call".into()]))?;
1263                Ok(r)
1264            }
1265            other => Err(TypeError::TypeMismatch {
1266                at_node: node_id.into(),
1267                expected: "function".into(),
1268                got: other.pretty(),
1269                context: vec!["in call".into()],
1270            }),
1271        }
1272    }
1273
1274    fn check_constructor(
1275        &mut self,
1276        name: &str,
1277        args: &[a::CExpr],
1278        node_id: &str,
1279        locals: &mut IndexMap<String, Ty>,
1280        effs: &mut EffectSet,
1281    ) -> Result<Ty, TypeError> {
1282        let owning = self.type_env.ctor_to_type.get(name).cloned()
1283            .ok_or_else(|| TypeError::UnknownVariant {
1284                at_node: node_id.into(),
1285                constructor: name.to_string(),
1286            })?;
1287        let def = self.type_env.types.get(&owning).cloned()
1288            .expect("ctor_to_type points to a real type");
1289        let variants = match &def.kind {
1290            TypeDefKind::Union(v) => v.clone(),
1291            _ => return Err(TypeError::UnknownVariant {
1292                at_node: node_id.into(),
1293                constructor: name.to_string(),
1294            }),
1295        };
1296        // Instantiate the type's params with fresh vars; substitute into
1297        // both the variant's payload type and the resulting Con(...).
1298        let mut subst = IndexMap::new();
1299        let mut con_args = Vec::with_capacity(def.params.len());
1300        for (i, _p) in def.params.iter().enumerate() {
1301            let fresh = self.u.fresh();
1302            subst.insert(i as u32, fresh.clone());
1303            con_args.push(fresh);
1304        }
1305        let payload = variants.get(name).cloned().flatten();
1306        match (payload, args) {
1307            (None, []) => Ok(Ty::Con(owning, con_args)),
1308            (Some(payload), args) => {
1309                let inst_payload = subst_vars(&payload, &subst, &IndexMap::new());
1310                let arg_count = match &inst_payload {
1311                    Ty::Tuple(items) => items.len(),
1312                    _ => 1,
1313                };
1314                if arg_count != args.len() {
1315                    return Err(TypeError::ArityMismatch {
1316                        at_node: node_id.into(),
1317                        expected: arg_count,
1318                        got: args.len(),
1319                    });
1320                }
1321                if args.len() == 1 {
1322                    let at = self.check_expr(&args[0], node_id, locals, effs)?;
1323                    self.unify_with_record_coercion(&at, &inst_payload).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("constructor `{}`", name)]))?;
1324                } else if let Ty::Tuple(items) = inst_payload {
1325                    for (i, (a, t)) in args.iter().zip(items.iter()).enumerate() {
1326                        let at = self.check_expr(a, node_id, locals, effs)?;
1327                        self.unify_with_record_coercion(&at, t).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("constructor `{}` arg {}", name, i + 1)]))?;
1328                    }
1329                }
1330                Ok(Ty::Con(owning, con_args))
1331            }
1332            (None, _) => Err(TypeError::ArityMismatch {
1333                at_node: node_id.into(), expected: 0, got: args.len(),
1334            }),
1335        }
1336    }
1337
1338    fn bind_pattern(
1339        &mut self,
1340        pat: &a::Pattern,
1341        ty: &Ty,
1342        locals: &mut IndexMap<String, Ty>,
1343        node_id: &str,
1344    ) -> Result<(), TypeError> {
1345        match pat {
1346            a::Pattern::PWild => Ok(()),
1347            a::Pattern::PVar { name } => {
1348                locals.insert(name.clone(), ty.clone());
1349                Ok(())
1350            }
1351            a::Pattern::PLiteral { value } => {
1352                let lt = lit_type(value);
1353                self.unify_with_record_coercion(&lt, ty).map_err(|e| mismatch_err(node_id, e, &self.u, vec!["in pattern".into()]))?;
1354                Ok(())
1355            }
1356            a::Pattern::PConstructor { name, args } => {
1357                // Re-use constructor logic but in pattern position.
1358                let owning = self.type_env.ctor_to_type.get(name).cloned()
1359                    .ok_or_else(|| TypeError::UnknownVariant {
1360                        at_node: node_id.into(), constructor: name.clone(),
1361                    })?;
1362                let def = self.type_env.types.get(&owning).cloned().unwrap();
1363                let mut subst = IndexMap::new();
1364                let mut con_args = Vec::new();
1365                for (i, _) in def.params.iter().enumerate() {
1366                    let fresh = self.u.fresh();
1367                    subst.insert(i as u32, fresh.clone());
1368                    con_args.push(fresh);
1369                }
1370                let con_ty = Ty::Con(owning.clone(), con_args);
1371                self.unify_with_record_coercion(&con_ty, ty).map_err(|e| mismatch_err(node_id, e, &self.u, vec![format!("constructor pattern `{}`", name)]))?;
1372                let payload = match &def.kind {
1373                    TypeDefKind::Union(v) => v.get(name).cloned().flatten(),
1374                    _ => None,
1375                };
1376                match (payload, args.as_slice()) {
1377                    (None, []) => Ok(()),
1378                    (Some(payload), args) => {
1379                        let inst = subst_vars(&payload, &subst, &IndexMap::new());
1380                        if args.len() == 1 {
1381                            self.bind_pattern(&args[0], &inst, locals, node_id)?;
1382                        } else if let Ty::Tuple(items) = inst {
1383                            for (a, t) in args.iter().zip(items.iter()) {
1384                                self.bind_pattern(a, t, locals, node_id)?;
1385                            }
1386                        }
1387                        Ok(())
1388                    }
1389                    (None, _) => Err(TypeError::ArityMismatch {
1390                        at_node: node_id.into(), expected: 0, got: args.len(),
1391                    }),
1392                }
1393            }
1394            a::Pattern::PRecord { fields } => {
1395                // Unfold a record-aliased Con (`type Bands = { ... }`)
1396                // so a structural `{ idea: pat, ... }` pattern can match
1397                // a nominal-typed scrutinee, mirror of #79's literal
1398                // coercion at every position.
1399                let resolved = self.unfold_record_alias(self.u.resolve(ty));
1400                let rec = match resolved {
1401                    Ty::Record(r) => r,
1402                    _ => return Err(TypeError::TypeMismatch {
1403                        at_node: node_id.into(),
1404                        expected: "record".into(),
1405                        got: ty.pretty(),
1406                        context: vec!["in record pattern".into()],
1407                    }),
1408                };
1409                for f in fields {
1410                    let ft = rec.get(&f.name).cloned()
1411                        .ok_or_else(|| TypeError::UnknownField {
1412                            at_node: node_id.into(),
1413                            record_type: Ty::Record(rec.clone()).pretty(),
1414                            field: f.name.clone(),
1415                        })?;
1416                    self.bind_pattern(&f.pattern, &ft, locals, node_id)?;
1417                }
1418                Ok(())
1419            }
1420            a::Pattern::PTuple { items } => {
1421                // An empty-tuple pattern `()` is equivalent to Unit.
1422                if items.is_empty() {
1423                    return self.unify_with_record_coercion(&Ty::Unit, ty)
1424                        .map_err(|e| mismatch_err(node_id, e, &self.u, vec!["in unit pattern".into()]));
1425                }
1426                let resolved = self.u.resolve(ty);
1427                let tup = match resolved {
1428                    Ty::Tuple(t) => t,
1429                    Ty::Var(_) => {
1430                        let fresh: Vec<Ty> = items.iter().map(|_| self.u.fresh()).collect();
1431                        let tup_ty = Ty::Tuple(fresh.clone());
1432                        self.unify_with_record_coercion(&tup_ty, ty).map_err(|e| mismatch_err(node_id, e, &self.u, vec!["in tuple pattern".into()]))?;
1433                        fresh
1434                    }
1435                    other => {
1436                        return Err(TypeError::TypeMismatch {
1437                            at_node: node_id.into(),
1438                            expected: "tuple".into(),
1439                            got: other.pretty(),
1440                            context: vec!["in tuple pattern".into()],
1441                        });
1442                    }
1443                };
1444                if tup.len() != items.len() {
1445                    return Err(TypeError::ArityMismatch {
1446                        at_node: node_id.into(), expected: tup.len(), got: items.len(),
1447                    });
1448                }
1449                for (p, t) in items.iter().zip(tup.iter()) {
1450                    self.bind_pattern(p, t, locals, node_id)?;
1451                }
1452                Ok(())
1453            }
1454        }
1455    }
1456}
1457
1458fn lit_type(l: &a::CLit) -> Ty {
1459    match l {
1460        a::CLit::Int { .. } => Ty::int(),
1461        a::CLit::Float { .. } => Ty::float(),
1462        a::CLit::Str { .. } => Ty::str(),
1463        a::CLit::Bytes { .. } => Ty::bytes(),
1464        a::CLit::Bool { .. } => Ty::bool(),
1465        a::CLit::Unit => Ty::Unit,
1466    }
1467}
1468
1469fn instantiate(s: &Scheme, u: &mut Unifier) -> Ty {
1470    instantiate_with_eff(s, u).0
1471}
1472
1473/// Like `instantiate`, but also returns the effect-var substitution
1474/// (scheme effect-var id → fresh id). `check_fn` uses it to map the
1475/// function's surface row-variable names to their instantiated ids, so a
1476/// row-polymorphic lambda in the body can join the same row.
1477fn instantiate_with_eff(s: &Scheme, u: &mut Unifier) -> (Ty, IndexMap<u32, u32>) {
1478    let mut ty_subst = IndexMap::new();
1479    for v in &s.vars { ty_subst.insert(*v, u.fresh()); }
1480    let mut eff_subst = IndexMap::new();
1481    for v in &s.eff_vars { eff_subst.insert(*v, u.fresh_eff_id()); }
1482    let ty = subst_vars(&s.ty, &ty_subst, &eff_subst);
1483    (ty, eff_subst)
1484}
1485
1486fn subst_vars(
1487    t: &Ty,
1488    subst: &IndexMap<TyVarId, Ty>,
1489    eff_subst: &IndexMap<u32, u32>,
1490) -> Ty {
1491    match t {
1492        Ty::Var(v) => subst.get(v).cloned().unwrap_or_else(|| Ty::Var(*v)),
1493        Ty::Prim(_) | Ty::Unit | Ty::Never => t.clone(),
1494        Ty::List(inner) => Ty::List(Box::new(subst_vars(inner, subst, eff_subst))),
1495        Ty::Tuple(items) => Ty::Tuple(items.iter().map(|t| subst_vars(t, subst, eff_subst)).collect()),
1496        Ty::Record(fs) => {
1497            let mut out = IndexMap::new();
1498            for (k, v) in fs { out.insert(k.clone(), subst_vars(v, subst, eff_subst)); }
1499            Ty::Record(out)
1500        }
1501        Ty::Con(n, args) => Ty::Con(n.clone(),
1502            args.iter().map(|t| subst_vars(t, subst, eff_subst)).collect()),
1503        Ty::Function { params, effects, ret } => {
1504            // Refresh the effect-row variable if it's quantified in the
1505            // scheme; concrete kinds carry through unchanged.
1506            let new_effects = EffectSet {
1507                concrete: effects.concrete.clone(),
1508                var: effects.var.and_then(|v| eff_subst.get(&v).copied()).or(effects.var),
1509            };
1510            Ty::Function {
1511                params: params.iter().map(|t| subst_vars(t, subst, eff_subst)).collect(),
1512                effects: new_effects,
1513                ret: Box::new(subst_vars(ret, subst, eff_subst)),
1514            }
1515        }
1516    }
1517}
1518
1519fn mismatch_err(node_id: &str, e: UnifyError, u: &Unifier, context: Vec<String>) -> TypeError {
1520    match e {
1521        UnifyError::Mismatch { a, b } => TypeError::TypeMismatch {
1522            at_node: node_id.into(),
1523            expected: u.resolve(&b).pretty(),
1524            got: u.resolve(&a).pretty(),
1525            context,
1526        },
1527        UnifyError::Infinite { .. } => TypeError::InfiniteType { at_node: node_id.into() },
1528        UnifyError::EffectMismatch { a, b } => {
1529            // Render the two rows in compact form, e.g. `[net]` vs `[]`.
1530            // Effect rows are invariant, so this is its own rule_tag
1531            // (#565) rather than a generic type-mismatch — the
1532            // explanation steers the fix toward narrowing the body.
1533            let render = |e: &EffectSet| -> String {
1534                let mut parts: Vec<String> = e.concrete.iter()
1535                    .map(crate::types::EffectKind::pretty).collect();
1536                if let Some(v) = e.var { parts.push(format!("?e{}", v)); }
1537                if parts.is_empty() { "[]".into() } else { format!("[{}]", parts.join(", ")) }
1538            };
1539            TypeError::EffectRowMismatch {
1540                at_node: node_id.into(),
1541                expected: render(&b),
1542                got: render(&a),
1543                context,
1544            }
1545        }
1546    }
1547}