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