//! AST → AST rewriter that runs **after** [`crate::load_module`] and //! **before** anything that consumes a [`Module`] (typecheck, codegen). //! //! ## Iter 16a: nested constructor patterns in `match` //! //! Through Iter 15e, a [`Pattern::Ctor`]'s sub-patterns were restricted to //! [`Pattern::Var`] and [`Pattern::Wild`] — nested ctors like //! `(pat-ctor Cons a (pat-ctor Cons b _))` were rejected by the //! checker (`nested-ctor-pattern-not-allowed`) and silently mishandled //! by the codegen. Iter 16a lifts the gate by **desugaring** nested //! ctor sub-patterns into chains of single-level matches at the AST //! level, before either the checker or codegen sees the module. The //! rewrite is pure (no side effects, no I/O), runs in memory, and //! produces a [`Module`] semantically equivalent to its input. //! //! Lit sub-patterns inside a Ctor remain rejected — that is a separate //! iter. The [`crate::ast::Pattern`] enum, the JSON schema, and every //! on-disk hash are unchanged: the desugar runs **after** `load_module`, //! so canonical bytes computed from the source file are untouched. The //! checker / codegen consume the desugared form. //! //! ## Algorithm //! //! For a [`Term::Match`] whose arms contain a non-flat ctor pattern: //! //! 1. Bottom-up: recursively desugar the scrutinee and every arm body //! first (children are normalised before parents). //! 2. Let-bind the scrutinee to a fresh name `$mp_N` (so that we don't //! re-evaluate effectful scrutinees per arm). //! 3. Build a chain of single-level matches via //! `build_chain(s_var, arms, default)` where `default` is the //! polymorphic bottom builtin `__unreachable__` (`forall a. a`) //! — codegen lowers it to LLVM `unreachable`. Valid programs //! never reach it because the checker requires exhaustiveness //! (the catch-all arm dominates the chain). An earlier shape used //! a `Unit` literal as the default, which forced any match whose //! arms returned a non-Unit type to carry a synthetic `_` arm //! dominating the terminator. The polymorphic `__unreachable__` //! removes that workaround. //! 4. Each arm is lowered via `desugar_one_arm`: //! - `Pattern::Wild` → arm body (catch-all; later arms are dropped). //! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`. //! - `Pattern::Lit { lit }` → `Term::If { cond = (== s_var lit), //! then = arm.body, else_ = fall_k }` (Iter 16c). After this //! rewrite, no `Pattern::Lit` reaches typecheck or codegen. //! - `Pattern::Ctor { ctor, fields }`: lift each field to a fresh //! var, build a flat outer pattern, walk fields right-to-left //! wrapping inner sub-patterns via `wrap_sub`. Emit a //! `Term::Match` with two arms: the flat ctor and a wildcard //! fall-through into the rest of the chain. //! //! `wrap_sub(fv, sub, body, fall_k)` recurses on `sub`: //! - `Var(n)` → `Let(n, Var(fv), body)`. //! - `Wild` → `body`. //! - `Lit` (Iter 16c) → `Term::If { cond = (== fv lit), then = body, //! else_ = fall_k }`. Same shape as the top-level case but on the //! field-bound fresh variable. //! - `Ctor` → recursively `desugar_match(Var(fv), [(sub, body), //! (Wild, fall_k)])`. The recursion is what handles arbitrary //! nesting depth (`Cons a (Cons b (Cons c _))`). //! //! The fall-through term is cloned per inner match because each branch //! that fails must continue to the same fallthrough. Worst-case size is //! O(arms × depth); acceptable for typical patterns. //! //! ## Fresh-name safety //! //! `$` is a valid identifier character in form (A) (the lexer's `Ident` //! token is "anything not paren/int/string"), so a user *could* write //! `$mp_0` themselves. To avoid collisions, the fresh-name generator //! reads a pre-collected set of every name appearing in any //! [`crate::ast::Term::Var`] or [`crate::ast::Pattern::Var`] of the //! module and bumps the counter until it lands on a name not in that //! set. //! //! ## Iter 16b.1: local recursive `let` (no-capture) //! //! [`Term::LetRec`] is a surface-only AST node introduced in 16b.1. The //! desugar pass eliminates it before typecheck/codegen by **lifting** //! the LetRec to a synthetic top-level fn in the same module. The //! lifted name has the form `$lr_N`, fresh against both the //! existing module-top-level def names and the `Desugarer::used` set. //! Inside the LetRec's `body` and `in_term`, every reference to the //! original local name is rewritten via [`subst_var`] to the lifted //! name. The lifted [`FnDef`] is appended to the module's `defs` so //! the typechecker / codegen see it as if it had been written //! top-level by hand. //! //! Capture is **not** supported in 16b.1: if the LetRec's body's free //! variables (excluding `{name} ∪ params`) intersect the enclosing //! lexical scope (params of the surrounding fn, let-bound names, or //! pattern-bound names), the desugar pass panics with a diagnostic //! that points the author at 16b.2 (closure conversion). Free vars //! that resolve to module-top-level def names, qualified import names //! (containing `.`), effect-op names (containing `/`), and builtin //! operators are all ignored — those reach a lifted top-level fn //! through the same path they reach any other top-level fn. //! //! ## What this module deliberately does not do //! //! - It does not alter [`crate::ast::Pattern`] or [`crate::ast::Term`]. //! - It does not introduce a new IR or hashable form. //! - It does not implement Maranget-style decision trees; the rewrite //! is a literal-translation chain that the existing single-level //! match codegen already handles. //! - It does not implement closure conversion for [`Term::LetRec`] //! bodies that capture from the enclosing scope (queued as 16b.2). use crate::ast::*; use std::collections::{BTreeMap, BTreeSet}; /// per-name scope information used by the LetRec lifter. /// `KnownType(t)` is set for fn-params and Lam-params, where the /// type is statically declared. Other binder kinds use the /// placeholder variants and the LetRec lifter rejects captures of /// those names with a clear error pointing at the follow-up iter /// that will lift the restriction. #[derive(Debug, Clone)] enum ScopeEntry { /// Fn-param or Lam-param, type known at this point. KnownType(Type), /// Bound by `Term::Let` — value's type is inferred at /// typecheck, unknown at desugar. Captures error → 16b.3. /// Also used for fn-params of a `Type::Forall`-typed enclosing /// fn, where the param types may mention outer type vars and /// the lifted signature would need a synthesized `Forall` — /// out of scope for 16b.2 (queued for 16b.6). LetBound, /// Bound by a `Term::Match` arm pattern — type requires /// constructor-field substitution from the scrutinee. The /// desugar pass cannot resolve it (the scrutinee's type may /// only be known after inference); captures of this kind are /// deferred to `ailang-check::lift_letrecs` (Iter 16b.4), /// which uses `type_check_pattern_for_lift` to substitute the /// scrutinee's type args into the matched ctor's declared /// field types and extend the locals scope accordingly. MatchArm, /// Bound by a `Term::LetRec` (its own name) — fn-typed, but /// the value flows through a recursive position; supported /// for fn/Lam-param transitivity but not for nested-LetRec /// mutual capture. The LetRec lifter rejects captures of a /// name with this entry → 16b.7. EnclosingLetRec, } /// Rewrite `m` so that every [`Pattern::Ctor`] sub-pattern is a /// [`Pattern::Var`] or [`Pattern::Wild`] (i.e. flat) and every /// [`Term::LetRec`] is replaced by a reference to a synthetic /// top-level fn. /// /// Pure / total: returns a new [`Module`]; does not mutate `m`. /// Idempotent: a module that is already flat and LetRec-free is /// returned as-is modulo cloning. pub fn desugar_module(m: &Module) -> Module { let mut used: BTreeSet = BTreeSet::new(); for def in &m.defs { match def { Def::Fn(f) => { used.insert(f.name.clone()); for p in &f.params { used.insert(p.clone()); } collect_used_in_term(&f.body, &mut used); } Def::Const(c) => { used.insert(c.name.clone()); collect_used_in_term(&c.value, &mut used); } Def::Type(td) => { used.insert(td.name.clone()); } // class/instance defs are passthrough for the // desugar pass — their bodies (default methods, instance // method bodies) will be desugared once 22b.2 wires the // class/instance arms into typecheck. For 22b.1 the names // still go into `used` so the synthetic-name generator // does not collide. Def::Class(c) => { used.insert(c.name.clone()); } Def::Instance(i) => { used.insert(i.class.clone()); } } } // pre-collect every top-level def name. The LetRec // lifter consults this set to (a) know whether a free var of a // LetRec body resolves to a top-level def (no capture) and // (b) keep its fresh-name generator from colliding with an // existing def — including ones lifted earlier in the same pass. let mut module_top_names: BTreeSet = BTreeSet::new(); for def in &m.defs { module_top_names.insert(def.name().to_string()); } let mut d = Desugarer { counter: 0, used, lifted: Vec::new(), module_top_names, current_def_forall_vars: Vec::new(), }; let mut out = m.clone(); for def in &mut out.defs { match def { Def::Fn(f) => { // Build initial scope from fn-params with their // declared types (peeled out of `f.ty`). // // 16b.6: a `Type::Forall { vars, body: Fn(ptys, ...) }` // enclosing fn is now supported. Its param types may // mention `vars` — that's fine, the lifted fn becomes // `Forall(vars, Fn(ptys ++ capture_tys, ...))` so the // type vars are still bound at the lift site. Fn-params // of a Forall enclosing fn therefore enter the scope as // `KnownType`, not `LetBound` (the 16b.2 fallback was // overly conservative). The enclosing fn's `Forall.vars` // are stashed in `Desugarer.current_def_forall_vars` for // the LetRec arm to read. let mut scope: BTreeMap = BTreeMap::new(); let inner_fn_ty: Option<&Type> = match &f.ty { Type::Fn { .. } => Some(&f.ty), Type::Forall { body, .. } => match body.as_ref() { Type::Fn { .. } => Some(body.as_ref()), _ => None, }, _ => None, }; let inner_fn_params: Option<&[Type]> = inner_fn_ty.and_then(|t| match t { Type::Fn { params, .. } => Some(params.as_slice()), _ => None, }); match inner_fn_params { Some(ptys) if ptys.len() == f.params.len() => { for (p, pty) in f.params.iter().zip(ptys.iter()) { scope.insert(p.clone(), ScopeEntry::KnownType(pty.clone())); } } _ => { // Defensive: malformed fn type (arity mismatch, // non-Fn). Fall back to LetBound for every param // so captures are rejected cleanly rather than // silently mistyped. for p in &f.params { scope.insert(p.clone(), ScopeEntry::LetBound); } } } // stash the enclosing fn's Forall.vars (or // empty for a mono enclosing fn) for the LetRec arm. let saved = std::mem::take(&mut d.current_def_forall_vars); d.current_def_forall_vars = match &f.ty { Type::Forall { vars, .. } => vars.clone(), _ => Vec::new(), }; f.body = d.desugar_term(&f.body, &scope); d.current_def_forall_vars = saved; } Def::Const(c) => { let scope: BTreeMap = BTreeMap::new(); c.value = d.desugar_term(&c.value, &scope); } Def::Type(_) => {} // class/instance defs are not desugared yet. // Their bodies (default methods, instance method bodies) // will be processed once 22b.2 lands the class/instance // arms in typecheck and 22b.3 in codegen. Def::Class(_) | Def::Instance(_) => {} } } // append every lifted fn to the desugared module. // Order: original defs first, then lifts in the order they were // produced by the bottom-up walk. Typecheck/codegen are order- // insensitive at the def list level (they index by name), so this // is purely cosmetic. out.defs.extend(d.lifted); out } /// Walks a term and inserts every [`Term::Var.name`] and every /// [`Pattern::Var.name`] into `used`. Used to seed /// [`Desugarer::fresh`] so a generated name `$mp_N` cannot shadow a /// source-level identifier. fn collect_used_in_term(t: &Term, used: &mut BTreeSet) { match t { Term::Lit { .. } => {} Term::Var { name } => { used.insert(name.clone()); } Term::App { callee, args, .. } => { collect_used_in_term(callee, used); for a in args { collect_used_in_term(a, used); } } Term::Let { name, value, body } => { used.insert(name.clone()); collect_used_in_term(value, used); collect_used_in_term(body, used); } Term::If { cond, then, else_ } => { collect_used_in_term(cond, used); collect_used_in_term(then, used); collect_used_in_term(else_, used); } Term::Do { args, .. } => { for a in args { collect_used_in_term(a, used); } } Term::Ctor { args, .. } => { for a in args { collect_used_in_term(a, used); } } Term::Match { scrutinee, arms } => { collect_used_in_term(scrutinee, used); for arm in arms { collect_used_in_pattern(&arm.pat, used); collect_used_in_term(&arm.body, used); } } Term::Lam { params, body, .. } => { for p in params { used.insert(p.clone()); } collect_used_in_term(body, used); } Term::Seq { lhs, rhs } => { collect_used_in_term(lhs, used); collect_used_in_term(rhs, used); } Term::LetRec { name, params, body, in_term, .. } => { used.insert(name.clone()); for p in params { used.insert(p.clone()); } collect_used_in_term(body, used); collect_used_in_term(in_term, used); } Term::Clone { value } => { // identity for the used-name walk. collect_used_in_term(value, used); } Term::ReuseAs { source, body } => { // structural recursion through both children. collect_used_in_term(source, used); collect_used_in_term(body, used); } Term::Loop { binders, body } => { for b in binders { used.insert(b.name.clone()); collect_used_in_term(&b.init, used); } collect_used_in_term(body, used); } Term::Recur { args } => { for a in args { collect_used_in_term(a, used); } } Term::New { args, .. } => { for arg in args { match arg { NewArg::Value(v) => collect_used_in_term(v, used), NewArg::Type(_) => {} } } } } } /// Walks a pattern and inserts every [`Pattern::Var.name`] into `used`. fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet) { match p { Pattern::Wild => {} Pattern::Var { name } => { used.insert(name.clone()); } Pattern::Lit { .. } => {} Pattern::Ctor { fields, .. } => { for sub in fields { collect_used_in_pattern(sub, used); } } } } /// State carried across a module-wide desugar pass. /// /// `counter` is incremented for every fresh-name request; `used` /// contains every source-level identifier (var-bind or var-reference) /// in the module so that [`fresh`](Self::fresh) cannot shadow one. /// /// LetRec-lift fields: /// - `lifted` accumulates synthetic top-level fns produced by /// [`Term::LetRec`] desugaring. Appended to `Module.defs` once the /// per-def walk finishes. /// - `module_top_names` mirrors every name reachable as a top-level /// def at this point in the pass (originals + already-lifted). The /// capture analyser uses it to classify free vars; the fresh-name /// generator [`fresh_lifted`](Self::fresh_lifted) consults it so /// later lifts cannot collide with earlier ones. /// /// Forall-tracking field: /// - `current_def_forall_vars` carries the enclosing fn's /// `Type::Forall.vars` while desugaring its body. Empty for /// monomorphic enclosing fns. Read by the LetRec lifter to wrap /// the synthetic lifted fn's signature in `Type::Forall` mirroring /// the enclosing fn — so the lifted fn enters codegen's /// monomorphisation queue at every call site of the enclosing fn, /// specialising at the same type args as its host. struct Desugarer { counter: u64, used: BTreeSet, lifted: Vec, module_top_names: BTreeSet, /// the enclosing fn's Forall.vars (or empty if mono). /// Reset on entry to each `Def::Fn`. current_def_forall_vars: Vec, } impl Desugarer { /// Returns a name of the form `$mp_N` not present in `used`. /// Increments the counter until it lands on a free name; the /// generated name itself is added to `used` so a subsequent call /// returns a distinct one. fn fresh(&mut self) -> String { loop { let n = format!("$mp_{}", self.counter); self.counter += 1; if !self.used.contains(&n) { self.used.insert(n.clone()); return n; } } } /// returns a name of the form `$lr_N` not in /// `used` and not in `module_top_names`. Bumps both sets so a /// later lift cannot collide. The `hint` is the source-level /// LetRec name; it makes lifted bindings traceable through the /// pipeline (typecheck errors / IR mangling / panic messages). fn fresh_lifted(&mut self, hint: &str) -> String { let mut n = 0u64; loop { let candidate = format!("{hint}$lr_{n}"); n += 1; if !self.used.contains(&candidate) && !self.module_top_names.contains(&candidate) { self.used.insert(candidate.clone()); self.module_top_names.insert(candidate.clone()); return candidate; } } } /// Recursively rewrites `t`: descends into every child first /// (bottom-up), then dispatches a [`Term::Match`] to /// [`desugar_match`](Self::desugar_match) and a [`Term::LetRec`] /// to the 16b.1 / 16b.2 lifter. /// /// `scope` maps every name lexically bound at this point to its /// [`ScopeEntry`] — initialised at the def boundary (fn-params /// with `KnownType` for monomorphic enclosing fns; `LetBound` /// for `Type::Forall`-quantified ones; empty for [`Def::Const`]) /// and extended locally by every binder ([`Term::Let`] → /// `LetBound`; [`Term::Lam`] → `KnownType`; [`Term::Match`] arm /// pattern bindings → `MatchArm`; [`Term::LetRec`] → its own /// name as `EnclosingLetRec`, params as `KnownType`). Used by /// the LetRec lifter to decide between (a) the 16b.2 fast path /// (all KnownType captures → lift here), (b) the 16b.3/16b.4 /// defer path (any LetBound or MatchArm capture → leave the /// LetRec in place for `ailang-check::lift_letrecs`), and (c) /// the 16b.7 panic path (EnclosingLetRec capture). fn desugar_term(&mut self, t: &Term, scope: &BTreeMap) -> Term { match t { Term::Lit { .. } | Term::Var { .. } => t.clone(), Term::App { callee, args, tail } => Term::App { callee: Box::new(self.desugar_term(callee, scope)), args: args.iter().map(|a| self.desugar_term(a, scope)).collect(), tail: *tail, }, Term::Let { name, value, body } => { let v = self.desugar_term(value, scope); let mut inner = scope.clone(); inner.insert(name.clone(), ScopeEntry::LetBound); let b = self.desugar_term(body, &inner); Term::Let { name: name.clone(), value: Box::new(v), body: Box::new(b), } } Term::If { cond, then, else_ } => Term::If { cond: Box::new(self.desugar_term(cond, scope)), then: Box::new(self.desugar_term(then, scope)), else_: Box::new(self.desugar_term(else_, scope)), }, Term::Do { op, args, tail } => Term::Do { op: op.clone(), args: args.iter().map(|a| self.desugar_term(a, scope)).collect(), tail: *tail, }, Term::Ctor { type_name, ctor, args } => Term::Ctor { type_name: type_name.clone(), ctor: ctor.clone(), args: args.iter().map(|a| self.desugar_term(a, scope)).collect(), }, Term::Match { scrutinee, arms } => { // Recurse into children first (bottom-up). let scrutinee = self.desugar_term(scrutinee, scope); let arms: Vec = arms .iter() .map(|a| { let mut inner = scope.clone(); let mut pat_binds: BTreeSet = BTreeSet::new(); pattern_binds(&a.pat, &mut pat_binds); for n in pat_binds { inner.insert(n, ScopeEntry::MatchArm); } Arm { pat: a.pat.clone(), body: self.desugar_term(&a.body, &inner), } }) .collect(); self.desugar_match(scrutinee, arms) } Term::Lam { params, param_tys, ret_ty, effects, body, } => { let mut inner = scope.clone(); for (p, pty) in params.iter().zip(param_tys.iter()) { inner.insert(p.clone(), ScopeEntry::KnownType(pty.clone())); } Term::Lam { params: params.clone(), param_tys: param_tys.clone(), ret_ty: ret_ty.clone(), effects: effects.clone(), body: Box::new(self.desugar_term(body, &inner)), } } Term::Seq { lhs, rhs } => Term::Seq { lhs: Box::new(self.desugar_term(lhs, scope)), rhs: Box::new(self.desugar_term(rhs, scope)), }, Term::Clone { value } => Term::Clone { // pure structural recursion through the // wrapper. Same pattern as `Term::Let`'s value branch. value: Box::new(self.desugar_term(value, scope)), }, Term::ReuseAs { source, body } => Term::ReuseAs { // pure structural recursion through both // children. Same pattern as `Term::Clone`. source: Box::new(self.desugar_term(source, scope)), body: Box::new(self.desugar_term(body, scope)), }, Term::Loop { binders, body } => { // loop-recur iter 1: structural recursion. Each // binder's init is desugared in scope of the outer env // plus already-declared binders; the body sees all // binders. The LetBound sentinel keeps generated fresh // names off binder names. let mut inner = scope.clone(); let new_binders: Vec = binders .iter() .map(|b| { let init = self.desugar_term(&b.init, &inner); inner.insert(b.name.clone(), ScopeEntry::LetBound); LoopBinder { name: b.name.clone(), ty: b.ty.clone(), init, } }) .collect(); Term::Loop { binders: new_binders, body: Box::new(self.desugar_term(body, &inner)), } } Term::Recur { args } => Term::Recur { args: args .iter() .map(|a| self.desugar_term(a, scope)) .collect(), }, Term::LetRec { name, ty, params, body, in_term } => { // lift to a synthetic top-level fn (no-capture). // extend the lift to the path-1 safe subset — // captures of fn-params / Lam-params (whose types are // known statically). // when ANY capture is `LetBound` the desugar // pass cannot resolve its type (the let-value's type is // only known after typecheck). For that case we LEAVE // the LetRec in place, with body and in_term recursively // desugared. A post-typecheck pass (`lift_letrecs` in // `ailang-check`) walks every surviving LetRec and lifts // it using the typechecker's resolved types. // extends the defer path to `MatchArm` // captures. The post-typecheck pass already walks // `Term::Match` arms and (since 16b.3) calls // `type_check_pattern_for_lift` to extend locals with // pattern bindings — that machinery resolves Match-arm // capture types via constructor-field substitution // against the matched ctor's declared field types. // // 16b.2's fast path (KnownType-only captures → lift here) // STILL fires when applicable. Any `LetBound` or // `MatchArm` capture forces the all-or-nothing defer // path. // // `EnclosingLetRec` captures STILL panic (16b.7 — // nested mutual recursion needs separate machinery). // // The non-callee-use check (16b.5 violation) runs FIRST // so the diagnostic fires consistently regardless of // which path the LetRec takes. // Peel `ty` to its inner Fn so we know the LetRec's // own param types (for body-scope) and so we can // detect a Forall LetRec early. let inner_params_tys: Vec = match peel_forall_to_fn(ty) { Some(Type::Fn { params: ps, .. }) => ps.clone(), _ => panic!( "Iter 16b.3: LetRec `{}` must have a Fn type (or Forall); got {:?}", name, ty ), }; if inner_params_tys.len() != params.len() { panic!( "Iter 16b.3: LetRec `{}` param count {} != type's param count {}", name, params.len(), inner_params_tys.len() ); } // Body's scope: outer ∪ {name → EnclosingLetRec} ∪ // params with their declared types as KnownType. let mut body_scope = scope.clone(); body_scope.insert(name.clone(), ScopeEntry::EnclosingLetRec); for (p, pty) in params.iter().zip(inner_params_tys.iter()) { body_scope.insert(p.clone(), ScopeEntry::KnownType(pty.clone())); } let desugared_body = self.desugar_term(body, &body_scope); // in_term's scope: outer ∪ {name → EnclosingLetRec} // (params are lambda-local to the LetRec's body, // not visible in `in`). let mut in_scope = scope.clone(); in_scope.insert(name.clone(), ScopeEntry::EnclosingLetRec); let desugared_in = self.desugar_term(in_term, &in_scope); // 16b.2: validate that `name` is only ever used as the // callee of a Term::App in `body` — never as a value. // Body-side name-as-value would require an eta-Lam inside // the body that references the LetRec's own pre-lift name; // that's a non-trivial extension (the Lam's body would need // to call the unlifted name, which by then is already // rewritten to the lifted callee — chicken-and-egg). Stays // rejected. // // 16b.5: in_term-side name-as-value is now SUPPORTED. We // detect it here but DO NOT panic; instead, the lift logic // below wraps `in_term'` in a `Let { f, lam(...), in_term' }` // whose lam eta-expands the lifted fn, supplying the // captures positionally. Bare-`f` references in `in_term'` // then resolve to the let-bound lam value. Callee-position // references in `in_term'` go directly to the lifted fn // (efficient; no closure indirection). if let Some(violation) = find_non_callee_use(&desugared_body, name) { panic!( "Iter 16b.5: LetRec `{}` appears as a value INSIDE its own body \ (not as the callee of `app`); name-as-value of a LetRec inside \ its own body is not yet supported. Offending term: {:?}", name, violation ); } let in_has_value_use = find_non_callee_use(&desugared_in, name).is_some(); // reject name-as-value in `in_term` when the // enclosing fn is polymorphic. The 16b.5 wrap synthesises // a `Term::Lam` whose AST has no `Forall` slot, so // wrapping a polymorphic lifted fn into a monomorphic Lam // would lose the type vars. Solving this needs closure // conversion with polymorphism (queue tag `closure-poly` // / informally `16b.5b`). Other 16b.5 use cases — name- // as-value in a MONOMORPHIC enclosing fn — continue to // work via the eta-Lam wrap. if in_has_value_use && !self.current_def_forall_vars.is_empty() { panic!( "Iter 16b.6: name-as-value of LetRec `{}` is not yet supported in \ a polymorphic enclosing fn — closure conversion with \ polymorphism would be required. Queued separately as \ `closure-poly` (informally 16b.5b).", name ); } // Capture detection (against the *outer* scope, before // the body-scope extension). let mut local_bound: BTreeSet = BTreeSet::new(); local_bound.insert(name.clone()); for p in params { local_bound.insert(p.clone()); } let mut frees: BTreeSet = BTreeSet::new(); free_vars_in_term(&desugared_body, &local_bound, &mut frees); let captures: Vec = frees .iter() .filter(|f| scope.contains_key(*f)) .cloned() .collect(); // 16b.4: classify each capture's ScopeEntry. // - All KnownType → lift here (16b.2 fast path). // - Any LetBound → defer to `lift_letrecs` (16b.3). // - Any MatchArm → defer to `lift_letrecs` (16b.4). // The post-typecheck pass resolves the binding's type // from the enclosing match's scrutinee type, with // constructor-field substitution against the matched // ctor's declared field types. // - Any EnclosingLetRec → panic (16b.7). // Mixed KnownType + (LetBound | MatchArm) get deferred: // any non-KnownType capture forces the all-or-nothing // defer path. The post-typecheck pass handles every // supported capture kind uniformly. let mut needs_defer = false; for c in &captures { match scope.get(c).expect("capture-in-scope-by-construction") { ScopeEntry::KnownType(_) => {} ScopeEntry::LetBound => { needs_defer = true; } ScopeEntry::MatchArm => { // 16b.4: defer instead of panic. `lift_letrecs` // resolves the type via `type_check_pattern_for_lift` // (added in 16b.3) which already substitutes the // matched ADT's type args into the ctor's // declared field types. needs_defer = true; } ScopeEntry::EnclosingLetRec => panic!( "Iter 16b.7: nested LetRec `{}` captures outer LetRec name \ `{}` — closure conversion of a LetRec inside its own body \ would be required (the capture's value is the outer LetRec's \ pre-lift fn-value, which has no callable form before the \ outer lift completes). Queued as `closure-of-self` (informally \ 16b.5-body / closure-poly).", name, c ), } } // 16b.3/16b.4: defer-arm. At least one capture is LetBound // or MatchArm-bound, so we can't resolve the lifted // signature here. Reconstruct the LetRec with desugared // sub-terms and let `ailang-check::lift_letrecs` handle // it after typecheck. if needs_defer { return Term::LetRec { name: name.clone(), ty: ty.clone(), params: params.clone(), body: Box::new(desugared_body), in_term: Box::new(desugared_in), }; } // 16b.2 fast path: every capture has KnownType — lift now. let capture_types: Vec<(String, Type)> = captures .iter() .map(|c| match scope.get(c).expect("classified-above") { ScopeEntry::KnownType(t) => (c.clone(), t.clone()), _ => unreachable!("non-KnownType filtered above"), }) .collect(); // Build augmented type: original Fn with capture types // appended to `params`. // // when the ENCLOSING fn is polymorphic // (`current_def_forall_vars` non-empty), wrap the // augmented Fn in a `Type::Forall` mirroring the // enclosing fn's type vars. The capture types may // mention any of those vars; that's fine — the Forall // binds them. At every call site of the LetRec name // inside the enclosing fn's body, codegen's Iter // 12b/14a monomorphisation specialises `f$lr_N` at the // same type args as the enclosing fn's current mono. // // The LetRec's own `ty` (`Type::Fn` — never `Forall`, // since LetRec itself doesn't quantify) is the inner // type. let inner_augmented_ty = match ty { Type::Fn { params: ps, ret, effects, .. } => { let mut new_ps = ps.clone(); for (_, t) in &capture_types { new_ps.push(t.clone()); } Type::Fn { params: new_ps, ret: ret.clone(), effects: effects.clone(), param_modes: vec![], ret_mode: ParamMode::Implicit, } } Type::Forall { .. } => panic!( "Iter 16b.6 invariant: LetRec `{}` has a Forall type at its \ own declaration; LetRec doesn't quantify, only the enclosing \ fn does", name ), other => panic!( "Iter 16b.3: LetRec `{}` has non-Fn/Forall type {:?}", name, other ), }; let augmented_ty = if self.current_def_forall_vars.is_empty() { inner_augmented_ty } else { Type::Forall { vars: self.current_def_forall_vars.clone(), constraints: vec![], body: Box::new(inner_augmented_ty), } }; // Augmented param-name list: original params + capture // names (using the captured variable names directly so // the lifted body's references already resolve // correctly). let mut augmented_params = params.clone(); for (cn, _) in &capture_types { augmented_params.push(cn.clone()); } // Lift. let lifted_name = self.fresh_lifted(name); // Rewrite (app name args) to (app lifted_name args... cap0 // cap1 ...) FIRST in body, then substitute name -> // lifted_name everywhere (handles non-call references, // but body name-as-value is rejected above so this is // a defensive belt-and-braces pass). let extras: Vec = capture_types.iter().map(|(n, _)| n.clone()).collect(); let body_call_rw = subst_call_with_extras(&desugared_body, name, &lifted_name, &extras); let body_full = subst_var(&body_call_rw, name, &lifted_name); // 16b.5: rewrite call sites in `in_term` to the lifted // name with captures appended. Do NOT run `subst_var` on // `in_term` if there's a name-as-value use — we want // those bare `Var{name}` references to resolve to the // eta-Lam binding we add below. If there is no // name-as-value use, we still skip subst_var (no leftover // bare references exist after subst_call_with_extras). let in_call_rw = subst_call_with_extras(&desugared_in, name, &lifted_name, &extras); // Effects on the LetRec's declared type — the eta-Lam // inherits them so its body can call the lifted fn (which // carries the same effects). let lr_effects: Vec = match peel_forall_to_fn(ty) { Some(Type::Fn { effects: es, .. }) => es.clone(), _ => vec![], }; // Original LetRec param types and ret type — the eta-Lam's // signature mirrors the LetRec's declared type so a value // bound by `(let f (lam ...) ...)` has type Fn(t1..tk) -> tr. let (orig_param_tys, orig_ret_ty): (Vec, Type) = match peel_forall_to_fn(ty) { Some(Type::Fn { params: ps, ret, .. }) => { (ps.clone(), (**ret).clone()) } _ => unreachable!("ty shape validated above"), }; let in_full = if in_has_value_use { // Build the eta-Lam: `(lam (params P1..Pk -> RT) [effects] // (app f$lr_N P1..Pk c1..cm))`. Param names reuse the // LetRec's original param names — fine because they // shadow only inside the Lam body. let lam_args: Vec = params .iter() .map(|p| Term::Var { name: p.clone() }) .chain(extras.iter().map(|c| Term::Var { name: c.clone() })) .collect(); let lam_body = Term::App { callee: Box::new(Term::Var { name: lifted_name.clone() }), args: lam_args, tail: false, }; let eta_lam = Term::Lam { params: params.clone(), param_tys: orig_param_tys, ret_ty: Box::new(orig_ret_ty), effects: lr_effects, body: Box::new(lam_body), }; Term::Let { name: name.clone(), value: Box::new(eta_lam), body: Box::new(in_call_rw), } } else { in_call_rw }; self.lifted.push(Def::Fn(FnDef { name: lifted_name, ty: augmented_ty, params: augmented_params, body: body_full, suppress: vec![], doc: None, export: None, })); in_full } Term::New { type_name, args } => Term::New { type_name: type_name.clone(), args: args .iter() .map(|arg| match arg { NewArg::Value(v) => NewArg::Value(self.desugar_term(v, scope)), NewArg::Type(t) => NewArg::Type(t.clone()), }) .collect(), }, } } /// Lowers a `match` whose children have already been desugared. /// /// If every arm is already flat ([`is_flat`] returns true), the /// match is reconstructed unchanged (no let-binding or chain /// allocation overhead). Otherwise the scrutinee is let-bound to /// a fresh `$mp_N` and the arms are translated to a chain of /// single-level matches over that variable. fn desugar_match(&mut self, scrutinee: Term, arms: Vec) -> Term { if arms.iter().all(|a| is_flat(&a.pat)) { return Term::Match { scrutinee: Box::new(scrutinee), arms, }; } let s = self.fresh(); let s_var = Term::Var { name: s.clone() }; // `default` is unreachable for valid programs (the // typechecker requires either a catch-all arm or exhaustive // ctor coverage). Use the polymorphic bottom builtin // `__unreachable__` (`forall a. a`) so the terminator unifies // against any arm result type without forcing a synthetic // `Unit`-typed `_` arm to dominate it. Codegen lowers the // var to LLVM `unreachable`. let default = Term::Var { name: "__unreachable__".into() }; let chain = self.build_chain(&s_var, &arms, &default); Term::Let { name: s, value: Box::new(scrutinee), body: Box::new(chain), } } /// Recursively builds a chain of single-arm matches with a shared /// fall-through. Empty arms ⇒ `default`; otherwise the first arm /// is desugared with the rest of the chain as its fall-through. fn build_chain(&mut self, s_var: &Term, arms: &[Arm], default: &Term) -> Term { if arms.is_empty() { return default.clone(); } let head = &arms[0]; let rest = &arms[1..]; let fall_k = self.build_chain(s_var, rest, default); self.desugar_one_arm(s_var, head, fall_k) } /// Lowers one arm into a term. Wild/Var arms drop the chain (the /// arm matches everything); Lit and Ctor arms emit a `Term::Match` /// with the desugared head pattern as the first arm and a /// wildcard fall-through to `fall_k`. fn desugar_one_arm(&mut self, s_var: &Term, arm: &Arm, fall_k: Term) -> Term { match &arm.pat { Pattern::Wild => arm.body.clone(), Pattern::Var { name } => Term::Let { name: name.clone(), value: Box::new(s_var.clone()), body: Box::new(arm.body.clone()), }, Pattern::Lit { lit } => { // a top-level lit arm desugars to `if (== s_var lit) // then arm.body else fall_k`. Eliminates `Pattern::Lit` from // the desugared output entirely; codegen never sees it. let cmp = build_eq(s_var.clone(), lit); Term::If { cond: Box::new(cmp), then: Box::new(arm.body.clone()), else_: Box::new(fall_k), } } Pattern::Ctor { ctor, fields } => { // Lift each field to a fresh var; build the flat outer // pattern, then walk right-to-left wrapping each inner // sub-pattern via `wrap_sub` so the deepest field is // matched first inside-out. let fresh_vars: Vec = fields.iter().map(|_| self.fresh()).collect(); let flat_fields: Vec = fresh_vars .iter() .map(|n| Pattern::Var { name: n.clone() }) .collect(); let mut inner = arm.body.clone(); for (sub, fv) in fields.iter().zip(fresh_vars.iter()).rev() { inner = self.wrap_sub(fv, sub, inner, &fall_k); } Term::Match { scrutinee: Box::new(s_var.clone()), arms: vec![ Arm { pat: Pattern::Ctor { ctor: ctor.clone(), fields: flat_fields, }, body: inner, }, Arm { pat: Pattern::Wild, body: fall_k, }, ], } } } } /// Wraps `body` so that it only runs when the value bound to `fv` /// matches `sub`. For `Var` / `Wild` the wrap is a let-bind / no-op; /// for nested `Ctor` / `Lit` the wrap is a recursive /// [`desugar_match`](Self::desugar_match) — that recursion is what /// flattens arbitrarily-nested patterns. fn wrap_sub(&mut self, fv: &str, sub: &Pattern, body: Term, fall_k: &Term) -> Term { match sub { Pattern::Wild => body, Pattern::Var { name } => Term::Let { name: name.clone(), value: Box::new(Term::Var { name: fv.to_string(), }), body: Box::new(body), }, Pattern::Lit { lit } => { // a lit sub-pattern desugars to `if (== fv lit) body // else fall_k`. Same shape as the top-level case, but on the // field-bound fresh variable rather than the original // scrutinee. let cmp = build_eq(Term::Var { name: fv.to_string() }, lit); Term::If { cond: Box::new(cmp), then: Box::new(body), else_: Box::new(fall_k.clone()), } } Pattern::Ctor { .. } => self.desugar_match( Term::Var { name: fv.to_string(), }, vec![ Arm { pat: sub.clone(), body, }, Arm { pat: Pattern::Wild, body: fall_k.clone(), }, ], ), } } } /// True iff `p` is fully shallow: a [`Pattern::Var`], [`Pattern::Wild`], /// or a [`Pattern::Ctor`] all of whose fields are `Var` or `Wild`. Used /// by [`Desugarer::desugar_match`] to skip the let-binding and chain /// construction for already-flat matches. /// /// [`Pattern::Lit`] is **not** flat — it always desugars to a /// [`Term::If`] via [`build_eq`], so it must take the chain path even /// when no other arm needs flattening. Otherwise the early-return in /// [`Desugarer::desugar_match`] would leak a `Pattern::Lit` arm to /// typecheck/codegen, which the codegen rejects with an internal error. fn is_flat(p: &Pattern) -> bool { match p { Pattern::Wild | Pattern::Var { .. } => true, // lit patterns are not flat; they desugar to If. Pattern::Lit { .. } => false, Pattern::Ctor { fields, .. } => fields .iter() .all(|f| matches!(f, Pattern::Var { .. } | Pattern::Wild)), } } /// Build an equality-test term for a lit pattern. The shape is /// Builds the equality-test AST node for a literal pattern. Lowers /// `(pat-lit )` to `(if (eq sv ) )`. The /// emitted `eq` resolves via prelude.Eq's class-method dispatch /// (per design/contracts/0016-method-dispatch.md) — Int/Bool/Str/Unit /// patterns all route through the corresponding primitive instance. /// /// Float-Literal patterns are hard-rejected at typecheck /// (`CheckError::FloatPatternNotAllowed`, per /// design/contracts/0005-float-semantics.md) before this fn is reached, /// so the `eq`-dispatch never encounters Float at runtime; the /// Float arm of the match below remains for AST-level totality. /// /// Unit literals are degenerate — every Unit value is equal — so an /// emitted `true` literal stands in (no `eq` call needed). fn build_eq(scrutinee: Term, lit: &Literal) -> Term { match lit { Literal::Unit => Term::Lit { lit: Literal::Bool { value: true }, }, Literal::Int { .. } | Literal::Bool { .. } | Literal::Str { .. } | Literal::Float { .. } => Term::App { callee: Box::new(Term::Var { name: "eq".to_string(), }), args: vec![scrutinee, Term::Lit { lit: lit.clone() }], tail: false, }, } } /// collect free variable names of `t`, with `bound` being /// the set of names lexically bound at this point. A /// [`Term::Var`] `{ name }` is "free" iff `name` is not in `bound`. /// Compound binders ([`Term::Let`], [`Term::Lam`], [`Term::Match`] /// arms, [`Term::LetRec`]) extend `bound` for the sub-walk. /// /// Used by the [`Term::LetRec`] desugar to decide whether a local /// recursive let would capture any name from the enclosing scope. /// /// made `pub` so the post-typecheck `lift_letrecs` pass /// in `ailang-check` can reuse it (same free-var computation; the /// post-typecheck pass needs it to recompute captures of LetRec /// nodes that desugar deferred). pub fn free_vars_in_term(t: &Term, bound: &BTreeSet, out: &mut BTreeSet) { match t { Term::Lit { .. } => {} Term::Var { name } => { if !bound.contains(name) { out.insert(name.clone()); } } Term::App { callee, args, .. } => { free_vars_in_term(callee, bound, out); for a in args { free_vars_in_term(a, bound, out); } } Term::Let { name, value, body } => { free_vars_in_term(value, bound, out); let mut b = bound.clone(); b.insert(name.clone()); free_vars_in_term(body, &b, out); } Term::If { cond, then, else_ } => { free_vars_in_term(cond, bound, out); free_vars_in_term(then, bound, out); free_vars_in_term(else_, bound, out); } Term::Do { args, .. } => { for a in args { free_vars_in_term(a, bound, out); } } Term::Ctor { args, .. } => { for a in args { free_vars_in_term(a, bound, out); } } Term::Match { scrutinee, arms } => { free_vars_in_term(scrutinee, bound, out); for arm in arms { let mut b = bound.clone(); pattern_binds(&arm.pat, &mut b); free_vars_in_term(&arm.body, &b, out); } } Term::Lam { params, body, .. } => { let mut b = bound.clone(); for p in params { b.insert(p.clone()); } free_vars_in_term(body, &b, out); } Term::Seq { lhs, rhs } => { free_vars_in_term(lhs, bound, out); free_vars_in_term(rhs, bound, out); } Term::LetRec { name, params, body, in_term, .. } => { // Inside body: name + params are bound. let mut b_body = bound.clone(); b_body.insert(name.clone()); for p in params { b_body.insert(p.clone()); } free_vars_in_term(body, &b_body, out); // Inside in_term: only name is bound. let mut b_in = bound.clone(); b_in.insert(name.clone()); free_vars_in_term(in_term, &b_in, out); } Term::Clone { value } => { // `(clone X)` has the same free vars as `X`. free_vars_in_term(value, bound, out); } Term::ReuseAs { source, body } => { // free vars are the union of source and body. free_vars_in_term(source, bound, out); free_vars_in_term(body, bound, out); } Term::Loop { binders, body } => { // loop-recur iter 1: binder names bind inside the loop — // each init sees the outer env plus already-declared // binders; the body sees all. let mut b = bound.clone(); for bd in binders { free_vars_in_term(&bd.init, &b, out); b.insert(bd.name.clone()); } free_vars_in_term(body, &b, out); } Term::Recur { args } => { for a in args { free_vars_in_term(a, bound, out); } } Term::New { args, .. } => { for arg in args { match arg { NewArg::Value(v) => free_vars_in_term(v, bound, out), NewArg::Type(_) => {} } } } } } /// collect every name a pattern binds into `out`. Mirrors /// `Pattern::pattern_bound_names` in `ailang-codegen`; duplicated here /// because `ailang-core` cannot depend on the codegen crate. /// /// made `pub` so callers (e.g. `ailang-check::lift_letrecs`) /// can reproduce the same shadowing semantics during their own /// scope-aware walks. pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet) { match p { Pattern::Wild | Pattern::Lit { .. } => {} Pattern::Var { name } => { out.insert(name.clone()); } Pattern::Ctor { fields, .. } => { for sub in fields { pattern_binds(sub, out); } } } } /// rewrite every free [`Term::Var`] `{ name == from }` /// reference in `t` to [`Term::Var`] `{ name = to }`. Respects /// shadowing: if a binder rebinds `from`, occurrences inside that /// binder's scope stop being free and are left alone. /// /// Used after lifting a [`Term::LetRec`] body to a synthetic top-level /// fn — every recursive self-call site in the lifted body and every /// reference in the in-term needs to point at the new global name. /// /// made `pub` for the same reason as /// [`subst_call_with_extras`]. pub fn subst_var(t: &Term, from: &str, to: &str) -> Term { match t { Term::Lit { .. } => t.clone(), Term::Var { name } => { if name == from { Term::Var { name: to.to_string() } } else { t.clone() } } Term::App { callee, args, tail } => Term::App { callee: Box::new(subst_var(callee, from, to)), args: args.iter().map(|a| subst_var(a, from, to)).collect(), tail: *tail, }, Term::Let { name, value, body } => { let v = subst_var(value, from, to); // If this `let` rebinds `from`, leave its body alone. let b = if name == from { (**body).clone() } else { subst_var(body, from, to) }; Term::Let { name: name.clone(), value: Box::new(v), body: Box::new(b), } } Term::If { cond, then, else_ } => Term::If { cond: Box::new(subst_var(cond, from, to)), then: Box::new(subst_var(then, from, to)), else_: Box::new(subst_var(else_, from, to)), }, Term::Do { op, args, tail } => Term::Do { op: op.clone(), args: args.iter().map(|a| subst_var(a, from, to)).collect(), tail: *tail, }, Term::Ctor { type_name, ctor, args } => Term::Ctor { type_name: type_name.clone(), ctor: ctor.clone(), args: args.iter().map(|a| subst_var(a, from, to)).collect(), }, Term::Match { scrutinee, arms } => Term::Match { scrutinee: Box::new(subst_var(scrutinee, from, to)), arms: arms .iter() .map(|a| { // Pattern-bound vars shadow `from` inside the arm. let mut binds: BTreeSet = BTreeSet::new(); pattern_binds(&a.pat, &mut binds); let body = if binds.contains(from) { a.body.clone() } else { subst_var(&a.body, from, to) }; Arm { pat: a.pat.clone(), body } }) .collect(), }, Term::Lam { params, param_tys, ret_ty, effects, body } => { let body = if params.iter().any(|p| p == from) { (**body).clone() } else { subst_var(body, from, to) }; Term::Lam { params: params.clone(), param_tys: param_tys.clone(), ret_ty: ret_ty.clone(), effects: effects.clone(), body: Box::new(body), } } Term::Seq { lhs, rhs } => Term::Seq { lhs: Box::new(subst_var(lhs, from, to)), rhs: Box::new(subst_var(rhs, from, to)), }, Term::LetRec { name, ty, params, body, in_term } => { let body_shadowed = name == from || params.iter().any(|p| p == from); let body_rw = if body_shadowed { (**body).clone() } else { subst_var(body, from, to) }; let in_shadowed = name == from; let in_rw = if in_shadowed { (**in_term).clone() } else { subst_var(in_term, from, to) }; Term::LetRec { name: name.clone(), ty: ty.clone(), params: params.clone(), body: Box::new(body_rw), in_term: Box::new(in_rw), } } Term::Clone { value } => Term::Clone { // structural recursion through the wrapper. value: Box::new(subst_var(value, from, to)), }, Term::ReuseAs { source, body } => Term::ReuseAs { // structural recursion through both children. source: Box::new(subst_var(source, from, to)), body: Box::new(subst_var(body, from, to)), }, Term::Loop { binders, body } => { // loop-recur iter 1: binders lexically shadow outer // names — once a binder named `from` is declared, later // inits and the body stop being substituted. let mut shadowed = false; let new_binders: Vec = binders .iter() .map(|b| { let init = if shadowed { b.init.clone() } else { subst_var(&b.init, from, to) }; if b.name == from { shadowed = true; } LoopBinder { name: b.name.clone(), ty: b.ty.clone(), init, } }) .collect(); let body_rw = if shadowed { (**body).clone() } else { subst_var(body, from, to) }; Term::Loop { binders: new_binders, body: Box::new(body_rw), } } Term::Recur { args } => Term::Recur { args: args.iter().map(|a| subst_var(a, from, to)).collect(), }, Term::New { type_name, args } => Term::New { type_name: type_name.clone(), args: args .iter() .map(|arg| match arg { NewArg::Value(v) => NewArg::Value(subst_var(v, from, to)), NewArg::Type(t) => NewArg::Type(t.clone()), }) .collect(), }, } } /// peel a `Type::Forall { body: Type::Fn { ... } }` to /// expose the inner `Type::Fn`. Returns `Some(&Type::Fn)` if the /// type is `Fn` or `Forall`; `None` otherwise. fn peel_forall_to_fn(t: &Type) -> Option<&Type> { match t { Type::Fn { .. } => Some(t), Type::Forall { body, .. } => peel_forall_to_fn(body), _ => None, } } /// walk `t` and rewrite every `Term::App { callee = /// Var{name}, args }` to `Term::App { callee = Var{lifted}, args = /// args ++ extras_as_vars }`. Recurses into all sub-terms. Does /// not touch `Term::Var { name }` in non-callee positions — that's /// flagged separately by [`find_non_callee_use`]. /// /// made `pub` so the post-typecheck `lift_letrecs` pass /// in `ailang-check` can reuse it (same call-site rewrite shape; the /// only difference is that the capture types come from the /// typechecker's env rather than being statically known). pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[String]) -> Term { match t { Term::Lit { .. } => t.clone(), Term::Var { .. } => t.clone(), Term::App { callee, args, tail } => { let mut new_args: Vec = args .iter() .map(|a| subst_call_with_extras(a, name, lifted, extras)) .collect(); let new_callee = match callee.as_ref() { Term::Var { name: n } if n == name => { // Append extras as Var references — at this site, // the captured variable names are still in scope // because the lifted fn appends them as extra // params with the same names. for ex in extras { new_args.push(Term::Var { name: ex.clone() }); } Box::new(Term::Var { name: lifted.to_string() }) } _ => Box::new(subst_call_with_extras(callee, name, lifted, extras)), }; Term::App { callee: new_callee, args: new_args, tail: *tail, } } Term::Let { name: n, value, body } => Term::Let { name: n.clone(), value: Box::new(subst_call_with_extras(value, name, lifted, extras)), body: Box::new(subst_call_with_extras(body, name, lifted, extras)), }, Term::If { cond, then, else_ } => Term::If { cond: Box::new(subst_call_with_extras(cond, name, lifted, extras)), then: Box::new(subst_call_with_extras(then, name, lifted, extras)), else_: Box::new(subst_call_with_extras(else_, name, lifted, extras)), }, Term::Do { op, args, tail } => Term::Do { op: op.clone(), args: args .iter() .map(|a| subst_call_with_extras(a, name, lifted, extras)) .collect(), tail: *tail, }, Term::Ctor { type_name, ctor, args } => Term::Ctor { type_name: type_name.clone(), ctor: ctor.clone(), args: args .iter() .map(|a| subst_call_with_extras(a, name, lifted, extras)) .collect(), }, Term::Match { scrutinee, arms } => Term::Match { scrutinee: Box::new(subst_call_with_extras(scrutinee, name, lifted, extras)), arms: arms .iter() .map(|a| Arm { pat: a.pat.clone(), body: subst_call_with_extras(&a.body, name, lifted, extras), }) .collect(), }, Term::Lam { params, param_tys, ret_ty, effects, body, } => Term::Lam { params: params.clone(), param_tys: param_tys.clone(), ret_ty: ret_ty.clone(), effects: effects.clone(), body: Box::new(subst_call_with_extras(body, name, lifted, extras)), }, Term::Seq { lhs, rhs } => Term::Seq { lhs: Box::new(subst_call_with_extras(lhs, name, lifted, extras)), rhs: Box::new(subst_call_with_extras(rhs, name, lifted, extras)), }, Term::LetRec { name: n, ty, params, body, in_term } => Term::LetRec { name: n.clone(), ty: ty.clone(), params: params.clone(), body: Box::new(subst_call_with_extras(body, name, lifted, extras)), in_term: Box::new(subst_call_with_extras(in_term, name, lifted, extras)), }, Term::Clone { value } => Term::Clone { // structural recursion through the wrapper. value: Box::new(subst_call_with_extras(value, name, lifted, extras)), }, Term::ReuseAs { source, body } => Term::ReuseAs { // structural recursion through both children. source: Box::new(subst_call_with_extras(source, name, lifted, extras)), body: Box::new(subst_call_with_extras(body, name, lifted, extras)), }, Term::Loop { binders, body } => Term::Loop { binders: binders .iter() .map(|b| LoopBinder { name: b.name.clone(), ty: b.ty.clone(), init: subst_call_with_extras(&b.init, name, lifted, extras), }) .collect(), body: Box::new(subst_call_with_extras(body, name, lifted, extras)), }, Term::Recur { args } => Term::Recur { args: args .iter() .map(|a| subst_call_with_extras(a, name, lifted, extras)) .collect(), }, Term::New { type_name, args } => Term::New { type_name: type_name.clone(), args: args .iter() .map(|arg| match arg { NewArg::Value(v) => { NewArg::Value(subst_call_with_extras(v, name, lifted, extras)) } NewArg::Type(t) => NewArg::Type(t.clone()), }) .collect(), }, } } /// returns `Some(t)` if `t` contains a `Term::Var { /// name }` reference in a position that is **not** the callee of a /// `Term::App`. Returns `None` if every reference is in callee /// position. Used by the LetRec lifter to enforce the "direct-call /// only" restriction in 16b.2. /// /// made `pub` for the same reason as /// [`subst_call_with_extras`]. pub fn find_non_callee_use(t: &Term, name: &str) -> Option { match t { Term::Lit { .. } => None, Term::Var { name: n } if n == name => Some(t.clone()), Term::Var { .. } => None, Term::App { callee, args, .. } => { // Allowed: callee = Var{name}. Args are checked normally. // Disallowed: callee != Var{name} but recursing into the // callee surfaces the name as a non-callee position. if !matches!(callee.as_ref(), Term::Var { name: n } if n == name) { if let Some(v) = find_non_callee_use(callee, name) { return Some(v); } } for a in args { if let Some(v) = find_non_callee_use(a, name) { return Some(v); } } None } Term::Let { value, body, .. } => { find_non_callee_use(value, name).or_else(|| find_non_callee_use(body, name)) } Term::If { cond, then, else_ } => find_non_callee_use(cond, name) .or_else(|| find_non_callee_use(then, name)) .or_else(|| find_non_callee_use(else_, name)), Term::Do { args, .. } => args.iter().find_map(|a| find_non_callee_use(a, name)), Term::Ctor { args, .. } => args.iter().find_map(|a| find_non_callee_use(a, name)), Term::Match { scrutinee, arms } => find_non_callee_use(scrutinee, name) .or_else(|| arms.iter().find_map(|a| find_non_callee_use(&a.body, name))), Term::Lam { body, .. } => find_non_callee_use(body, name), Term::Seq { lhs, rhs } => { find_non_callee_use(lhs, name).or_else(|| find_non_callee_use(rhs, name)) } Term::LetRec { body, in_term, .. } => find_non_callee_use(body, name) .or_else(|| find_non_callee_use(in_term, name)), // clone is identity for the non-callee scan. Term::Clone { value } => find_non_callee_use(value, name), // scan both source and body. Term::ReuseAs { source, body } => { find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name)) } Term::Loop { binders, body } => binders .iter() .find_map(|b| find_non_callee_use(&b.init, name)) .or_else(|| find_non_callee_use(body, name)), Term::Recur { args } => { args.iter().find_map(|a| find_non_callee_use(a, name)) } Term::New { args, .. } => args.iter().find_map(|arg| match arg { NewArg::Value(v) => find_non_callee_use(v, name), NewArg::Type(_) => None, }), } } #[cfg(test)] mod tests { use super::*; /// Helper: walk a term, return true iff any [`Pattern::Ctor`] in /// any reachable [`Term::Match`] has a non-flat field. fn any_nested_ctor(t: &Term) -> bool { match t { Term::Lit { .. } | Term::Var { .. } => false, Term::App { callee, args, .. } => { any_nested_ctor(callee) || args.iter().any(any_nested_ctor) } Term::Let { value, body, .. } => any_nested_ctor(value) || any_nested_ctor(body), Term::If { cond, then, else_ } => { any_nested_ctor(cond) || any_nested_ctor(then) || any_nested_ctor(else_) } Term::Do { args, .. } => args.iter().any(any_nested_ctor), Term::Ctor { args, .. } => args.iter().any(any_nested_ctor), Term::Match { scrutinee, arms } => { if any_nested_ctor(scrutinee) { return true; } for a in arms { if !is_flat(&a.pat) { return true; } if any_nested_ctor(&a.body) { return true; } } false } Term::Lam { body, .. } => any_nested_ctor(body), Term::Seq { lhs, rhs } => any_nested_ctor(lhs) || any_nested_ctor(rhs), Term::LetRec { body, in_term, .. } => { any_nested_ctor(body) || any_nested_ctor(in_term) } Term::Clone { value } => any_nested_ctor(value), Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body), Term::Loop { binders, body } => { binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body) } Term::Recur { args } => args.iter().any(any_nested_ctor), Term::New { args, .. } => args.iter().any(|arg| match arg { NewArg::Value(v) => any_nested_ctor(v), NewArg::Type(_) => false, }), } } /// Helper: walk a term, return true iff any [`Term::LetRec`] is /// reachable. After 16b.1 desugaring this should be `false`. fn any_let_rec(t: &Term) -> bool { match t { Term::Lit { .. } | Term::Var { .. } => false, Term::App { callee, args, .. } => { any_let_rec(callee) || args.iter().any(any_let_rec) } Term::Let { value, body, .. } => any_let_rec(value) || any_let_rec(body), Term::If { cond, then, else_ } => { any_let_rec(cond) || any_let_rec(then) || any_let_rec(else_) } Term::Do { args, .. } => args.iter().any(any_let_rec), Term::Ctor { args, .. } => args.iter().any(any_let_rec), Term::Match { scrutinee, arms } => { any_let_rec(scrutinee) || arms.iter().any(|a| any_let_rec(&a.body)) } Term::Lam { body, .. } => any_let_rec(body), Term::Seq { lhs, rhs } => any_let_rec(lhs) || any_let_rec(rhs), Term::LetRec { .. } => true, Term::Clone { value } => any_let_rec(value), Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body), Term::Loop { binders, body } => { binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body) } Term::Recur { args } => args.iter().any(any_let_rec), Term::New { args, .. } => args.iter().any(|arg| match arg { NewArg::Value(v) => any_let_rec(v), NewArg::Type(_) => false, }), } } /// a `match` containing `(Cons a (Cons b _))` desugars to /// a tree with no nested ctor sub-patterns. Property: the rewriter's /// output never has a `Pattern::Ctor` whose field is itself a /// `Pattern::Ctor`. #[test] fn nested_cons_cons_flattens() { // Build: match xs of // Cons a (Cons b _) -> ... // _ -> ... let body_match = Term::Match { scrutinee: Box::new(Term::Var { name: "xs".into() }), arms: vec![ Arm { pat: Pattern::Ctor { ctor: "Cons".into(), fields: vec![ Pattern::Var { name: "a".into() }, Pattern::Ctor { ctor: "Cons".into(), fields: vec![ Pattern::Var { name: "b".into() }, Pattern::Wild, ], }, ], }, body: Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "a".into() }, Term::Var { name: "b".into() }, ], tail: false, }, }, Arm { pat: Pattern::Wild, body: Term::Lit { lit: Literal::Int { value: 0 } }, }, ], }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["xs".into()], body: body_match, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); let body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; assert!( !any_nested_ctor(body), "desugarer must remove every nested-ctor sub-pattern; got: {body:#?}" ); } /// A flat-only match round-trips structurally: there should be no /// extra let-binding or chain wrapping. #[test] fn flat_match_is_unchanged_shape() { let original = Term::Match { scrutinee: Box::new(Term::Var { name: "xs".into() }), arms: vec![ Arm { pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![], }, body: Term::Lit { lit: Literal::Int { value: 0 } }, }, Arm { pat: Pattern::Ctor { ctor: "Cons".into(), fields: vec![ Pattern::Var { name: "h".into() }, Pattern::Var { name: "t".into() }, ], }, body: Term::Var { name: "h".into() }, }, ], }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["xs".into()], body: original.clone(), suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); let body = match &out.defs[0] { Def::Fn(f) => f.body.clone(), _ => unreachable!(), }; // Already-flat match must round-trip with the same Match shape // at the top level (no Let-wrap). assert!(matches!(body, Term::Match { .. })); } /// a [`Term::LetRec`] whose body has no captures from /// the enclosing scope is lifted to a synthetic top-level fn. The /// resulting module's `defs` grows by one and the original LetRec /// is gone from the term tree. fn fact_letrec_term() -> Term { // (let-rec fact (params n) (type ...) (body ...) (in (app fact 5))) // Body just returns `n` so the test doesn't depend on `<=`/etc. Term::LetRec { name: "fact".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], body: Box::new(Term::Var { name: "n".into() }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "fact".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 5 } }], tail: false, }), } } #[test] fn let_rec_no_capture_lifts_to_top_level() { let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: fact_letrec_term(), suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); // One original + one lifted = two defs. assert_eq!(out.defs.len(), 2, "expected one lifted fn appended"); // The new def is a fn whose name starts with the LetRec hint. let lifted = match &out.defs[1] { Def::Fn(f) => f, _ => panic!("expected a lifted FnDef"), }; assert!( lifted.name.starts_with("fact$lr_"), "lifted name `{}` should start with `fact$lr_`", lifted.name ); assert_eq!(lifted.params, vec!["n".to_string()]); // The original main's body must no longer contain a LetRec. let main_body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; assert!( !any_let_rec(main_body), "desugarer must remove every Term::LetRec from the term tree; got: {main_body:#?}" ); // The in-term of the LetRec was `(app fact 5)`; after the lift // it should be `(app 5)`. match main_body { Term::App { callee, .. } => match callee.as_ref() { Term::Var { name } => assert_eq!(name, &lifted.name), other => panic!("expected lifted Var as callee, got {other:?}"), }, other => panic!("expected App in main body, got {other:?}"), } } /// a LetRec whose body captures a fn-param of the /// enclosing scope (here, `outer` is a parameter of `main`) is /// **lifted** with the capture appended to the lifted fn's /// signature. The original 16b.1 panic ("would capture") is /// gone for fn-param captures; this test was renamed and /// repurposed to assert the lift succeeds and produces the /// expected augmented signature + call-site rewrite. #[test] fn let_rec_capture_fn_param_lifts_with_extra_arg() { let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "outer".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], // (let-rec helper (params x) (type Int -> Int) // (body (app + x n)) -- captures `n` // (in (app helper 5))) body: Term::LetRec { name: "helper".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "x".into() }, Term::Var { name: "n".into() }, ], tail: false, }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "helper".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 5 } }], tail: false, }), }, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); // (a) two defs: original outer + lifted helper. assert_eq!(out.defs.len(), 2, "expected one lifted fn appended"); let lifted = match &out.defs[1] { Def::Fn(f) => f, _ => panic!("expected lifted FnDef"), }; // (b) lifted ty has the capture type appended. assert_eq!( lifted.ty, Type::Fn { params: vec![Type::int(), Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, "lifted fn type should have capture appended; got {:?}", lifted.ty ); // (b') param-name list has the capture name appended. assert_eq!(lifted.params, vec!["x".to_string(), "n".to_string()]); // (c) the lifted body is `(app + x n)` — `n` is now a // fn-param of the lifted def, so the body is unchanged. match &lifted.body { Term::App { callee, args, .. } => { match callee.as_ref() { Term::Var { name } => assert_eq!(name, "+"), other => panic!("expected `+` callee, got {other:?}"), } assert_eq!(args.len(), 2); match &args[0] { Term::Var { name } => assert_eq!(name, "x"), other => panic!("expected x, got {other:?}"), } match &args[1] { Term::Var { name } => assert_eq!(name, "n"), other => panic!("expected n, got {other:?}"), } } other => panic!("expected lifted body to be App, got {other:?}"), } // (d) `outer`'s body is now `(app helper$lr_0 5 n)` — // call-site got the capture appended. let outer_body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; match outer_body { Term::App { callee, args, .. } => { match callee.as_ref() { Term::Var { name } => assert_eq!(name, &lifted.name), other => panic!("expected lifted callee, got {other:?}"), } assert_eq!(args.len(), 2, "expected 2 args (1 original + 1 capture)"); match &args[0] { Term::Lit { lit: Literal::Int { value: 5 } } => {} other => panic!("expected 5, got {other:?}"), } match &args[1] { Term::Var { name } => assert_eq!(name, "n"), other => panic!("expected n, got {other:?}"), } } other => panic!("expected outer body to be App, got {other:?}"), } } /// a LetRec whose body captures a `Term::Let`-bound /// name is no longer rejected at desugar time. Instead the /// desugar pass leaves the LetRec in place (with body and /// in_term recursively desugared) so the post-typecheck pass in /// `ailang-check` can lift it using the typechecker's resolved /// types. The 16b.1/16b.2 panic for this shape is gone. #[test] fn let_rec_capture_let_binding_is_deferred_to_post_typecheck() { // (let y 7 in (let-rec helper (params x) ... (body (app + x y)) // (in (app helper 1)))) let letrec = Term::LetRec { name: "helper".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "x".into() }, Term::Var { name: "y".into() }, ], tail: false, }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "helper".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: Term::Let { name: "y".into(), value: Box::new(Term::Lit { lit: Literal::Int { value: 7 } }), body: Box::new(letrec), }, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); // No fn was lifted (the LetRec is left in place) — the // module's defs count is unchanged. assert_eq!( out.defs.len(), 1, "expected no lifted fn (LetRec deferred); got {:?}", out.defs.iter().map(|d| d.name()).collect::>() ); // The original LetRec must STILL be present somewhere in // main's body. let main_body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; assert!( any_let_rec(main_body), "expected Term::LetRec to still be present in body; got {main_body:#?}" ); } /// a LetRec whose body captures a name bound by a /// `Pattern::Ctor` Var sub-pattern (a `ScopeEntry::MatchArm` /// binding) is no longer rejected at desugar time. The desugar /// pass leaves the LetRec in place (with body and in_term /// recursively desugared) so the post-typecheck pass in /// `ailang-check` can lift it using the typechecker's resolved /// types — including constructor-field substitution against /// the matched ADT's type args. The 16b.3-era panic for this /// shape is gone. #[test] fn let_rec_capture_match_arm_is_deferred_to_post_typecheck() { // Module-level scope: // (data Pair (vars a b) (ctor MkPair a b)) // (fn outer : (Pair Int Int) -> Int = \p. // match p // case (pat-ctor MkPair x y) -> // let-rec helper : (Int) -> Int = \z. (+ z x) // in (app helper 0) // ) let pair_td = TypeDef { name: "Pair".into(), vars: vec!["a".into(), "b".into()], ctors: vec![Ctor { name: "MkPair".into(), fields: vec![ Type::Var { name: "a".into() }, Type::Var { name: "b".into() }, ], }], doc: None, drop_iterative: false, param_in: BTreeMap::new(), }; let letrec = Term::LetRec { name: "helper".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["z".into()], body: Box::new(Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "z".into() }, Term::Var { name: "x".into() }, ], tail: false, }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "helper".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 0 } }], tail: false, }), }; let outer_body = Term::Match { scrutinee: Box::new(Term::Var { name: "p".into() }), arms: vec![Arm { pat: Pattern::Ctor { ctor: "MkPair".into(), fields: vec![ Pattern::Var { name: "x".into() }, Pattern::Var { name: "y".into() }, ], }, body: letrec, }], }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![ Def::Type(pair_td), Def::Fn(FnDef { name: "outer".into(), ty: Type::Fn { params: vec![Type::Con { name: "Pair".into(), args: vec![Type::int(), Type::int()], }], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["p".into()], body: outer_body, suppress: vec![], doc: None, export: None, }), ], }; let out = desugar_module(&m); // No fn was lifted (the LetRec is deferred) — the module's // defs count is unchanged at 2 (Pair + outer). assert_eq!( out.defs.len(), 2, "expected no lifted fn (LetRec deferred); got {:?}", out.defs.iter().map(|d| d.name()).collect::>() ); // The original LetRec must STILL be present somewhere in // outer's body (after match desugar — the match arm has only // `Var` sub-patterns so it stays flat and the body is reachable). let outer_body_out = match &out.defs[1] { Def::Fn(f) => &f.body, _ => unreachable!(), }; assert!( any_let_rec(outer_body_out), "expected Term::LetRec to still be present in body; got {outer_body_out:#?}" ); } /// name-as-value INSIDE the LetRec's own body is /// still rejected (would require an eta-Lam that calls the /// pre-lift name — chicken-and-egg with the call-site rewrite). /// Here `(let g f ...)` binds the LetRec name `f` to a let-bound /// name `g`, which is a value position — not a callee. #[test] #[should_panic(expected = "16b.5")] fn let_rec_name_as_value_in_body_panics() { // (let-rec f (params x) (type Int->Int) // (body (let g f (app g x))) -- f used as a value INSIDE body // (in (app f 1))) let letrec = Term::LetRec { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::Let { name: "g".into(), value: Box::new(Term::Var { name: "f".into() }), body: Box::new(Term::App { callee: Box::new(Term::Var { name: "g".into() }), args: vec![Term::Var { name: "x".into() }], tail: false, }), }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "f".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: letrec, suppress: vec![], doc: None, export: None, })], }; let _ = desugar_module(&m); } /// name-as-value in the in-clause is now SUPPORTED. /// The desugar pass lifts the LetRec to a synthetic top-level fn /// AND wraps the rewritten in-term in `(let f (lam ...) ...)` /// whose lam eta-expands the lifted fn. After desugar, the /// surviving term tree contains: /// - one lifted fn `f$lr_N` /// - inside `main`, a `Term::Let { name: "f", value: Term::Lam, /// body: }` where the Lam's body calls /// `f$lr_N` positionally. #[test] fn let_rec_name_as_value_in_in_term_wraps_to_eta_lam() { // (let-rec f (params x) (type Int->Int) // (body (app f x)) -- only callee uses (legal) // (in (let g f (app g 1)))) -- f used as a value let letrec = Term::LetRec { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["x".into()], body: Box::new(Term::App { callee: Box::new(Term::Var { name: "f".into() }), args: vec![Term::Var { name: "x".into() }], tail: false, }), in_term: Box::new(Term::Let { name: "g".into(), value: Box::new(Term::Var { name: "f".into() }), body: Box::new(Term::App { callee: Box::new(Term::Var { name: "g".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: letrec, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); // Two defs: original `main` + lifted `f$lr_0`. assert_eq!( out.defs.len(), 2, "expected one lifted fn appended; got {:?}", out.defs.iter().map(|d| d.name()).collect::>() ); let lifted_name = out.defs[1].name().to_string(); assert!( lifted_name.starts_with("f$lr_"), "unexpected lifted-name shape: {lifted_name}" ); // main's body must be wrapped in `Let { name: "f", value: Lam, ... }`. let main_body = match &out.defs[0] { Def::Fn(fd) => &fd.body, _ => unreachable!(), }; match main_body { Term::Let { name, value, .. } => { assert_eq!(name, "f", "wrap name must be the original LetRec name"); match value.as_ref() { Term::Lam { params, body, .. } => { assert_eq!(params, &vec!["x".to_string()]); // The lam's body is `(app f$lr_0 x)` (no captures // here, so just original-params). match body.as_ref() { Term::App { callee, args, .. } => { match callee.as_ref() { Term::Var { name: cn } => { assert_eq!(cn, &lifted_name); } other => panic!("expected callee Var, got {other:?}"), } assert_eq!(args.len(), 1, "no captures expected"); } other => panic!("expected lam body App, got {other:?}"), } } other => panic!("expected wrap value Lam, got {other:?}"), } } other => panic!("expected wrap Let at main body, got {other:?}"), } } /// a LetRec inside a polymorphic enclosing fn gets its /// fn-param captures lifted with a `Forall(vars, Fn(...))` augmented /// signature, mirroring the enclosing fn's type vars. Without 16b.6, /// the desugar pass would have classified the fn-params as /// `LetBound` (16b.2 fallback) and deferred to `lift_letrecs`; with /// 16b.6, the fn-params are `KnownType` and the fast-path lift fires /// here, producing a `Forall`-typed synthetic FnDef. #[test] fn let_rec_capture_under_polymorphic_enclosing_fn_lifts_to_forall_fn() { // (fn id_n_times : Forall(a). Fn(Int, a) -> a // (params n x) // (body // (let-rec loop : Fn(Int, a) -> a (params k acc) // (body (if (== k 0) acc (app loop (- k 1) acc))) // (in (app loop n x))))) // Captures: none (all references inside the body are to params // of `loop` itself or top-level builtins). To force a capture // of an `a`-typed param, use the simpler shape: // // (fn rec_id : Forall(a). Fn(a) -> a // (params x) // (body // (let-rec loop : Fn(Int) -> a (params k) // (body (if (== k 0) x (app loop (- k 1)))) // (in (app loop 1))))) // // Here `loop` captures `x: a` from the enclosing fn's params. // Without 16b.6, `x` would be ScopeEntry::LetBound (because the // enclosing fn is Forall) and the LetRec would defer; with // 16b.6, `x` is KnownType and the lift fires here. let letrec = Term::LetRec { name: "loop".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["k".into()], // 2026-05-21 operator-routing-eq-ord: keep `==` here (vs // migrating to `eq`) because this AST literal sits in an // in-source mod test for desugar with no prelude // injection; `eq` would be unbound. After Task 7 deletes // `==`, this test gets deleted as obsolete; the desugar // shape is exercised end-to-end via the workspace flow. body: Box::new(Term::If { cond: Box::new(Term::App { callee: Box::new(Term::Var { name: "==".into() }), args: vec![ Term::Var { name: "k".into() }, Term::Lit { lit: Literal::Int { value: 0 } }, ], tail: false, }), then: Box::new(Term::Var { name: "x".into() }), else_: Box::new(Term::App { callee: Box::new(Term::Var { name: "loop".into() }), args: vec![Term::App { callee: Box::new(Term::Var { name: "-".into() }), args: vec![ Term::Var { name: "k".into() }, Term::Lit { lit: Literal::Int { value: 1 } }, ], tail: false, }], tail: false, }), }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "loop".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "rec_id".into(), ty: Type::Forall { vars: vec!["a".into()], constraints: vec![], body: Box::new(Type::Fn { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }), }, params: vec!["x".into()], body: letrec, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); assert_eq!( out.defs.len(), 2, "expected one lifted fn appended; got {:?}", out.defs.iter().map(|d| d.name()).collect::>() ); let lifted = match &out.defs[1] { Def::Fn(fd) => fd, _ => unreachable!(), }; assert!( lifted.name.starts_with("loop$lr_"), "unexpected lifted-name shape: {}", lifted.name ); // Lifted type must be Forall(a). Fn(Int, a) -> a (k + x capture). match &lifted.ty { Type::Forall { vars, constraints: _, body } => { assert_eq!(vars, &vec!["a".to_string()], "Forall vars must mirror enclosing"); match body.as_ref() { Type::Fn { params, ret, .. } => { assert_eq!(params.len(), 2, "expected Int + a-typed capture"); assert!( matches!(¶ms[0], Type::Con { name, .. } if name == "Int"), "first param must be Int (loop's own k); got {:?}", params[0] ); assert!( matches!(¶ms[1], Type::Var { name } if name == "a"), "second param must be the captured `x: a`; got {:?}", params[1] ); assert!( matches!(ret.as_ref(), Type::Var { name } if name == "a"), "ret must be `a`; got {:?}", ret ); } other => panic!("Forall body must be Fn; got {:?}", other), } } other => panic!("lifted type must be Forall; got {:?}", other), } // Lifted params: original "k" + captured "x". assert_eq!(lifted.params, vec!["k".to_string(), "x".to_string()]); } /// a LetRec inside a polymorphic enclosing fn whose /// `in_term` uses the LetRec name as a value (not as a callee) is /// rejected. The 16b.5 eta-Lam wrap can't quantify `Forall.vars` /// because `Term::Lam` has no Forall slot — wrapping a polymorphic /// lifted fn into a monomorphic Lam loses the type vars. Queued /// separately as `closure-poly` (informally 16b.5b). #[test] #[should_panic(expected = "16b.6")] fn let_rec_name_as_value_in_polymorphic_enclosing_fn_panics() { // Same shape as `let_rec_name_as_value_in_in_term_wraps_to_eta_lam` // but the enclosing fn is Forall-quantified. let letrec = Term::LetRec { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["k".into()], body: Box::new(Term::Var { name: "x".into() }), in_term: Box::new(Term::Let { name: "g".into(), value: Box::new(Term::Var { name: "f".into() }), // value-position f body: Box::new(Term::App { callee: Box::new(Term::Var { name: "g".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "outer".into(), ty: Type::Forall { vars: vec!["a".into()], constraints: vec![], body: Box::new(Type::Fn { params: vec![Type::Var { name: "a".into() }], ret: Box::new(Type::Var { name: "a".into() }), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }), }, params: vec!["x".into()], body: letrec, suppress: vec![], doc: None, export: None, })], }; let _ = desugar_module(&m); } /// a nested LetRec whose inner LetRec captures the /// OUTER LetRec's NAME (not its params or any other local) is /// rejected at desugar time with the closure-of-self message. /// Capturing the outer's PARAMS is supported (covered by the /// `nested_let_rec` fixture / e2e); capturing the NAME is the /// chicken-and-egg case where the outer's value form does not /// exist before the outer's lift completes. #[test] #[should_panic(expected = "16b.7")] fn nested_let_rec_inner_captures_outer_name_panics() { // Outer LetRec body contains an inner LetRec that references // `outer` (the outer LetRec's own name) inside its body — as // a callee, even, but the panic fires on the capture // classification, not on a non-callee check. // // Shape inside main: // (let-rec outer (params i) (type Int->Int) // (body (let-rec inner (params j) (type Int->Int) // (body (app outer j)) ;; inner captures outer-NAME // (in (app inner 0)))) // (in (app outer 1))) let inner = Term::LetRec { name: "inner".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["j".into()], body: Box::new(Term::App { callee: Box::new(Term::Var { name: "outer".into() }), args: vec![Term::Var { name: "j".into() }], tail: false, }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "inner".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 0 } }], tail: false, }), }; let outer = Term::LetRec { name: "outer".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["i".into()], body: Box::new(inner), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "outer".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: outer, suppress: vec![], doc: None, export: None, })], }; let _ = desugar_module(&m); } /// a nested LetRec whose inner /// LetRec captures the OUTER LetRec's PARAM (not its name) lifts /// cleanly through the existing 16b.2 fast path. The outer's /// params live in the inner's outer-scope as `KnownType`, so the /// inner classifies them like any other fn-param capture and /// appends them to its lifted signature. The post-order traversal /// then lifts the outer next; references to the outer's params /// inside the inner-call rewrites stay in scope (the outer hasn't /// renamed its params yet). #[test] fn nested_let_rec_inner_captures_outer_param_lifts() { // Shape inside main: // (let-rec outer (params i) (type Int->Int) // (body (let-rec inner (params j) (type Int->Int) // (body (app + i j)) ;; inner captures outer-PARAM i // (in (app inner 0)))) // (in (app outer 1))) let inner = Term::LetRec { name: "inner".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["j".into()], body: Box::new(Term::App { callee: Box::new(Term::Var { name: "+".into() }), args: vec![ Term::Var { name: "i".into() }, Term::Var { name: "j".into() }, ], tail: false, }), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "inner".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 0 } }], tail: false, }), }; let outer = Term::LetRec { name: "outer".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["i".into()], body: Box::new(inner), in_term: Box::new(Term::App { callee: Box::new(Term::Var { name: "outer".into() }), args: vec![Term::Lit { lit: Literal::Int { value: 1 } }], tail: false, }), }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "main".into(), ty: Type::Fn { params: vec![], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec![], body: outer, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); // Three defs: original `main` + lifted `inner$lr_0` + lifted // `outer$lr_1`. Inner is lifted first (post-order), with outer's // param `i` appended; outer is lifted next, with no captures. let names: Vec = out.defs.iter().map(|d| d.name().to_string()).collect(); assert_eq!( names.len(), 3, "expected main + two lifted fns; got {names:?}" ); assert_eq!(names[0], "main"); assert!( names[1].starts_with("inner$lr_") && names[2].starts_with("outer$lr_"), "expected inner then outer lifted-fn names; got {names:?}" ); // The lifted inner must have two params: its own (`j`) plus the // captured outer-param (`i`). if let Def::Fn(inner_lifted) = &out.defs[1] { assert_eq!( inner_lifted.params, vec!["j".to_string(), "i".to_string()], "inner lifted params should be [j, i] (own + captured outer param)" ); } else { panic!("expected Def::Fn at defs[1]"); } } /// walks a term, returns true iff any [`Pattern::Lit`] is /// reachable in any [`Term::Match`] arm. After 16c desugaring, the /// invariant is `false` for every desugared output — `Pattern::Lit` /// is gone before typecheck/codegen runs. fn any_lit_pattern(t: &Term) -> bool { fn pat_has_lit(p: &Pattern) -> bool { match p { Pattern::Wild | Pattern::Var { .. } => false, Pattern::Lit { .. } => true, Pattern::Ctor { fields, .. } => fields.iter().any(pat_has_lit), } } match t { Term::Lit { .. } | Term::Var { .. } => false, Term::App { callee, args, .. } => { any_lit_pattern(callee) || args.iter().any(any_lit_pattern) } Term::Let { value, body, .. } => any_lit_pattern(value) || any_lit_pattern(body), Term::If { cond, then, else_ } => { any_lit_pattern(cond) || any_lit_pattern(then) || any_lit_pattern(else_) } Term::Do { args, .. } => args.iter().any(any_lit_pattern), Term::Ctor { args, .. } => args.iter().any(any_lit_pattern), Term::Match { scrutinee, arms } => { if any_lit_pattern(scrutinee) { return true; } for a in arms { if pat_has_lit(&a.pat) { return true; } if any_lit_pattern(&a.body) { return true; } } false } Term::Lam { body, .. } => any_lit_pattern(body), Term::Seq { lhs, rhs } => any_lit_pattern(lhs) || any_lit_pattern(rhs), Term::LetRec { body, in_term, .. } => { any_lit_pattern(body) || any_lit_pattern(in_term) } Term::Clone { value } => any_lit_pattern(value), Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body), Term::Loop { binders, body } => { binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body) } Term::Recur { args } => args.iter().any(any_lit_pattern), Term::New { args, .. } => args.iter().any(|arg| match arg { NewArg::Value(v) => any_lit_pattern(v), NewArg::Type(_) => false, }), } } /// a top-level lit arm desugars to a `Term::If` whose /// condition is `(== s_var lit)`. The desugared body must contain /// no `Pattern::Lit` anywhere. #[test] fn top_level_lit_desugars_to_if() { // (match n // (case (pat-lit 0) 100) // (case _ 999)) let body_match = Term::Match { scrutinee: Box::new(Term::Var { name: "n".into() }), arms: vec![ Arm { pat: Pattern::Lit { lit: Literal::Int { value: 0 }, }, body: Term::Lit { lit: Literal::Int { value: 100 }, }, }, Arm { pat: Pattern::Wild, body: Term::Lit { lit: Literal::Int { value: 999 }, }, }, ], }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], body: body_match, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); let body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; assert!( !any_lit_pattern(body), "desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}" ); // The desugar wraps the match in `let $mp_N = scrutinee in `, // and the chain head must be a `Term::If` (the lit arm). let chain = match body { Term::Let { body, .. } => body.as_ref(), other => panic!("expected outer Let from chain machinery, got {other:?}"), }; assert!( matches!(chain, Term::If { .. }), "expected Term::If at the chain head, got {chain:?}" ); } /// a `(pat-ctor Cons (pat-lit 0) _)` sub-pattern desugars /// to a tree with no `Pattern::Lit` anywhere — the lit is rewritten /// to a `Term::If` against the field-bound fresh variable. #[test] fn nested_lit_in_ctor_desugars_to_if() { // (match xs // (case (pat-ctor Cons (pat-lit 0) _) 0) // (case _ -1)) let body_match = Term::Match { scrutinee: Box::new(Term::Var { name: "xs".into() }), arms: vec![ Arm { pat: Pattern::Ctor { ctor: "Cons".into(), fields: vec![ Pattern::Lit { lit: Literal::Int { value: 0 }, }, Pattern::Wild, ], }, body: Term::Lit { lit: Literal::Int { value: 0 }, }, }, Arm { pat: Pattern::Wild, body: Term::Lit { lit: Literal::Int { value: -1 }, }, }, ], }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["xs".into()], body: body_match, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); let body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; assert!( !any_lit_pattern(body), "desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}" ); assert!( !any_nested_ctor(body), "desugarer must also produce no nested ctor sub-patterns; got: {body:#?}" ); } /// `is_flat` must classify /// `Pattern::Lit` as **not** flat, so the chain machinery in /// [`Desugarer::desugar_match`] is always invoked for lit-arms /// (rather than the early-return that would leak the lit pattern /// through to typecheck/codegen). #[test] fn flat_arm_with_lit_is_no_longer_flat() { let p = Pattern::Lit { lit: Literal::Int { value: 0 }, }; assert!( !is_flat(&p), "Pattern::Lit must be classified as non-flat after 16c" ); } /// the chain machinery's deepest fall-through is the /// polymorphic `__unreachable__` builtin (`forall a. a`), not a /// `Unit` literal. A match with two lit arms and a wildcard /// catches all paths via the wild-arm body, so the deepest /// `else_` of the chain is the `__unreachable__` var. #[test] fn chain_default_is_unreachable_builtin() { // (match n (case (pat-lit 0) 100) (case (pat-lit 1) 200)) // Note: no wildcard on purpose — the chain reaches the // synthetic terminator after both lit arms fail. let body_match = Term::Match { scrutinee: Box::new(Term::Var { name: "n".into() }), arms: vec![ Arm { pat: Pattern::Lit { lit: Literal::Int { value: 0 }, }, body: Term::Lit { lit: Literal::Int { value: 100 }, }, }, Arm { pat: Pattern::Lit { lit: Literal::Int { value: 1 }, }, body: Term::Lit { lit: Literal::Int { value: 200 }, }, }, ], }; let m = Module { schema: crate::SCHEMA.to_string(), name: "t".into(), kernel: false, imports: vec![], defs: vec![Def::Fn(FnDef { name: "f".into(), ty: Type::Fn { params: vec![Type::int()], ret: Box::new(Type::int()), effects: vec![], param_modes: vec![], ret_mode: ParamMode::Implicit, }, params: vec!["n".into()], body: body_match, suppress: vec![], doc: None, export: None, })], }; let out = desugar_module(&m); let body = match &out.defs[0] { Def::Fn(f) => &f.body, _ => unreachable!(), }; // Outer is `let $mp_N = scrutinee in `. The chain is // `if (== sv 0) then 100 else if (== sv 1) then 200 else __unreachable__`. let chain = match body { Term::Let { body, .. } => body.as_ref(), other => panic!("expected outer Let from chain machinery, got {other:?}"), }; let inner_else = match chain { Term::If { else_, .. } => else_.as_ref(), other => panic!("expected outer If, got {other:?}"), }; let deepest = match inner_else { Term::If { else_, .. } => else_.as_ref(), other => panic!("expected nested If, got {other:?}"), }; assert!( matches!(deepest, Term::Var { name } if name == "__unreachable__"), "deepest chain fall-through must be `__unreachable__`, got {deepest:?}" ); } }