9339279181
Terminal iteration of the kernel-extension-mechanics milestone. Ships
the four language-level mechanisms named in the spec's § Goal:
Module.kernel + TypeDef.param-in schema, their Form-A surface,
flag-driven kernel-tier auto-injection, and generic param-in checker
enforcement with a new diagnostic.
Schema (Tasks 1+2). Module gains a `kernel: bool` field
(skip_serializing_if = is_false), TypeDef gains a
`param_in: BTreeMap<String, BTreeSet<String>>` field
(skip-if-empty, kebab-renamed to "param-in"). Both fields are
strictly additive — every pre-existing fixture's canonical-JSON hash
is bit-stable except `prelude.ail`, which intentionally gains
`(kernel)`. The struct-literal sweep covered ~104 Module sites and
~35 TypeDef sites across the workspace; the additive serde-default
covers JSON deserialise paths, only Rust struct literals broke.
Form-A surface (Tasks 3+4). `(kernel)` is a bare module-header
attribute; `(param-in (a Int Float) (b Str))` is one outer
TypeDef-body clause carrying one or more inner var-lists (OQ1
decision — mirrors `(ctors …)`, one parser arm, deterministic
BTreeMap iteration). Both round-trip Form-A → JSON → Form-A
bit-identical.
Workspace-load migration (Task 5). The hardcoded `&["prelude"]`
literal at loader.rs:108 became a `modules.values().filter(|m|
m.kernel)` derivation; `parse_prelude()` injection stays because
the prelude has no on-disk manifest in user workspaces. Prelude
now carries `(kernel)` in its source, so the new filter picks it
up automatically. Code-path migration only — observable behaviour
is identical (prelude_free_fns.rs stays green). prelude hash
re-pinned (af372f28c726f29f) with Honesty-Rule provenance comment.
WorkspaceLoadError::ReservedModuleName diagnostic prose
repurposed: any built-in kernel module name is reserved
(currently prelude + kernel_stub), not specifically prelude. CLI
mapping at main.rs updated in lockstep.
Stub crate (Task 6). New `crates/ailang-kernel-stub/` is a
zero-dependency leaf crate carrying only `pub const STUB_AIL:
&str` with the Form-A source of the kernel_stub module (one
parametric TypeDef with param-in, one ctor). The parse hop —
`parse_kernel_stub()` — lives in ailang-surface next to
parse_prelude, keeping the crate-dependency graph acyclic
(`ailang-surface → ailang-kernel-stub → ailang-core`, no
back-edge). The stub is injected unconditionally in all builds as
the ratifying fixture for the kernel-extension mechanism; future
base extensions may add more or retire the stub. Drift-pinned by
`kernel_stub_module_round_trips`.
Checker (Task 7). New `CheckError::ParamNotInRestrictedSet`
variant + code() + ctx() arms + enforcement in
`check_type_well_formed`'s Type::Con arm — generic, data-driven
from the TypeDef, mentions no specific extension type. Two
in-source tests pin both the rejection (`Str` outside `{Int,
Float}`) and the acceptance (`Int` inside) paths.
Workspace-load integration tests (Task 8). New
`workspace_kernel.rs` integration-test crate with three tests:
auto-import without explicit `(import …)` declaration, two
kernel-tier modules co-load, explicit-import-overrides-auto-
import precedence preserved. Loader is import-tree-only so the
auto-import tests use a bridge module that brings the kernel
module into the workspace via the import graph — docstring
captures the reachability nuance for future readers.
Doc-state transitions (Task 9). INDEX.md kernel-extensions row
annotation transitions from "design accepted 2026-05-28; impl in
progress" to "mechanisms milestone closed 2026-05-28; raw-buf and
series milestones pending". Whitepaper STATUS + auto-import +
param-in sections transitioned forward→present for shipped
mechanisms; forward-tense survives only in sections describing
the still-pending raw-buf/series milestones (per Honesty-Rule).
data-model contract gains anchor blocks for both new schema
fields.
Side-effect: every binary's IR snapshot now contains ~52 lines
for `drop_kernel_stub_StubT` because the stub is auto-injected
into every workspace load. Snapshots refreshed; e2e expects 4
modules per workspace (prelude + kernel_stub + entry + zero or
more user modules) instead of the previous 3.
Plan defects scrubbed in the implementation (folded back into
the planner template via the planner's self-review checklist
next time): Task 4 sample test src used fictional
`(ctors (MkT a))` list form (project grammar is per-`(ctor MkT
a)`); Task 6 original wiring would have created a cycle
ailang-surface → ailang-kernel-stub → ailang-surface (inverted —
stub crate is zero-dep, parse hop lives in surface); Task 7 in-
source tests referenced a fictional `check_type_in_module`
helper (used the existing Workspace + check_workspace
convention); Task 8 first integration test expected loader to
auto-load kernel modules from disk (loader is import-tree-only;
tests use a bridge module).
Concern-5 fix folded in pre-commit: workspace.rs ReservedModuleName
doc-prose initially said "in test/dev builds" for kernel_stub —
but stub is unconditionally injected in all builds. Doc copy
tightened to present-state per Honesty-Rule.
Stats: 0 spec-review-loops, 0 quality-review-loops, 2 sweep-script
retries on Task 2 (brace-depth bug on nested vec![Ctor{…}],
recovered via per-file checkout + rewritten anchor-on-existing-
field sweep), 1 e2e-snapshot refresh on Task 6.
3130 lines
130 KiB
Rust
3130 lines
130 KiB
Rust
//! AST → AST rewriter that runs **after** [`crate::load_module`] and
|
||
//! **before** anything that consumes a [`Module`] (typecheck, codegen).
|
||
//!
|
||
//! ## Iter 16a: nested constructor patterns in `match`
|
||
//!
|
||
//! Through Iter 15e, a [`Pattern::Ctor`]'s sub-patterns were restricted to
|
||
//! [`Pattern::Var`] and [`Pattern::Wild`] — nested ctors like
|
||
//! `(pat-ctor Cons a (pat-ctor Cons b _))` were rejected by the
|
||
//! checker (`nested-ctor-pattern-not-allowed`) and silently mishandled
|
||
//! by the codegen. Iter 16a lifts the gate by **desugaring** nested
|
||
//! ctor sub-patterns into chains of single-level matches at the AST
|
||
//! level, before either the checker or codegen sees the module. The
|
||
//! rewrite is pure (no side effects, no I/O), runs in memory, and
|
||
//! produces a [`Module`] semantically equivalent to its input.
|
||
//!
|
||
//! Lit sub-patterns inside a Ctor remain rejected — that is a separate
|
||
//! iter. The [`crate::ast::Pattern`] enum, the JSON schema, and every
|
||
//! on-disk hash are unchanged: the desugar runs **after** `load_module`,
|
||
//! so canonical bytes computed from the source file are untouched. The
|
||
//! checker / codegen consume the desugared form.
|
||
//!
|
||
//! ## Algorithm
|
||
//!
|
||
//! For a [`Term::Match`] whose arms contain a non-flat ctor pattern:
|
||
//!
|
||
//! 1. Bottom-up: recursively desugar the scrutinee and every arm body
|
||
//! first (children are normalised before parents).
|
||
//! 2. Let-bind the scrutinee to a fresh name `$mp_N` (so that we don't
|
||
//! re-evaluate effectful scrutinees per arm).
|
||
//! 3. Build a chain of single-level matches via
|
||
//! `build_chain(s_var, arms, default)` where `default` is the
|
||
//! polymorphic bottom builtin `__unreachable__` (`forall a. a`)
|
||
//! — codegen lowers it to LLVM `unreachable`. Valid programs
|
||
//! never reach it because the checker requires exhaustiveness
|
||
//! (the catch-all arm dominates the chain). An earlier shape used
|
||
//! a `Unit` literal as the default, which forced any match whose
|
||
//! arms returned a non-Unit type to carry a synthetic `_` arm
|
||
//! dominating the terminator. The polymorphic `__unreachable__`
|
||
//! removes that workaround.
|
||
//! 4. Each arm is lowered via `desugar_one_arm`:
|
||
//! - `Pattern::Wild` → arm body (catch-all; later arms are dropped).
|
||
//! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`.
|
||
//! - `Pattern::Lit { lit }` → `Term::If { cond = (== s_var lit),
|
||
//! then = arm.body, else_ = fall_k }` (Iter 16c). After this
|
||
//! rewrite, no `Pattern::Lit` reaches typecheck or codegen.
|
||
//! - `Pattern::Ctor { ctor, fields }`: lift each field to a fresh
|
||
//! var, build a flat outer pattern, walk fields right-to-left
|
||
//! wrapping inner sub-patterns via `wrap_sub`. Emit a
|
||
//! `Term::Match` with two arms: the flat ctor and a wildcard
|
||
//! fall-through into the rest of the chain.
|
||
//!
|
||
//! `wrap_sub(fv, sub, body, fall_k)` recurses on `sub`:
|
||
//! - `Var(n)` → `Let(n, Var(fv), body)`.
|
||
//! - `Wild` → `body`.
|
||
//! - `Lit` (Iter 16c) → `Term::If { cond = (== fv lit), then = body,
|
||
//! else_ = fall_k }`. Same shape as the top-level case but on the
|
||
//! field-bound fresh variable.
|
||
//! - `Ctor` → recursively `desugar_match(Var(fv), [(sub, body),
|
||
//! (Wild, fall_k)])`. The recursion is what handles arbitrary
|
||
//! nesting depth (`Cons a (Cons b (Cons c _))`).
|
||
//!
|
||
//! The fall-through term is cloned per inner match because each branch
|
||
//! that fails must continue to the same fallthrough. Worst-case size is
|
||
//! O(arms × depth); acceptable for typical patterns.
|
||
//!
|
||
//! ## Fresh-name safety
|
||
//!
|
||
//! `$` is a valid identifier character in form (A) (the lexer's `Ident`
|
||
//! token is "anything not paren/int/string"), so a user *could* write
|
||
//! `$mp_0` themselves. To avoid collisions, the fresh-name generator
|
||
//! reads a pre-collected set of every name appearing in any
|
||
//! [`crate::ast::Term::Var`] or [`crate::ast::Pattern::Var`] of the
|
||
//! module and bumps the counter until it lands on a name not in that
|
||
//! set.
|
||
//!
|
||
//! ## Iter 16b.1: local recursive `let` (no-capture)
|
||
//!
|
||
//! [`Term::LetRec`] is a surface-only AST node introduced in 16b.1. The
|
||
//! desugar pass eliminates it before typecheck/codegen by **lifting**
|
||
//! the LetRec to a synthetic top-level fn in the same module. The
|
||
//! lifted name has the form `<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};
|
||
|
||
/// 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());
|
||
}
|
||
// class/instance defs are passthrough for the
|
||
// desugar pass — their bodies (default methods, instance
|
||
// method bodies) will be desugared once 22b.2 wires the
|
||
// class/instance arms into typecheck. For 22b.1 the names
|
||
// still go into `used` so the synthetic-name generator
|
||
// does not collide.
|
||
Def::Class(c) => {
|
||
used.insert(c.name.clone());
|
||
}
|
||
Def::Instance(i) => {
|
||
used.insert(i.class.clone());
|
||
}
|
||
}
|
||
}
|
||
// pre-collect every top-level def name. The LetRec
|
||
// lifter consults this set to (a) know whether a free var of a
|
||
// LetRec body resolves to a top-level def (no capture) and
|
||
// (b) keep its fresh-name generator from colliding with an
|
||
// existing def — including ones lifted earlier in the same pass.
|
||
let mut module_top_names: BTreeSet<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) => {
|
||
// 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);
|
||
}
|
||
}
|
||
}
|
||
// 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(_) => {}
|
||
// class/instance defs are not desugared yet.
|
||
// Their bodies (default methods, instance method bodies)
|
||
// will be processed once 22b.2 lands the class/instance
|
||
// arms in typecheck and 22b.3 in codegen.
|
||
Def::Class(_) | Def::Instance(_) => {}
|
||
}
|
||
}
|
||
// append every lifted fn to the desugared module.
|
||
// Order: original defs first, then lifts in the order they were
|
||
// produced by the bottom-up walk. Typecheck/codegen are order-
|
||
// insensitive at the def list level (they index by name), so this
|
||
// is purely cosmetic.
|
||
out.defs.extend(d.lifted);
|
||
out
|
||
}
|
||
|
||
/// Walks a term and inserts every [`Term::Var.name`] and every
|
||
/// [`Pattern::Var.name`] into `used`. Used to seed
|
||
/// [`Desugarer::fresh`] so a generated name `$mp_N` cannot shadow a
|
||
/// source-level identifier.
|
||
fn collect_used_in_term(t: &Term, used: &mut BTreeSet<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 } => {
|
||
// identity for the used-name walk.
|
||
collect_used_in_term(value, used);
|
||
}
|
||
Term::ReuseAs { source, body } => {
|
||
// structural recursion through both children.
|
||
collect_used_in_term(source, used);
|
||
collect_used_in_term(body, used);
|
||
}
|
||
Term::Loop { binders, body } => {
|
||
for b in binders {
|
||
used.insert(b.name.clone());
|
||
collect_used_in_term(&b.init, used);
|
||
}
|
||
collect_used_in_term(body, used);
|
||
}
|
||
Term::Recur { args } => {
|
||
for a in args {
|
||
collect_used_in_term(a, used);
|
||
}
|
||
}
|
||
Term::New { args, .. } => {
|
||
for arg in args {
|
||
match arg {
|
||
NewArg::Value(v) => collect_used_in_term(v, used),
|
||
NewArg::Type(_) => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Walks a pattern and inserts every [`Pattern::Var.name`] into `used`.
|
||
fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<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.
|
||
///
|
||
/// LetRec-lift fields:
|
||
/// - `lifted` accumulates synthetic top-level fns produced by
|
||
/// [`Term::LetRec`] desugaring. Appended to `Module.defs` once the
|
||
/// per-def walk finishes.
|
||
/// - `module_top_names` mirrors every name reachable as a top-level
|
||
/// def at this point in the pass (originals + already-lifted). The
|
||
/// capture analyser uses it to classify free vars; the fresh-name
|
||
/// generator [`fresh_lifted`](Self::fresh_lifted) consults it so
|
||
/// later lifts cannot collide with earlier ones.
|
||
///
|
||
/// Forall-tracking field:
|
||
/// - `current_def_forall_vars` carries the enclosing fn's
|
||
/// `Type::Forall.vars` while desugaring its body. Empty for
|
||
/// monomorphic enclosing fns. Read by the LetRec lifter to wrap
|
||
/// the synthetic lifted fn's signature in `Type::Forall` mirroring
|
||
/// the enclosing fn — so the lifted fn enters codegen's
|
||
/// monomorphisation queue at every call site of the enclosing fn,
|
||
/// specialising at the same type args as its host.
|
||
struct Desugarer {
|
||
counter: u64,
|
||
used: BTreeSet<String>,
|
||
lifted: Vec<Def>,
|
||
module_top_names: BTreeSet<String>,
|
||
/// 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 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 {
|
||
// pure structural recursion through the
|
||
// wrapper. Same pattern as `Term::Let`'s value branch.
|
||
value: Box::new(self.desugar_term(value, scope)),
|
||
},
|
||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||
// pure structural recursion through both
|
||
// children. Same pattern as `Term::Clone`.
|
||
source: Box::new(self.desugar_term(source, scope)),
|
||
body: Box::new(self.desugar_term(body, scope)),
|
||
},
|
||
Term::Loop { binders, body } => {
|
||
// loop-recur iter 1: structural recursion. Each
|
||
// binder's init is desugared in scope of the outer env
|
||
// plus already-declared binders; the body sees all
|
||
// binders. The LetBound sentinel keeps generated fresh
|
||
// names off binder names.
|
||
let mut inner = scope.clone();
|
||
let new_binders: Vec<LoopBinder> = binders
|
||
.iter()
|
||
.map(|b| {
|
||
let init = self.desugar_term(&b.init, &inner);
|
||
inner.insert(b.name.clone(), ScopeEntry::LetBound);
|
||
LoopBinder {
|
||
name: b.name.clone(),
|
||
ty: b.ty.clone(),
|
||
init,
|
||
}
|
||
})
|
||
.collect();
|
||
Term::Loop {
|
||
binders: new_binders,
|
||
body: Box::new(self.desugar_term(body, &inner)),
|
||
}
|
||
}
|
||
Term::Recur { args } => Term::Recur {
|
||
args: args
|
||
.iter()
|
||
.map(|a| self.desugar_term(a, scope))
|
||
.collect(),
|
||
},
|
||
Term::LetRec { name, ty, params, body, in_term } => {
|
||
// lift to a synthetic top-level fn (no-capture).
|
||
// extend the lift to the path-1 safe subset —
|
||
// captures of fn-params / Lam-params (whose types are
|
||
// known statically).
|
||
// when ANY capture is `LetBound` the desugar
|
||
// pass cannot resolve its type (the let-value's type is
|
||
// only known after typecheck). For that case we LEAVE
|
||
// the LetRec in place, with body and in_term recursively
|
||
// desugared. A post-typecheck pass (`lift_letrecs` in
|
||
// `ailang-check`) walks every surviving LetRec and lifts
|
||
// it using the typechecker's resolved types.
|
||
// extends the defer path to `MatchArm`
|
||
// captures. The post-typecheck pass already walks
|
||
// `Term::Match` arms and (since 16b.3) calls
|
||
// `type_check_pattern_for_lift` to extend locals with
|
||
// pattern bindings — that machinery resolves Match-arm
|
||
// capture types via constructor-field substitution
|
||
// against the matched ctor's declared field types.
|
||
//
|
||
// 16b.2's fast path (KnownType-only captures → lift here)
|
||
// STILL fires when applicable. Any `LetBound` or
|
||
// `MatchArm` capture forces the all-or-nothing defer
|
||
// path.
|
||
//
|
||
// `EnclosingLetRec` captures STILL panic (16b.7 —
|
||
// nested mutual recursion needs separate machinery).
|
||
//
|
||
// The non-callee-use check (16b.5 violation) runs FIRST
|
||
// so the diagnostic fires consistently regardless of
|
||
// which path the LetRec takes.
|
||
|
||
// Peel `ty` to its inner Fn so we know the LetRec's
|
||
// own param types (for body-scope) and so we can
|
||
// detect a Forall LetRec early.
|
||
let inner_params_tys: Vec<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();
|
||
|
||
// 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`.
|
||
//
|
||
// 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,
|
||
export: None,
|
||
}));
|
||
in_full
|
||
}
|
||
Term::New { type_name, args } => Term::New {
|
||
type_name: type_name.clone(),
|
||
args: args
|
||
.iter()
|
||
.map(|arg| match arg {
|
||
NewArg::Value(v) => NewArg::Value(self.desugar_term(v, scope)),
|
||
NewArg::Type(t) => NewArg::Type(t.clone()),
|
||
})
|
||
.collect(),
|
||
},
|
||
}
|
||
}
|
||
|
||
/// Lowers a `match` whose children have already been desugared.
|
||
///
|
||
/// If every arm is already flat ([`is_flat`] returns true), the
|
||
/// match is reconstructed unchanged (no let-binding or chain
|
||
/// allocation overhead). Otherwise the scrutinee is let-bound to
|
||
/// a fresh `$mp_N` and the arms are translated to a chain of
|
||
/// single-level matches over that variable.
|
||
fn desugar_match(&mut self, scrutinee: Term, arms: Vec<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() };
|
||
// `default` is unreachable for valid programs (the
|
||
// typechecker requires either a catch-all arm or exhaustive
|
||
// ctor coverage). Use the polymorphic bottom builtin
|
||
// `__unreachable__` (`forall a. a`) so the terminator unifies
|
||
// against any arm result type without forcing a synthetic
|
||
// `Unit`-typed `_` arm to dominate it. Codegen lowers the
|
||
// var to LLVM `unreachable`.
|
||
let default = Term::Var { name: "__unreachable__".into() };
|
||
let chain = self.build_chain(&s_var, &arms, &default);
|
||
Term::Let {
|
||
name: s,
|
||
value: Box::new(scrutinee),
|
||
body: Box::new(chain),
|
||
}
|
||
}
|
||
|
||
/// Recursively builds a chain of single-arm matches with a shared
|
||
/// fall-through. Empty arms ⇒ `default`; otherwise the first arm
|
||
/// is desugared with the rest of the chain as its fall-through.
|
||
fn build_chain(&mut self, s_var: &Term, arms: &[Arm], default: &Term) -> Term {
|
||
if arms.is_empty() {
|
||
return default.clone();
|
||
}
|
||
let head = &arms[0];
|
||
let rest = &arms[1..];
|
||
let fall_k = self.build_chain(s_var, rest, default);
|
||
self.desugar_one_arm(s_var, head, fall_k)
|
||
}
|
||
|
||
/// Lowers one arm into a term. Wild/Var arms drop the chain (the
|
||
/// arm matches everything); Lit and Ctor arms emit a `Term::Match`
|
||
/// with the desugared head pattern as the first arm and a
|
||
/// wildcard fall-through to `fall_k`.
|
||
fn desugar_one_arm(&mut self, s_var: &Term, arm: &Arm, fall_k: Term) -> Term {
|
||
match &arm.pat {
|
||
Pattern::Wild => arm.body.clone(),
|
||
Pattern::Var { name } => Term::Let {
|
||
name: name.clone(),
|
||
value: Box::new(s_var.clone()),
|
||
body: Box::new(arm.body.clone()),
|
||
},
|
||
Pattern::Lit { lit } => {
|
||
// a top-level lit arm desugars to `if (== s_var lit)
|
||
// then arm.body else fall_k`. Eliminates `Pattern::Lit` from
|
||
// the desugared output entirely; codegen never sees it.
|
||
let cmp = build_eq(s_var.clone(), lit);
|
||
Term::If {
|
||
cond: Box::new(cmp),
|
||
then: Box::new(arm.body.clone()),
|
||
else_: Box::new(fall_k),
|
||
}
|
||
}
|
||
Pattern::Ctor { ctor, fields } => {
|
||
// Lift each field to a fresh var; build the flat outer
|
||
// pattern, then walk right-to-left wrapping each inner
|
||
// sub-pattern via `wrap_sub` so the deepest field is
|
||
// matched first inside-out.
|
||
let fresh_vars: Vec<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 } => {
|
||
// a lit sub-pattern desugars to `if (== fv lit) body
|
||
// else fall_k`. Same shape as the top-level case, but on the
|
||
// field-bound fresh variable rather than the original
|
||
// scrutinee.
|
||
let cmp = build_eq(Term::Var { name: fv.to_string() }, lit);
|
||
Term::If {
|
||
cond: Box::new(cmp),
|
||
then: Box::new(body),
|
||
else_: Box::new(fall_k.clone()),
|
||
}
|
||
}
|
||
Pattern::Ctor { .. } => self.desugar_match(
|
||
Term::Var {
|
||
name: fv.to_string(),
|
||
},
|
||
vec![
|
||
Arm {
|
||
pat: sub.clone(),
|
||
body,
|
||
},
|
||
Arm {
|
||
pat: Pattern::Wild,
|
||
body: fall_k.clone(),
|
||
},
|
||
],
|
||
),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// True iff `p` is fully shallow: a [`Pattern::Var`], [`Pattern::Wild`],
|
||
/// or a [`Pattern::Ctor`] all of whose fields are `Var` or `Wild`. Used
|
||
/// by [`Desugarer::desugar_match`] to skip the let-binding and chain
|
||
/// construction for already-flat matches.
|
||
///
|
||
/// [`Pattern::Lit`] is **not** flat — it always desugars to a
|
||
/// [`Term::If`] via [`build_eq`], so it must take the chain path even
|
||
/// when no other arm needs flattening. Otherwise the early-return in
|
||
/// [`Desugarer::desugar_match`] would leak a `Pattern::Lit` arm to
|
||
/// typecheck/codegen, which the codegen rejects with an internal error.
|
||
fn is_flat(p: &Pattern) -> bool {
|
||
match p {
|
||
Pattern::Wild | Pattern::Var { .. } => true,
|
||
// lit patterns are not flat; they desugar to If.
|
||
Pattern::Lit { .. } => false,
|
||
Pattern::Ctor { fields, .. } => fields
|
||
.iter()
|
||
.all(|f| matches!(f, Pattern::Var { .. } | Pattern::Wild)),
|
||
}
|
||
}
|
||
|
||
/// Build an equality-test term for a lit pattern. The shape is
|
||
/// Builds the equality-test AST node for a literal pattern. Lowers
|
||
/// `(pat-lit <lit>)` to `(if (eq sv <lit>) <body> <fall_k>)`. The
|
||
/// emitted `eq` resolves via prelude.Eq's class-method dispatch
|
||
/// (per design/contracts/0016-method-dispatch.md) — Int/Bool/Str/Unit
|
||
/// patterns all route through the corresponding primitive instance.
|
||
///
|
||
/// Float-Literal patterns are hard-rejected at typecheck
|
||
/// (`CheckError::FloatPatternNotAllowed`, per
|
||
/// design/contracts/0005-float-semantics.md) before this fn is reached,
|
||
/// so the `eq`-dispatch never encounters Float at runtime; the
|
||
/// Float arm of the match below remains for AST-level totality.
|
||
///
|
||
/// Unit literals are degenerate — every Unit value is equal — so an
|
||
/// emitted `true` literal stands in (no `eq` call needed).
|
||
fn build_eq(scrutinee: Term, lit: &Literal) -> Term {
|
||
match lit {
|
||
Literal::Unit => Term::Lit {
|
||
lit: Literal::Bool { value: true },
|
||
},
|
||
Literal::Int { .. }
|
||
| Literal::Bool { .. }
|
||
| Literal::Str { .. }
|
||
| Literal::Float { .. } => Term::App {
|
||
callee: Box::new(Term::Var {
|
||
name: "eq".to_string(),
|
||
}),
|
||
args: vec![scrutinee, Term::Lit { lit: lit.clone() }],
|
||
tail: false,
|
||
},
|
||
}
|
||
}
|
||
|
||
/// collect free variable names of `t`, with `bound` being
|
||
/// the set of names lexically bound at this point. A
|
||
/// [`Term::Var`] `{ name }` is "free" iff `name` is not in `bound`.
|
||
/// Compound binders ([`Term::Let`], [`Term::Lam`], [`Term::Match`]
|
||
/// arms, [`Term::LetRec`]) extend `bound` for the sub-walk.
|
||
///
|
||
/// Used by the [`Term::LetRec`] desugar to decide whether a local
|
||
/// recursive let would capture any name from the enclosing scope.
|
||
///
|
||
/// made `pub` so the post-typecheck `lift_letrecs` pass
|
||
/// in `ailang-check` can reuse it (same free-var computation; the
|
||
/// post-typecheck pass needs it to recompute captures of LetRec
|
||
/// nodes that desugar deferred).
|
||
pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<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 } => {
|
||
// `(clone X)` has the same free vars as `X`.
|
||
free_vars_in_term(value, bound, out);
|
||
}
|
||
Term::ReuseAs { source, body } => {
|
||
// free vars are the union of source and body.
|
||
free_vars_in_term(source, bound, out);
|
||
free_vars_in_term(body, bound, out);
|
||
}
|
||
Term::Loop { binders, body } => {
|
||
// loop-recur iter 1: binder names bind inside the loop —
|
||
// each init sees the outer env plus already-declared
|
||
// binders; the body sees all.
|
||
let mut b = bound.clone();
|
||
for bd in binders {
|
||
free_vars_in_term(&bd.init, &b, out);
|
||
b.insert(bd.name.clone());
|
||
}
|
||
free_vars_in_term(body, &b, out);
|
||
}
|
||
Term::Recur { args } => {
|
||
for a in args {
|
||
free_vars_in_term(a, bound, out);
|
||
}
|
||
}
|
||
Term::New { args, .. } => {
|
||
for arg in args {
|
||
match arg {
|
||
NewArg::Value(v) => free_vars_in_term(v, bound, out),
|
||
NewArg::Type(_) => {}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// collect every name a pattern binds into `out`. Mirrors
|
||
/// `Pattern::pattern_bound_names` in `ailang-codegen`; duplicated here
|
||
/// because `ailang-core` cannot depend on the codegen crate.
|
||
///
|
||
/// made `pub` so callers (e.g. `ailang-check::lift_letrecs`)
|
||
/// can reproduce the same shadowing semantics during their own
|
||
/// scope-aware walks.
|
||
pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet<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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// rewrite every free [`Term::Var`] `{ name == from }`
|
||
/// reference in `t` to [`Term::Var`] `{ name = to }`. Respects
|
||
/// shadowing: if a binder rebinds `from`, occurrences inside that
|
||
/// binder's scope stop being free and are left alone.
|
||
///
|
||
/// Used after lifting a [`Term::LetRec`] body to a synthetic top-level
|
||
/// fn — every recursive self-call site in the lifted body and every
|
||
/// reference in the in-term needs to point at the new global name.
|
||
///
|
||
/// made `pub` for the same reason as
|
||
/// [`subst_call_with_extras`].
|
||
pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
||
match t {
|
||
Term::Lit { .. } => t.clone(),
|
||
Term::Var { name } => {
|
||
if name == from {
|
||
Term::Var { name: to.to_string() }
|
||
} else {
|
||
t.clone()
|
||
}
|
||
}
|
||
Term::App { callee, args, tail } => Term::App {
|
||
callee: Box::new(subst_var(callee, from, to)),
|
||
args: args.iter().map(|a| subst_var(a, from, to)).collect(),
|
||
tail: *tail,
|
||
},
|
||
Term::Let { name, value, body } => {
|
||
let v = subst_var(value, from, to);
|
||
// If this `let` rebinds `from`, leave its body alone.
|
||
let b = if name == from {
|
||
(**body).clone()
|
||
} else {
|
||
subst_var(body, from, to)
|
||
};
|
||
Term::Let {
|
||
name: name.clone(),
|
||
value: Box::new(v),
|
||
body: Box::new(b),
|
||
}
|
||
}
|
||
Term::If { cond, then, else_ } => Term::If {
|
||
cond: Box::new(subst_var(cond, from, to)),
|
||
then: Box::new(subst_var(then, from, to)),
|
||
else_: Box::new(subst_var(else_, from, to)),
|
||
},
|
||
Term::Do { op, args, tail } => Term::Do {
|
||
op: op.clone(),
|
||
args: args.iter().map(|a| subst_var(a, from, to)).collect(),
|
||
tail: *tail,
|
||
},
|
||
Term::Ctor { type_name, ctor, args } => Term::Ctor {
|
||
type_name: type_name.clone(),
|
||
ctor: ctor.clone(),
|
||
args: args.iter().map(|a| subst_var(a, from, to)).collect(),
|
||
},
|
||
Term::Match { scrutinee, arms } => Term::Match {
|
||
scrutinee: Box::new(subst_var(scrutinee, from, to)),
|
||
arms: arms
|
||
.iter()
|
||
.map(|a| {
|
||
// Pattern-bound vars shadow `from` inside the arm.
|
||
let mut binds: BTreeSet<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 {
|
||
// structural recursion through the wrapper.
|
||
value: Box::new(subst_var(value, from, to)),
|
||
},
|
||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||
// structural recursion through both children.
|
||
source: Box::new(subst_var(source, from, to)),
|
||
body: Box::new(subst_var(body, from, to)),
|
||
},
|
||
Term::Loop { binders, body } => {
|
||
// loop-recur iter 1: binders lexically shadow outer
|
||
// names — once a binder named `from` is declared, later
|
||
// inits and the body stop being substituted.
|
||
let mut shadowed = false;
|
||
let new_binders: Vec<LoopBinder> = binders
|
||
.iter()
|
||
.map(|b| {
|
||
let init = if shadowed {
|
||
b.init.clone()
|
||
} else {
|
||
subst_var(&b.init, from, to)
|
||
};
|
||
if b.name == from {
|
||
shadowed = true;
|
||
}
|
||
LoopBinder {
|
||
name: b.name.clone(),
|
||
ty: b.ty.clone(),
|
||
init,
|
||
}
|
||
})
|
||
.collect();
|
||
let body_rw = if shadowed {
|
||
(**body).clone()
|
||
} else {
|
||
subst_var(body, from, to)
|
||
};
|
||
Term::Loop {
|
||
binders: new_binders,
|
||
body: Box::new(body_rw),
|
||
}
|
||
}
|
||
Term::Recur { args } => Term::Recur {
|
||
args: args.iter().map(|a| subst_var(a, from, to)).collect(),
|
||
},
|
||
Term::New { type_name, args } => Term::New {
|
||
type_name: type_name.clone(),
|
||
args: args
|
||
.iter()
|
||
.map(|arg| match arg {
|
||
NewArg::Value(v) => NewArg::Value(subst_var(v, from, to)),
|
||
NewArg::Type(t) => NewArg::Type(t.clone()),
|
||
})
|
||
.collect(),
|
||
},
|
||
}
|
||
}
|
||
|
||
/// peel a `Type::Forall { body: Type::Fn { ... } }` to
|
||
/// expose the inner `Type::Fn`. Returns `Some(&Type::Fn)` if the
|
||
/// type is `Fn` or `Forall<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,
|
||
}
|
||
}
|
||
|
||
/// walk `t` and rewrite every `Term::App { callee =
|
||
/// Var{name}, args }` to `Term::App { callee = Var{lifted}, args =
|
||
/// args ++ extras_as_vars }`. Recurses into all sub-terms. Does
|
||
/// not touch `Term::Var { name }` in non-callee positions — that's
|
||
/// flagged separately by [`find_non_callee_use`].
|
||
///
|
||
/// made `pub` so the post-typecheck `lift_letrecs` pass
|
||
/// in `ailang-check` can reuse it (same call-site rewrite shape; the
|
||
/// only difference is that the capture types come from the
|
||
/// typechecker's env rather than being statically known).
|
||
pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[String]) -> Term {
|
||
match t {
|
||
Term::Lit { .. } => t.clone(),
|
||
Term::Var { .. } => t.clone(),
|
||
Term::App { callee, args, tail } => {
|
||
let mut new_args: Vec<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 {
|
||
// structural recursion through the wrapper.
|
||
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
|
||
},
|
||
Term::ReuseAs { source, body } => Term::ReuseAs {
|
||
// structural recursion through both children.
|
||
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
|
||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||
},
|
||
Term::Loop { binders, body } => Term::Loop {
|
||
binders: binders
|
||
.iter()
|
||
.map(|b| LoopBinder {
|
||
name: b.name.clone(),
|
||
ty: b.ty.clone(),
|
||
init: subst_call_with_extras(&b.init, name, lifted, extras),
|
||
})
|
||
.collect(),
|
||
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
|
||
},
|
||
Term::Recur { args } => Term::Recur {
|
||
args: args
|
||
.iter()
|
||
.map(|a| subst_call_with_extras(a, name, lifted, extras))
|
||
.collect(),
|
||
},
|
||
Term::New { type_name, args } => Term::New {
|
||
type_name: type_name.clone(),
|
||
args: args
|
||
.iter()
|
||
.map(|arg| match arg {
|
||
NewArg::Value(v) => {
|
||
NewArg::Value(subst_call_with_extras(v, name, lifted, extras))
|
||
}
|
||
NewArg::Type(t) => NewArg::Type(t.clone()),
|
||
})
|
||
.collect(),
|
||
},
|
||
}
|
||
}
|
||
|
||
/// returns `Some(t)` if `t` contains a `Term::Var {
|
||
/// name }` reference in a position that is **not** the callee of a
|
||
/// `Term::App`. Returns `None` if every reference is in callee
|
||
/// position. Used by the LetRec lifter to enforce the "direct-call
|
||
/// only" restriction in 16b.2.
|
||
///
|
||
/// made `pub` for the same reason as
|
||
/// [`subst_call_with_extras`].
|
||
pub fn find_non_callee_use(t: &Term, name: &str) -> Option<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)),
|
||
// clone is identity for the non-callee scan.
|
||
Term::Clone { value } => find_non_callee_use(value, name),
|
||
// scan both source and body.
|
||
Term::ReuseAs { source, body } => {
|
||
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
|
||
}
|
||
Term::Loop { binders, body } => binders
|
||
.iter()
|
||
.find_map(|b| find_non_callee_use(&b.init, name))
|
||
.or_else(|| find_non_callee_use(body, name)),
|
||
Term::Recur { args } => {
|
||
args.iter().find_map(|a| find_non_callee_use(a, name))
|
||
}
|
||
Term::New { args, .. } => args.iter().find_map(|arg| match arg {
|
||
NewArg::Value(v) => find_non_callee_use(v, name),
|
||
NewArg::Type(_) => None,
|
||
}),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
/// Helper: walk a term, return true iff any [`Pattern::Ctor`] in
|
||
/// any reachable [`Term::Match`] has a non-flat field.
|
||
fn any_nested_ctor(t: &Term) -> bool {
|
||
match t {
|
||
Term::Lit { .. } | Term::Var { .. } => false,
|
||
Term::App { callee, args, .. } => {
|
||
any_nested_ctor(callee) || args.iter().any(any_nested_ctor)
|
||
}
|
||
Term::Let { value, body, .. } => any_nested_ctor(value) || any_nested_ctor(body),
|
||
Term::If { cond, then, else_ } => {
|
||
any_nested_ctor(cond) || any_nested_ctor(then) || any_nested_ctor(else_)
|
||
}
|
||
Term::Do { args, .. } => args.iter().any(any_nested_ctor),
|
||
Term::Ctor { args, .. } => args.iter().any(any_nested_ctor),
|
||
Term::Match { scrutinee, arms } => {
|
||
if any_nested_ctor(scrutinee) {
|
||
return true;
|
||
}
|
||
for a in arms {
|
||
if !is_flat(&a.pat) {
|
||
return true;
|
||
}
|
||
if any_nested_ctor(&a.body) {
|
||
return true;
|
||
}
|
||
}
|
||
false
|
||
}
|
||
Term::Lam { body, .. } => any_nested_ctor(body),
|
||
Term::Seq { lhs, rhs } => any_nested_ctor(lhs) || any_nested_ctor(rhs),
|
||
Term::LetRec { body, in_term, .. } => {
|
||
any_nested_ctor(body) || any_nested_ctor(in_term)
|
||
}
|
||
Term::Clone { value } => any_nested_ctor(value),
|
||
Term::ReuseAs { source, body } => any_nested_ctor(source) || any_nested_ctor(body),
|
||
Term::Loop { binders, body } => {
|
||
binders.iter().any(|b| any_nested_ctor(&b.init)) || any_nested_ctor(body)
|
||
}
|
||
Term::Recur { args } => args.iter().any(any_nested_ctor),
|
||
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
||
NewArg::Value(v) => any_nested_ctor(v),
|
||
NewArg::Type(_) => false,
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// Helper: walk a term, return true iff any [`Term::LetRec`] is
|
||
/// reachable. After 16b.1 desugaring this should be `false`.
|
||
fn any_let_rec(t: &Term) -> bool {
|
||
match t {
|
||
Term::Lit { .. } | Term::Var { .. } => false,
|
||
Term::App { callee, args, .. } => {
|
||
any_let_rec(callee) || args.iter().any(any_let_rec)
|
||
}
|
||
Term::Let { value, body, .. } => any_let_rec(value) || any_let_rec(body),
|
||
Term::If { cond, then, else_ } => {
|
||
any_let_rec(cond) || any_let_rec(then) || any_let_rec(else_)
|
||
}
|
||
Term::Do { args, .. } => args.iter().any(any_let_rec),
|
||
Term::Ctor { args, .. } => args.iter().any(any_let_rec),
|
||
Term::Match { scrutinee, arms } => {
|
||
any_let_rec(scrutinee) || arms.iter().any(|a| any_let_rec(&a.body))
|
||
}
|
||
Term::Lam { body, .. } => any_let_rec(body),
|
||
Term::Seq { lhs, rhs } => any_let_rec(lhs) || any_let_rec(rhs),
|
||
Term::LetRec { .. } => true,
|
||
Term::Clone { value } => any_let_rec(value),
|
||
Term::ReuseAs { source, body } => any_let_rec(source) || any_let_rec(body),
|
||
Term::Loop { binders, body } => {
|
||
binders.iter().any(|b| any_let_rec(&b.init)) || any_let_rec(body)
|
||
}
|
||
Term::Recur { args } => args.iter().any(any_let_rec),
|
||
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
||
NewArg::Value(v) => any_let_rec(v),
|
||
NewArg::Type(_) => false,
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// a `match` containing `(Cons a (Cons b _))` desugars to
|
||
/// a tree with no nested ctor sub-patterns. Property: the rewriter's
|
||
/// output never has a `Pattern::Ctor` whose field is itself a
|
||
/// `Pattern::Ctor`.
|
||
#[test]
|
||
fn nested_cons_cons_flattens() {
|
||
// Build: match xs of
|
||
// Cons a (Cons b _) -> ...
|
||
// _ -> ...
|
||
let body_match = Term::Match {
|
||
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
||
arms: vec![
|
||
Arm {
|
||
pat: Pattern::Ctor {
|
||
ctor: "Cons".into(),
|
||
fields: vec![
|
||
Pattern::Var { name: "a".into() },
|
||
Pattern::Ctor {
|
||
ctor: "Cons".into(),
|
||
fields: vec![
|
||
Pattern::Var { name: "b".into() },
|
||
Pattern::Wild,
|
||
],
|
||
},
|
||
],
|
||
},
|
||
body: Term::App {
|
||
callee: Box::new(Term::Var { name: "+".into() }),
|
||
args: vec![
|
||
Term::Var { name: "a".into() },
|
||
Term::Var { name: "b".into() },
|
||
],
|
||
tail: false,
|
||
},
|
||
},
|
||
Arm {
|
||
pat: Pattern::Wild,
|
||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||
},
|
||
],
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["xs".into()],
|
||
body: body_match,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
let body = match &out.defs[0] {
|
||
Def::Fn(f) => &f.body,
|
||
_ => unreachable!(),
|
||
};
|
||
assert!(
|
||
!any_nested_ctor(body),
|
||
"desugarer must remove every nested-ctor sub-pattern; got: {body:#?}"
|
||
);
|
||
}
|
||
|
||
/// A flat-only match round-trips structurally: there should be no
|
||
/// extra let-binding or chain wrapping.
|
||
#[test]
|
||
fn flat_match_is_unchanged_shape() {
|
||
let original = Term::Match {
|
||
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
||
arms: vec![
|
||
Arm {
|
||
pat: Pattern::Ctor {
|
||
ctor: "Nil".into(),
|
||
fields: vec![],
|
||
},
|
||
body: Term::Lit { lit: Literal::Int { value: 0 } },
|
||
},
|
||
Arm {
|
||
pat: Pattern::Ctor {
|
||
ctor: "Cons".into(),
|
||
fields: vec![
|
||
Pattern::Var { name: "h".into() },
|
||
Pattern::Var { name: "t".into() },
|
||
],
|
||
},
|
||
body: Term::Var { name: "h".into() },
|
||
},
|
||
],
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["xs".into()],
|
||
body: original.clone(),
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
let body = match &out.defs[0] {
|
||
Def::Fn(f) => f.body.clone(),
|
||
_ => unreachable!(),
|
||
};
|
||
// Already-flat match must round-trip with the same Match shape
|
||
// at the top level (no Let-wrap).
|
||
assert!(matches!(body, Term::Match { .. }));
|
||
}
|
||
|
||
/// a [`Term::LetRec`] whose body has no captures from
|
||
/// the enclosing scope is lifted to a synthetic top-level fn. The
|
||
/// resulting module's `defs` grows by one and the original LetRec
|
||
/// is gone from the term tree.
|
||
fn fact_letrec_term() -> Term {
|
||
// (let-rec fact (params n) (type ...) (body ...) (in (app fact 5)))
|
||
// Body just returns `n` so the test doesn't depend on `<=`/etc.
|
||
Term::LetRec {
|
||
name: "fact".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["n".into()],
|
||
body: Box::new(Term::Var { name: "n".into() }),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "fact".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 5 } }],
|
||
tail: false,
|
||
}),
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn let_rec_no_capture_lifts_to_top_level() {
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "main".into(),
|
||
ty: Type::Fn {
|
||
params: vec![],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec![],
|
||
body: fact_letrec_term(),
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
// One original + one lifted = two defs.
|
||
assert_eq!(out.defs.len(), 2, "expected one lifted fn appended");
|
||
// The new def is a fn whose name starts with the LetRec hint.
|
||
let lifted = match &out.defs[1] {
|
||
Def::Fn(f) => f,
|
||
_ => panic!("expected a lifted FnDef"),
|
||
};
|
||
assert!(
|
||
lifted.name.starts_with("fact$lr_"),
|
||
"lifted name `{}` should start with `fact$lr_`",
|
||
lifted.name
|
||
);
|
||
assert_eq!(lifted.params, vec!["n".to_string()]);
|
||
// The original main's body must no longer contain a LetRec.
|
||
let main_body = match &out.defs[0] {
|
||
Def::Fn(f) => &f.body,
|
||
_ => unreachable!(),
|
||
};
|
||
assert!(
|
||
!any_let_rec(main_body),
|
||
"desugarer must remove every Term::LetRec from the term tree; got: {main_body:#?}"
|
||
);
|
||
// The in-term of the LetRec was `(app fact 5)`; after the lift
|
||
// it should be `(app <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:?}"),
|
||
}
|
||
}
|
||
|
||
/// a LetRec whose body captures a fn-param of the
|
||
/// enclosing scope (here, `outer` is a parameter of `main`) is
|
||
/// **lifted** with the capture appended to the lifted fn's
|
||
/// signature. The original 16b.1 panic ("would capture") is
|
||
/// gone for fn-param captures; this test was renamed and
|
||
/// repurposed to assert the lift succeeds and produces the
|
||
/// expected augmented signature + call-site rewrite.
|
||
#[test]
|
||
fn let_rec_capture_fn_param_lifts_with_extra_arg() {
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "outer".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["n".into()],
|
||
// (let-rec helper (params x) (type Int -> Int)
|
||
// (body (app + x n)) -- captures `n`
|
||
// (in (app helper 5)))
|
||
body: Term::LetRec {
|
||
name: "helper".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["x".into()],
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "+".into() }),
|
||
args: vec![
|
||
Term::Var { name: "x".into() },
|
||
Term::Var { name: "n".into() },
|
||
],
|
||
tail: false,
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "helper".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 5 } }],
|
||
tail: false,
|
||
}),
|
||
},
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
// (a) two defs: original outer + lifted helper.
|
||
assert_eq!(out.defs.len(), 2, "expected one lifted fn appended");
|
||
let lifted = match &out.defs[1] {
|
||
Def::Fn(f) => f,
|
||
_ => panic!("expected lifted FnDef"),
|
||
};
|
||
// (b) lifted ty has the capture type appended.
|
||
assert_eq!(
|
||
lifted.ty,
|
||
Type::Fn {
|
||
params: vec![Type::int(), Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
"lifted fn type should have capture appended; got {:?}",
|
||
lifted.ty
|
||
);
|
||
// (b') param-name list has the capture name appended.
|
||
assert_eq!(lifted.params, vec!["x".to_string(), "n".to_string()]);
|
||
// (c) the lifted body is `(app + x n)` — `n` is now a
|
||
// fn-param of the lifted def, so the body is unchanged.
|
||
match &lifted.body {
|
||
Term::App { callee, args, .. } => {
|
||
match callee.as_ref() {
|
||
Term::Var { name } => assert_eq!(name, "+"),
|
||
other => panic!("expected `+` callee, got {other:?}"),
|
||
}
|
||
assert_eq!(args.len(), 2);
|
||
match &args[0] {
|
||
Term::Var { name } => assert_eq!(name, "x"),
|
||
other => panic!("expected x, got {other:?}"),
|
||
}
|
||
match &args[1] {
|
||
Term::Var { name } => assert_eq!(name, "n"),
|
||
other => panic!("expected n, got {other:?}"),
|
||
}
|
||
}
|
||
other => panic!("expected lifted body to be App, got {other:?}"),
|
||
}
|
||
// (d) `outer`'s body is now `(app helper$lr_0 5 n)` —
|
||
// call-site got the capture appended.
|
||
let outer_body = match &out.defs[0] {
|
||
Def::Fn(f) => &f.body,
|
||
_ => unreachable!(),
|
||
};
|
||
match outer_body {
|
||
Term::App { callee, args, .. } => {
|
||
match callee.as_ref() {
|
||
Term::Var { name } => assert_eq!(name, &lifted.name),
|
||
other => panic!("expected lifted callee, got {other:?}"),
|
||
}
|
||
assert_eq!(args.len(), 2, "expected 2 args (1 original + 1 capture)");
|
||
match &args[0] {
|
||
Term::Lit { lit: Literal::Int { value: 5 } } => {}
|
||
other => panic!("expected 5, got {other:?}"),
|
||
}
|
||
match &args[1] {
|
||
Term::Var { name } => assert_eq!(name, "n"),
|
||
other => panic!("expected n, got {other:?}"),
|
||
}
|
||
}
|
||
other => panic!("expected outer body to be App, got {other:?}"),
|
||
}
|
||
}
|
||
|
||
/// a LetRec whose body captures a `Term::Let`-bound
|
||
/// name is no longer rejected at desugar time. Instead the
|
||
/// desugar pass leaves the LetRec in place (with body and
|
||
/// in_term recursively desugared) so the post-typecheck pass in
|
||
/// `ailang-check` can lift it using the typechecker's resolved
|
||
/// types. The 16b.1/16b.2 panic for this shape is gone.
|
||
#[test]
|
||
fn let_rec_capture_let_binding_is_deferred_to_post_typecheck() {
|
||
// (let y 7 in (let-rec helper (params x) ... (body (app + x y))
|
||
// (in (app helper 1))))
|
||
let letrec = Term::LetRec {
|
||
name: "helper".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["x".into()],
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "+".into() }),
|
||
args: vec![
|
||
Term::Var { name: "x".into() },
|
||
Term::Var { name: "y".into() },
|
||
],
|
||
tail: false,
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "helper".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "main".into(),
|
||
ty: Type::Fn {
|
||
params: vec![],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec![],
|
||
body: Term::Let {
|
||
name: "y".into(),
|
||
value: Box::new(Term::Lit { lit: Literal::Int { value: 7 } }),
|
||
body: Box::new(letrec),
|
||
},
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
// No fn was lifted (the LetRec is left in place) — the
|
||
// module's defs count is unchanged.
|
||
assert_eq!(
|
||
out.defs.len(),
|
||
1,
|
||
"expected no lifted fn (LetRec deferred); got {:?}",
|
||
out.defs.iter().map(|d| d.name()).collect::<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:#?}"
|
||
);
|
||
}
|
||
|
||
/// a LetRec whose body captures a name bound by a
|
||
/// `Pattern::Ctor` Var sub-pattern (a `ScopeEntry::MatchArm`
|
||
/// binding) is no longer rejected at desugar time. The desugar
|
||
/// pass leaves the LetRec in place (with body and in_term
|
||
/// recursively desugared) so the post-typecheck pass in
|
||
/// `ailang-check` can lift it using the typechecker's resolved
|
||
/// types — including constructor-field substitution against
|
||
/// the matched ADT's type args. The 16b.3-era panic for this
|
||
/// shape is gone.
|
||
#[test]
|
||
fn let_rec_capture_match_arm_is_deferred_to_post_typecheck() {
|
||
// Module-level scope:
|
||
// (data Pair (vars a b) (ctor MkPair a b))
|
||
// (fn outer : (Pair Int Int) -> Int = \p.
|
||
// match p
|
||
// case (pat-ctor MkPair x y) ->
|
||
// let-rec helper : (Int) -> Int = \z. (+ z x)
|
||
// in (app helper 0)
|
||
// )
|
||
let pair_td = TypeDef {
|
||
name: "Pair".into(),
|
||
vars: vec!["a".into(), "b".into()],
|
||
ctors: vec![Ctor {
|
||
name: "MkPair".into(),
|
||
fields: vec![
|
||
Type::Var { name: "a".into() },
|
||
Type::Var { name: "b".into() },
|
||
],
|
||
}],
|
||
doc: None,
|
||
drop_iterative: false,
|
||
param_in: BTreeMap::new(),
|
||
};
|
||
let letrec = Term::LetRec {
|
||
name: "helper".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["z".into()],
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "+".into() }),
|
||
args: vec![
|
||
Term::Var { name: "z".into() },
|
||
Term::Var { name: "x".into() },
|
||
],
|
||
tail: false,
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "helper".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let outer_body = Term::Match {
|
||
scrutinee: Box::new(Term::Var { name: "p".into() }),
|
||
arms: vec![Arm {
|
||
pat: Pattern::Ctor {
|
||
ctor: "MkPair".into(),
|
||
fields: vec![
|
||
Pattern::Var { name: "x".into() },
|
||
Pattern::Var { name: "y".into() },
|
||
],
|
||
},
|
||
body: letrec,
|
||
}],
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![
|
||
Def::Type(pair_td),
|
||
Def::Fn(FnDef {
|
||
name: "outer".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::Con {
|
||
name: "Pair".into(),
|
||
args: vec![Type::int(), Type::int()],
|
||
}],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["p".into()],
|
||
body: outer_body,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
}),
|
||
],
|
||
};
|
||
let out = desugar_module(&m);
|
||
// No fn was lifted (the LetRec is deferred) — the module's
|
||
// defs count is unchanged at 2 (Pair + outer).
|
||
assert_eq!(
|
||
out.defs.len(),
|
||
2,
|
||
"expected no lifted fn (LetRec deferred); got {:?}",
|
||
out.defs.iter().map(|d| d.name()).collect::<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:#?}"
|
||
);
|
||
}
|
||
|
||
/// name-as-value INSIDE the LetRec's own body is
|
||
/// still rejected (would require an eta-Lam that calls the
|
||
/// pre-lift name — chicken-and-egg with the call-site rewrite).
|
||
/// Here `(let g f ...)` binds the LetRec name `f` to a let-bound
|
||
/// name `g`, which is a value position — not a callee.
|
||
#[test]
|
||
#[should_panic(expected = "16b.5")]
|
||
fn let_rec_name_as_value_in_body_panics() {
|
||
// (let-rec f (params x) (type Int->Int)
|
||
// (body (let g f (app g x))) -- f used as a value INSIDE body
|
||
// (in (app f 1)))
|
||
let letrec = Term::LetRec {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["x".into()],
|
||
body: Box::new(Term::Let {
|
||
name: "g".into(),
|
||
value: Box::new(Term::Var { name: "f".into() }),
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "g".into() }),
|
||
args: vec![Term::Var { name: "x".into() }],
|
||
tail: false,
|
||
}),
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "f".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "main".into(),
|
||
ty: Type::Fn {
|
||
params: vec![],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec![],
|
||
body: letrec,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let _ = desugar_module(&m);
|
||
}
|
||
|
||
/// name-as-value in the in-clause is now SUPPORTED.
|
||
/// The desugar pass lifts the LetRec to a synthetic top-level fn
|
||
/// AND wraps the rewritten in-term in `(let f (lam ...) ...)`
|
||
/// whose lam eta-expands the lifted fn. After desugar, the
|
||
/// surviving term tree contains:
|
||
/// - one lifted fn `f$lr_N`
|
||
/// - inside `main`, a `Term::Let { name: "f", value: Term::Lam,
|
||
/// body: <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(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "main".into(),
|
||
ty: Type::Fn {
|
||
params: vec![],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec![],
|
||
body: letrec,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
// Two defs: original `main` + lifted `f$lr_0`.
|
||
assert_eq!(
|
||
out.defs.len(),
|
||
2,
|
||
"expected one lifted fn appended; got {:?}",
|
||
out.defs.iter().map(|d| d.name()).collect::<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:?}"),
|
||
}
|
||
}
|
||
|
||
/// a LetRec inside a polymorphic enclosing fn gets its
|
||
/// fn-param captures lifted with a `Forall(vars, Fn(...))` augmented
|
||
/// signature, mirroring the enclosing fn's type vars. Without 16b.6,
|
||
/// the desugar pass would have classified the fn-params as
|
||
/// `LetBound` (16b.2 fallback) and deferred to `lift_letrecs`; with
|
||
/// 16b.6, the fn-params are `KnownType` and the fast-path lift fires
|
||
/// here, producing a `Forall`-typed synthetic FnDef.
|
||
#[test]
|
||
fn let_rec_capture_under_polymorphic_enclosing_fn_lifts_to_forall_fn() {
|
||
// (fn id_n_times : Forall(a). Fn(Int, a) -> a
|
||
// (params n x)
|
||
// (body
|
||
// (let-rec loop : Fn(Int, a) -> a (params k acc)
|
||
// (body (if (== k 0) acc (app loop (- k 1) acc)))
|
||
// (in (app loop n x)))))
|
||
// Captures: none (all references inside the body are to params
|
||
// of `loop` itself or top-level builtins). To force a capture
|
||
// of an `a`-typed param, use the simpler shape:
|
||
//
|
||
// (fn rec_id : Forall(a). Fn(a) -> a
|
||
// (params x)
|
||
// (body
|
||
// (let-rec loop : Fn(Int) -> a (params k)
|
||
// (body (if (== k 0) x (app loop (- k 1))))
|
||
// (in (app loop 1)))))
|
||
//
|
||
// Here `loop` captures `x: a` from the enclosing fn's params.
|
||
// Without 16b.6, `x` would be ScopeEntry::LetBound (because the
|
||
// enclosing fn is Forall) and the LetRec would defer; with
|
||
// 16b.6, `x` is KnownType and the lift fires here.
|
||
let letrec = Term::LetRec {
|
||
name: "loop".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::Var { name: "a".into() }),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["k".into()],
|
||
// 2026-05-21 operator-routing-eq-ord: keep `==` here (vs
|
||
// migrating to `eq`) because this AST literal sits in an
|
||
// in-source mod test for desugar with no prelude
|
||
// injection; `eq` would be unbound. After Task 7 deletes
|
||
// `==`, this test gets deleted as obsolete; the desugar
|
||
// shape is exercised end-to-end via the workspace flow.
|
||
body: Box::new(Term::If {
|
||
cond: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "==".into() }),
|
||
args: vec![
|
||
Term::Var { name: "k".into() },
|
||
Term::Lit { lit: Literal::Int { value: 0 } },
|
||
],
|
||
tail: false,
|
||
}),
|
||
then: Box::new(Term::Var { name: "x".into() }),
|
||
else_: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "loop".into() }),
|
||
args: vec![Term::App {
|
||
callee: Box::new(Term::Var { name: "-".into() }),
|
||
args: vec![
|
||
Term::Var { name: "k".into() },
|
||
Term::Lit { lit: Literal::Int { value: 1 } },
|
||
],
|
||
tail: false,
|
||
}],
|
||
tail: false,
|
||
}),
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "loop".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "rec_id".into(),
|
||
ty: Type::Forall {
|
||
vars: vec!["a".into()],
|
||
constraints: vec![],
|
||
body: Box::new(Type::Fn {
|
||
params: vec![Type::Var { name: "a".into() }],
|
||
ret: Box::new(Type::Var { name: "a".into() }),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
}),
|
||
},
|
||
params: vec!["x".into()],
|
||
body: letrec,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
assert_eq!(
|
||
out.defs.len(),
|
||
2,
|
||
"expected one lifted fn appended; got {:?}",
|
||
out.defs.iter().map(|d| d.name()).collect::<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!(¶ms[0], Type::Con { name, .. } if name == "Int"),
|
||
"first param must be Int (loop's own k); got {:?}",
|
||
params[0]
|
||
);
|
||
assert!(
|
||
matches!(¶ms[1], Type::Var { name } if name == "a"),
|
||
"second param must be the captured `x: a`; got {:?}",
|
||
params[1]
|
||
);
|
||
assert!(
|
||
matches!(ret.as_ref(), Type::Var { name } if name == "a"),
|
||
"ret must be `a`; got {:?}",
|
||
ret
|
||
);
|
||
}
|
||
other => panic!("Forall body must be Fn; got {:?}", other),
|
||
}
|
||
}
|
||
other => panic!("lifted type must be Forall; got {:?}", other),
|
||
}
|
||
// Lifted params: original "k" + captured "x".
|
||
assert_eq!(lifted.params, vec!["k".to_string(), "x".to_string()]);
|
||
}
|
||
|
||
/// a LetRec inside a polymorphic enclosing fn whose
|
||
/// `in_term` uses the LetRec name as a value (not as a callee) is
|
||
/// rejected. The 16b.5 eta-Lam wrap can't quantify `Forall.vars`
|
||
/// because `Term::Lam` has no Forall slot — wrapping a polymorphic
|
||
/// lifted fn into a monomorphic Lam loses the type vars. Queued
|
||
/// separately as `closure-poly` (informally 16b.5b).
|
||
#[test]
|
||
#[should_panic(expected = "16b.6")]
|
||
fn let_rec_name_as_value_in_polymorphic_enclosing_fn_panics() {
|
||
// Same shape as `let_rec_name_as_value_in_in_term_wraps_to_eta_lam`
|
||
// but the enclosing fn is Forall-quantified.
|
||
let letrec = Term::LetRec {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::Var { name: "a".into() }),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["k".into()],
|
||
body: Box::new(Term::Var { name: "x".into() }),
|
||
in_term: Box::new(Term::Let {
|
||
name: "g".into(),
|
||
value: Box::new(Term::Var { name: "f".into() }), // value-position f
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "g".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
|
||
tail: false,
|
||
}),
|
||
}),
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "outer".into(),
|
||
ty: Type::Forall {
|
||
vars: vec!["a".into()],
|
||
constraints: vec![],
|
||
body: Box::new(Type::Fn {
|
||
params: vec![Type::Var { name: "a".into() }],
|
||
ret: Box::new(Type::Var { name: "a".into() }),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
}),
|
||
},
|
||
params: vec!["x".into()],
|
||
body: letrec,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let _ = desugar_module(&m);
|
||
}
|
||
|
||
/// a nested LetRec whose inner LetRec captures the
|
||
/// OUTER LetRec's NAME (not its params or any other local) is
|
||
/// rejected at desugar time with the closure-of-self message.
|
||
/// Capturing the outer's PARAMS is supported (covered by the
|
||
/// `nested_let_rec` fixture / e2e); capturing the NAME is the
|
||
/// chicken-and-egg case where the outer's value form does not
|
||
/// exist before the outer's lift completes.
|
||
#[test]
|
||
#[should_panic(expected = "16b.7")]
|
||
fn nested_let_rec_inner_captures_outer_name_panics() {
|
||
// Outer LetRec body contains an inner LetRec that references
|
||
// `outer` (the outer LetRec's own name) inside its body — as
|
||
// a callee, even, but the panic fires on the capture
|
||
// classification, not on a non-callee check.
|
||
//
|
||
// Shape inside main:
|
||
// (let-rec outer (params i) (type Int->Int)
|
||
// (body (let-rec inner (params j) (type Int->Int)
|
||
// (body (app outer j)) ;; inner captures outer-NAME
|
||
// (in (app inner 0))))
|
||
// (in (app outer 1)))
|
||
let inner = Term::LetRec {
|
||
name: "inner".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["j".into()],
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "outer".into() }),
|
||
args: vec![Term::Var { name: "j".into() }],
|
||
tail: false,
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "inner".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let outer = Term::LetRec {
|
||
name: "outer".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["i".into()],
|
||
body: Box::new(inner),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "outer".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "main".into(),
|
||
ty: Type::Fn {
|
||
params: vec![],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec![],
|
||
body: outer,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let _ = desugar_module(&m);
|
||
}
|
||
|
||
/// a nested LetRec whose inner
|
||
/// LetRec captures the OUTER LetRec's PARAM (not its name) lifts
|
||
/// cleanly through the existing 16b.2 fast path. The outer's
|
||
/// params live in the inner's outer-scope as `KnownType`, so the
|
||
/// inner classifies them like any other fn-param capture and
|
||
/// appends them to its lifted signature. The post-order traversal
|
||
/// then lifts the outer next; references to the outer's params
|
||
/// inside the inner-call rewrites stay in scope (the outer hasn't
|
||
/// renamed its params yet).
|
||
#[test]
|
||
fn nested_let_rec_inner_captures_outer_param_lifts() {
|
||
// Shape inside main:
|
||
// (let-rec outer (params i) (type Int->Int)
|
||
// (body (let-rec inner (params j) (type Int->Int)
|
||
// (body (app + i j)) ;; inner captures outer-PARAM i
|
||
// (in (app inner 0))))
|
||
// (in (app outer 1)))
|
||
let inner = Term::LetRec {
|
||
name: "inner".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["j".into()],
|
||
body: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "+".into() }),
|
||
args: vec![
|
||
Term::Var { name: "i".into() },
|
||
Term::Var { name: "j".into() },
|
||
],
|
||
tail: false,
|
||
}),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "inner".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let outer = Term::LetRec {
|
||
name: "outer".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["i".into()],
|
||
body: Box::new(inner),
|
||
in_term: Box::new(Term::App {
|
||
callee: Box::new(Term::Var { name: "outer".into() }),
|
||
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
|
||
tail: false,
|
||
}),
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "main".into(),
|
||
ty: Type::Fn {
|
||
params: vec![],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec![],
|
||
body: outer,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
// Three defs: original `main` + lifted `inner$lr_0` + lifted
|
||
// `outer$lr_1`. Inner is lifted first (post-order), with outer's
|
||
// param `i` appended; outer is lifted next, with no captures.
|
||
let names: Vec<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]");
|
||
}
|
||
}
|
||
|
||
/// walks a term, returns true iff any [`Pattern::Lit`] is
|
||
/// reachable in any [`Term::Match`] arm. After 16c desugaring, the
|
||
/// invariant is `false` for every desugared output — `Pattern::Lit`
|
||
/// is gone before typecheck/codegen runs.
|
||
fn any_lit_pattern(t: &Term) -> bool {
|
||
fn pat_has_lit(p: &Pattern) -> bool {
|
||
match p {
|
||
Pattern::Wild | Pattern::Var { .. } => false,
|
||
Pattern::Lit { .. } => true,
|
||
Pattern::Ctor { fields, .. } => fields.iter().any(pat_has_lit),
|
||
}
|
||
}
|
||
match t {
|
||
Term::Lit { .. } | Term::Var { .. } => false,
|
||
Term::App { callee, args, .. } => {
|
||
any_lit_pattern(callee) || args.iter().any(any_lit_pattern)
|
||
}
|
||
Term::Let { value, body, .. } => any_lit_pattern(value) || any_lit_pattern(body),
|
||
Term::If { cond, then, else_ } => {
|
||
any_lit_pattern(cond) || any_lit_pattern(then) || any_lit_pattern(else_)
|
||
}
|
||
Term::Do { args, .. } => args.iter().any(any_lit_pattern),
|
||
Term::Ctor { args, .. } => args.iter().any(any_lit_pattern),
|
||
Term::Match { scrutinee, arms } => {
|
||
if any_lit_pattern(scrutinee) {
|
||
return true;
|
||
}
|
||
for a in arms {
|
||
if pat_has_lit(&a.pat) {
|
||
return true;
|
||
}
|
||
if any_lit_pattern(&a.body) {
|
||
return true;
|
||
}
|
||
}
|
||
false
|
||
}
|
||
Term::Lam { body, .. } => any_lit_pattern(body),
|
||
Term::Seq { lhs, rhs } => any_lit_pattern(lhs) || any_lit_pattern(rhs),
|
||
Term::LetRec { body, in_term, .. } => {
|
||
any_lit_pattern(body) || any_lit_pattern(in_term)
|
||
}
|
||
Term::Clone { value } => any_lit_pattern(value),
|
||
Term::ReuseAs { source, body } => any_lit_pattern(source) || any_lit_pattern(body),
|
||
Term::Loop { binders, body } => {
|
||
binders.iter().any(|b| any_lit_pattern(&b.init)) || any_lit_pattern(body)
|
||
}
|
||
Term::Recur { args } => args.iter().any(any_lit_pattern),
|
||
Term::New { args, .. } => args.iter().any(|arg| match arg {
|
||
NewArg::Value(v) => any_lit_pattern(v),
|
||
NewArg::Type(_) => false,
|
||
}),
|
||
}
|
||
}
|
||
|
||
/// a top-level lit arm desugars to a `Term::If` whose
|
||
/// condition is `(== s_var lit)`. The desugared body must contain
|
||
/// no `Pattern::Lit` anywhere.
|
||
#[test]
|
||
fn top_level_lit_desugars_to_if() {
|
||
// (match n
|
||
// (case (pat-lit 0) 100)
|
||
// (case _ 999))
|
||
let body_match = Term::Match {
|
||
scrutinee: Box::new(Term::Var { name: "n".into() }),
|
||
arms: vec![
|
||
Arm {
|
||
pat: Pattern::Lit {
|
||
lit: Literal::Int { value: 0 },
|
||
},
|
||
body: Term::Lit {
|
||
lit: Literal::Int { value: 100 },
|
||
},
|
||
},
|
||
Arm {
|
||
pat: Pattern::Wild,
|
||
body: Term::Lit {
|
||
lit: Literal::Int { value: 999 },
|
||
},
|
||
},
|
||
],
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["n".into()],
|
||
body: body_match,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
let body = match &out.defs[0] {
|
||
Def::Fn(f) => &f.body,
|
||
_ => unreachable!(),
|
||
};
|
||
assert!(
|
||
!any_lit_pattern(body),
|
||
"desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}"
|
||
);
|
||
// The desugar wraps the match in `let $mp_N = scrutinee in <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:?}"
|
||
);
|
||
}
|
||
|
||
/// a `(pat-ctor Cons (pat-lit 0) _)` sub-pattern desugars
|
||
/// to a tree with no `Pattern::Lit` anywhere — the lit is rewritten
|
||
/// to a `Term::If` against the field-bound fresh variable.
|
||
#[test]
|
||
fn nested_lit_in_ctor_desugars_to_if() {
|
||
// (match xs
|
||
// (case (pat-ctor Cons (pat-lit 0) _) 0)
|
||
// (case _ -1))
|
||
let body_match = Term::Match {
|
||
scrutinee: Box::new(Term::Var { name: "xs".into() }),
|
||
arms: vec![
|
||
Arm {
|
||
pat: Pattern::Ctor {
|
||
ctor: "Cons".into(),
|
||
fields: vec![
|
||
Pattern::Lit {
|
||
lit: Literal::Int { value: 0 },
|
||
},
|
||
Pattern::Wild,
|
||
],
|
||
},
|
||
body: Term::Lit {
|
||
lit: Literal::Int { value: 0 },
|
||
},
|
||
},
|
||
Arm {
|
||
pat: Pattern::Wild,
|
||
body: Term::Lit {
|
||
lit: Literal::Int { value: -1 },
|
||
},
|
||
},
|
||
],
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["xs".into()],
|
||
body: body_match,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
let body = match &out.defs[0] {
|
||
Def::Fn(f) => &f.body,
|
||
_ => unreachable!(),
|
||
};
|
||
assert!(
|
||
!any_lit_pattern(body),
|
||
"desugarer must remove every Pattern::Lit from the term tree; got: {body:#?}"
|
||
);
|
||
assert!(
|
||
!any_nested_ctor(body),
|
||
"desugarer must also produce no nested ctor sub-patterns; got: {body:#?}"
|
||
);
|
||
}
|
||
|
||
/// `is_flat` must classify
|
||
/// `Pattern::Lit` as **not** flat, so the chain machinery in
|
||
/// [`Desugarer::desugar_match`] is always invoked for lit-arms
|
||
/// (rather than the early-return that would leak the lit pattern
|
||
/// through to typecheck/codegen).
|
||
#[test]
|
||
fn flat_arm_with_lit_is_no_longer_flat() {
|
||
let p = Pattern::Lit {
|
||
lit: Literal::Int { value: 0 },
|
||
};
|
||
assert!(
|
||
!is_flat(&p),
|
||
"Pattern::Lit must be classified as non-flat after 16c"
|
||
);
|
||
}
|
||
|
||
/// the chain machinery's deepest fall-through is the
|
||
/// polymorphic `__unreachable__` builtin (`forall a. a`), not a
|
||
/// `Unit` literal. A match with two lit arms and a wildcard
|
||
/// catches all paths via the wild-arm body, so the deepest
|
||
/// `else_` of the chain is the `__unreachable__` var.
|
||
#[test]
|
||
fn chain_default_is_unreachable_builtin() {
|
||
// (match n (case (pat-lit 0) 100) (case (pat-lit 1) 200))
|
||
// Note: no wildcard on purpose — the chain reaches the
|
||
// synthetic terminator after both lit arms fail.
|
||
let body_match = Term::Match {
|
||
scrutinee: Box::new(Term::Var { name: "n".into() }),
|
||
arms: vec![
|
||
Arm {
|
||
pat: Pattern::Lit {
|
||
lit: Literal::Int { value: 0 },
|
||
},
|
||
body: Term::Lit {
|
||
lit: Literal::Int { value: 100 },
|
||
},
|
||
},
|
||
Arm {
|
||
pat: Pattern::Lit {
|
||
lit: Literal::Int { value: 1 },
|
||
},
|
||
body: Term::Lit {
|
||
lit: Literal::Int { value: 200 },
|
||
},
|
||
},
|
||
],
|
||
};
|
||
let m = Module {
|
||
schema: crate::SCHEMA.to_string(),
|
||
name: "t".into(),
|
||
kernel: false,
|
||
imports: vec![],
|
||
defs: vec![Def::Fn(FnDef {
|
||
name: "f".into(),
|
||
ty: Type::Fn {
|
||
params: vec![Type::int()],
|
||
ret: Box::new(Type::int()),
|
||
effects: vec![],
|
||
param_modes: vec![],
|
||
ret_mode: ParamMode::Implicit,
|
||
},
|
||
params: vec!["n".into()],
|
||
body: body_match,
|
||
suppress: vec![],
|
||
doc: None,
|
||
export: None,
|
||
})],
|
||
};
|
||
let out = desugar_module(&m);
|
||
let body = match &out.defs[0] {
|
||
Def::Fn(f) => &f.body,
|
||
_ => unreachable!(),
|
||
};
|
||
// Outer is `let $mp_N = scrutinee in <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:?}"
|
||
);
|
||
}
|
||
}
|