Skip to main content

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