//! 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`,
//!    Iter 16d) — codegen lowers it to LLVM `unreachable`. Valid
//!    programs never reach it because the checker requires
//!    exhaustiveness (the catch-all arm dominates the chain).
//!    Pre-16d the default was a `Unit` literal, 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 `<hint>$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};

/// Iter 16b.2: 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<String> = 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());
            }
            // Iter 22b.1: 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());
            }
        }
    }
    // Iter 16b.1: 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<String> = 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) => {
                // Iter 16b.2 / 16b.6: 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<String, ScopeEntry> = 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);
                        }
                    }
                }
                // Iter 16b.6: 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<String, ScopeEntry> = BTreeMap::new();
                c.value = d.desugar_term(&c.value, &scope);
            }
            Def::Type(_) => {}
            // Iter 22b.1: 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(_) => {}
        }
    }
    // Iter 16b.1: 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.into_iter());
    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<String>) {
    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 } => {
            // Iter 18c.1: identity for the used-name walk.
            collect_used_in_term(value, used);
        }
        Term::ReuseAs { source, body } => {
            // Iter 18d.1: structural recursion through both children.
            collect_used_in_term(source, used);
            collect_used_in_term(body, used);
        }
    }
}

/// Walks a pattern and inserts every [`Pattern::Var.name`] into `used`.
fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<String>) {
    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.
///
/// Iter 16b.1 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.
///
/// Iter 16b.6 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<String>,
    lifted: Vec<Def>,
    module_top_names: BTreeSet<String>,
    /// Iter 16b.6: the enclosing fn's Forall.vars (or empty if mono).
    /// Reset on entry to each `Def::Fn`.
    current_def_forall_vars: Vec<String>,
}

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;
            }
        }
    }

    /// Iter 16b.1: returns a name of the form `<hint>$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<String, ScopeEntry>) -> 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<Arm> = arms
                    .iter()
                    .map(|a| {
                        let mut inner = scope.clone();
                        let mut pat_binds: BTreeSet<String> = 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 {
                // Iter 18c.1: 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 {
                // Iter 18d.1: 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::LetRec { name, ty, params, body, in_term } => {
                // Iter 16b.1: lift to a synthetic top-level fn (no-capture).
                // Iter 16b.2: extend the lift to the path-1 safe subset —
                // captures of fn-params / Lam-params (whose types are
                // known statically).
                // Iter 16b.3: 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.
                // Iter 16b.4: 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<Type> = 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<Fn>); 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();

                // Iter 16b.6: 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<String> = BTreeSet::new();
                local_bound.insert(name.clone());
                for p in params {
                    local_bound.insert(p.clone());
                }
                let mut frees: BTreeSet<String> = BTreeSet::new();
                free_vars_in_term(&desugared_body, &local_bound, &mut frees);
                let captures: Vec<String> = 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`.
                //
                // Iter 16b.6: 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<String> =
                    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<String> = 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>, 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<Term> = 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,
                }));
                in_full
            }
        }
    }

    /// 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<Arm>) -> 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() };
        // Iter 16d: `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 } => {
                // Iter 16c: 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<String> = fields.iter().map(|_| self.fresh()).collect();
                let flat_fields: Vec<Pattern> = 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 } => {
                // Iter 16c: 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.
///
/// Iter 16c: [`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,
        // Iter 16c: 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)),
    }
}

/// Iter 16c: Build an equality-test term for a lit pattern. The shape is
/// `(app == scrutinee lit_term)` for Int/Bool/Str. Unit literals are
/// degenerate — every Unit value is equal — so an emitted `true` literal
/// stands in.
///
/// Note: as of Iter 16c, the `==` builtin is typed `(Int, Int) -> Bool`
/// only. Bool- and Str-lit patterns therefore reach typecheck as a
/// type error rather than a clean rewrite. They remain authorable at the
/// AST level (the desugar never refuses) but are not yet usable end-to-
/// end. Lifting that gate is bound to extending `==` to those types.
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: "==".to_string(),
            }),
            args: vec![scrutinee, Term::Lit { lit: lit.clone() }],
            tail: false,
        },
    }
}

/// Iter 16b.1: 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.
///
/// Iter 16b.3: 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<String>, out: &mut BTreeSet<String>) {
    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 } => {
            // Iter 18c.1: `(clone X)` has the same free vars as `X`.
            free_vars_in_term(value, bound, out);
        }
        Term::ReuseAs { source, body } => {
            // Iter 18d.1: free vars are the union of source and body.
            free_vars_in_term(source, bound, out);
            free_vars_in_term(body, bound, out);
        }
    }
}

