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