Files
AILang/crates/ailang-core/src/desugar.rs
T
Brummel ce6ab8ee44 Iter 18e: (drop-iterative) annotation + worklist allocator
Closes the 18-arc's stack-recursion limit. Recursive drop
cascades from 18c.4 overflow on long ADT chains (Linux's 8 MB
default stack maxes out around 1M cells of List). The new
opt-in (drop-iterative) annotation on a Def::Type switches the
synthesised drop_<m>_<T> body from recursive to iterative-with-
explicit-worklist for that type.

Schema:
- TypeDef.drop_iterative: bool. Default false; serde-skip
  when false so existing fixtures' canonical JSON hashes stay
  stable.
- Form-A: (drop-iterative) clause inside (data T ...).

Worklist runtime (4 new ABI symbols in runtime/rc.c):
- ailang_drop_worklist_new(initial_capacity)
- ailang_drop_worklist_push(wl, ptr)
- ailang_drop_worklist_pop(wl) -> ptr
- ailang_drop_worklist_free(wl)

Heap stretchy buffer, doubling on overflow, null-filtering on
push. Lean 4 / Roc precedent documented in the runtime; the
slot-repurposing strategy was considered and rejected because
not every box has a free pointer-typed slot to thread the
worklist through (Cons head is i64, slot 1 is ptr but it's
the field we're following — no free slot).

Codegen (emit_iterative_drop_fn_for_type): for a
drop_iterative type, drop_<m>_<T>(ptr %p) emits a worklist
loop. Fields of the SAME annotated type push onto the
worklist (mono-typed); fields of DIFFERENT types call their
own drop fn directly (recursive on those, but only if THEY
are themselves recursive — i.e. one level of cascade jump
maximum). Mono-typed-worklist is sound for the deep-self-
recursion case the iter targets (List of List of T just
needs the spine flattened).

Tests:
- examples/rc_drop_iterative_long_list — 1M-cell List of Int
  with (drop-iterative) annotation.
- alloc_rc_drop_iterative_handles_million_cell_list E2E —
  builds + runs under --alloc=rc, asserts clean exit. With
  annotation: exits 0. Without annotation (control): SIGSEGV
  at exit code 139 (verified by hand). Worklist is load-
  bearing.
- iter18e_drop_iterative_emits_worklist_body_no_self_recursion
  IR-shape: worklist body has br to loop_head AND no direct
  recursive call into drop_<m>_<T>.
- iter18e_no_annotation_keeps_recursive_drop_body — control:
  unannotated variant still emits the 18c.4 recursive shape.
- 3 surface parse-tests for the annotation round-trip.

Test deltas: e2e 58 -> 61 (+3), surface 18 -> 21 (+3). All
other buckets unchanged. cargo test --workspace green.

Known debt (deliberate):
- Mono-typed worklist: cross-type drop-iterative fields call
  the other type's drop fn directly. A heterogeneous
  worklist would be more general but adds tag tracking
  complexity for a case (drop-iterative T containing
  drop-iterative T') that's narrower than the deep-self-
  recursion target. Documented in
  emit_iterative_drop_fn_for_type's doc.
- Closure / Type::Var / Type::Forall fields fall back to
  shallow ailang_rc_dec via field_drop_call — same as the
  recursive variant.
- Dynamic-tag partial-drop fallback (head_or_zero epilogue
  shallow dec when moved_slots non-empty) — out of scope per
  brief.
2026-05-08 12:53:09 +02:00

2854 lines
120 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 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 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(),
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,
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 { .. } => 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,
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(),
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(),
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,
}),
},
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),
},
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,
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,
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,
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()],
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,
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, 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()],
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,
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,
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,
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,
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,
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,
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:?}"
);
}
}