/// Iter 16b.1: 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.
///
/// Iter 16b.3: 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<String>) {
    match p {
        Pattern::Wild | Pattern::Lit { .. } => {}
        Pattern::Var { name } => {
            out.insert(name.clone());
        }
        Pattern::Ctor { fields, .. } => {
            for sub in fields {
                pattern_binds(sub, out);
            }
        }
    }
}

/// Iter 16b.1: 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.
///
/// Iter 16b.3: 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<String> = 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 {
            // Iter 18c.1: structural recursion through the wrapper.
            value: Box::new(subst_var(value, from, to)),
        },
        Term::ReuseAs { source, body } => Term::ReuseAs {
            // Iter 18d.1: structural recursion through both children.
            source: Box::new(subst_var(source, from, to)),
            body: Box::new(subst_var(body, from, to)),
        },
    }
}

/// Iter 16b.2: peel a `Type::Forall { body: Type::Fn { ... } }` to
/// expose the inner `Type::Fn`. Returns `Some(&Type::Fn)` if the
/// type is `Fn` or `Forall<Fn>`; `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,
    }
}

/// Iter 16b.2: 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`].
///
/// Iter 16b.3: 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<Term> = 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 {
            // Iter 18c.1: structural recursion through the wrapper.
            value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
        },
        Term::ReuseAs { source, body } => Term::ReuseAs {
            // Iter 18d.1: 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)),
        },
    }
}

/// Iter 16b.2: 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.
///
/// Iter 16b.3: made `pub` for the same reason as
/// [`subst_call_with_extras`].
pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
    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)),
        // Iter 18c.1: clone is identity for the non-callee scan.
        Term::Clone { value } => find_non_callee_use(value, name),
        // Iter 18d.1: scan both source and body.
        Term::ReuseAs { source, body } => {
            find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
        }
    }
}

#[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),
        }
    }

    /// 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),
        }
    }

    /// Iter 16a: 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(),
            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,
            })],
        };
        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(),
            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,
            })],
        };
        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 { .. }));
    }

    /// Iter 16b.1: 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(),
            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,
            })],
        };
        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 <lifted_name> 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:?}"),
        }
    }

    /// Iter 16b.2: 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(),
            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,
            })],
        };
        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:?}"),
        }
    }

    /// Iter 16b.3: 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(),
            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,
            })],
        };
        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::<Vec<_>>()
        );
        // 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:#?}"
        );
    }

    /// Iter 16b.4: 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,
        };
        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(),
            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,
                }),
            ],
        };
        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::<Vec<_>>()
        );
        // 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:#?}"
        );
    }

    /// Iter 16b.5: 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(),
            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,
            })],
        };
        let _ = desugar_module(&m);
    }

    /// Iter 16b.5: 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: <rewritten in_term> }` 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(),
            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,
            })],
        };
        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::<Vec<_>>()
        );
        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:?}"),
        }
    }

    /// Iter 16b.6: 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()],
            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(),
            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,
            })],
        };
        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::<Vec<_>>()
        );
        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!(&params[0], Type::Con { name, .. } if name == "Int"),
                            "first param must be Int (loop's own k); got {:?}",
                            params[0]
                        );
                        assert!(
                            matches!(&params[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()]);
    }

    /// Iter 16b.6: 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(),
            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,
            })],
        };
        let _ = desugar_module(&m);
    }

    /// Iter 16b.7: 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(),
            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,
            })],
        };
        let _ = desugar_module(&m);
    }

    /// Iter 16b.7 (positive companion): 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(),
            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,
            })],
        };
        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<String> = 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]");
        }
    }

    /// Iter 16c: 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),
        }
    }

    /// Iter 16c: 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(),
            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,
            })],
        };
        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 <chain>`,
        // 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:?}"
        );
    }

    /// Iter 16c: 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(),
            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,
            })],
        };
        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:#?}"
        );
    }

    /// Iter 16c regression guard: `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"
        );
    }

    /// Iter 16d: 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(),
            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,
            })],
        };
        let out = desugar_module(&m);
        let body = match &out.defs[0] {
            Def::Fn(f) => &f.body,
            _ => unreachable!(),
        };
        // Outer is `let $mp_N = scrutinee in <chain>`. 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:?}"
        );
    }
}
