Iter 16b.6: LetRec inside polymorphic enclosing fn

Lifts the monomorphic-only restriction. Lifted top-level fn now
becomes Forall(α..., Fn(t..., T...) -> tr) when the enclosing fn
is polymorphic; capture types may mention outer type vars and
the existing mono pipeline (Iter 12b/14a) specializes them
correctly at call sites.

- desugar.rs / lift.rs: scope-building unified — Forall fn-params
  are now KnownType, not LetBound. Lift wraps augmented_ty in
  Forall mirroring the enclosing fn.
- Name-as-value-in-in-term in a polymorphic enclosing fn is
  rejected (eta-Lam wrap from 16b.5 has no Forall). Queued
  separately as closure-conversion-with-polymorphism.
- examples/poly_rec_capture.{ailx,ail.json}: apply_n_times :
  Forall(a). drives at Int and Bool to exercise two mono
  instantiations.
- e2e + check + desugar tests: 116 → 120 (+4).

Codegen pipeline untouched — apply_subst_to_type substitutes
through Fn.params uniformly (original params + appended captures).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-07 22:39:28 +02:00
parent ca507c9f52
commit 76d61aeced
7 changed files with 772 additions and 28 deletions
+28
View File
@@ -537,6 +537,34 @@ fn local_rec_as_value_capture_demo() {
assert_eq!(lines, vec!["1320", "12120"]);
}
/// Iter 16b.6: LetRec inside a polymorphic enclosing fn.
/// Property protected: when the enclosing fn is `Forall(a). Fn(...)`,
/// the desugar/lift pipeline now (a) enters fn-params as `KnownType`
/// (not `LetBound`) so a LetRec inside CAN capture them, and (b)
/// builds the synthetic lifted fn's signature as
/// `Forall(a). Fn(t1..tk, T1..Tm) -> tr`, mirroring the enclosing
/// fn's type vars. Capture types `T1..Tm` may mention `a`; the
/// outer Forall binds them. Codegen's mono pipeline (Iter 12b/14a)
/// then specialises `loop$lr_0` at every call with the same type
/// args as the enclosing `apply_n_times`.
///
/// `main` drives `apply_n_times` at TWO distinct instantiations
/// (`Int` with `succ`, `Bool` with `flip`) so the mono path is
/// exercised twice, producing both `loop$lr_0__I` and
/// `loop$lr_0__B` in the IR. Without 16b.6 the desugar pass would
/// have rejected the LetRec capture as a `LetBound`-style failure
/// (the 16b.2-era `Type::Forall` fallback marked all fn-params as
/// `LetBound`).
///
/// Expected stdout: 5 (succ applied 5 times to 0), false (flip
/// applied 4 times to false — even count of flips ⇒ false).
#[test]
fn poly_rec_capture_demo() {
let stdout = build_and_run("poly_rec_capture.ail.json");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["5", "false"]);
}
/// Guards `ail diff`: a modified body changes the hash of `sum`, while
/// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
/// `sum`, `unchanged` contains `main`, `added`/`removed` empty.
+162
View File
@@ -3394,4 +3394,166 @@ mod tests {
);
assert_eq!(synth.params, vec!["z".to_string(), "x".to_string()]);
}
/// Iter 16b.6: post-typecheck `lift_letrecs` lifts a deferred
/// LetRec inside a polymorphic enclosing fn into a `Forall`-typed
/// synthetic top-level fn, mirroring the enclosing fn's type
/// vars. Property protected:
///
/// 1. The fast-path desugar lift handles the `KnownType`-only
/// case end-to-end (see desugar's
/// `let_rec_capture_under_polymorphic_enclosing_fn_lifts_to_forall_fn`).
/// 2. The defer path (some capture is `Term::Let`-bound) goes
/// through `lift_letrecs`. This test forces that path by
/// introducing a `Term::Let { z = 7 }` inside the body and
/// capturing `z`. The lifted fn must still be `Forall(a). Fn(...)`,
/// even though the lift happens post-typecheck rather than
/// in desugar.
///
/// Without 16b.6, lift_letrecs would have panicked on the
/// `Type::Forall` match arm at lift-construction time.
#[test]
fn lift_letrecs_under_polymorphic_enclosing_fn_produces_forall_fn() {
// fn outer : Forall(a). (a) -> a = \x.
// let z = 7
// in let-rec helper : (Int) -> a = \k. if k == 0 then x else helper(k-1) + z (impossible — types don't match)
//
// Simpler shape: capture z (Let-bound, Int) in a polymorphic
// enclosing fn, with a body that returns x (the polymorphic
// capture).
//
// fn outer : Forall(a). (a) -> a = \x.
// let z = 7
// in let-rec helper : (Int) -> a = \k.
// if k == 0 then x else helper(k - z)
// in helper 1
let m = Module {
schema: SCHEMA.into(),
name: "t".into(),
imports: vec![],
defs: vec![fn_def(
"outer",
Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
}),
},
vec!["x".into()],
Term::Let {
name: "z".into(),
value: Box::new(Term::Lit { lit: Literal::Int { value: 7 } }),
body: Box::new(Term::LetRec {
name: "helper".into(),
ty: Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
},
params: vec!["k".into()],
body: Box::new(Term::If {
cond: Box::new(Term::App {
callee: Box::new(Term::Var { name: "==".into() }),
args: vec![
Term::Var { name: "k".into() },
Term::Lit { lit: Literal::Int { value: 0 } },
],
tail: false,
}),
then: Box::new(Term::Var { name: "x".into() }),
else_: Box::new(Term::App {
callee: Box::new(Term::Var { name: "helper".into() }),
args: vec![Term::App {
callee: Box::new(Term::Var { name: "-".into() }),
args: vec![
Term::Var { name: "k".into() },
Term::Var { name: "z".into() },
],
tail: false,
}],
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,
}),
}),
},
)],
};
// Typecheck succeeds.
check(&m).expect("typecheck before lift");
let desugared = ailang_core::desugar::desugar_module(&m);
// After desugar the LetRec should still be present (the
// capture set is `{x: KnownType, z: LetBound}` and any
// LetBound forces deferral).
let lifted = lift_letrecs(&desugared).expect("lift_letrecs");
assert_eq!(
lifted.defs.len(),
2,
"expected one synthetic fn appended; got {:?}",
lifted.defs.iter().map(|d| d.name()).collect::<Vec<_>>()
);
let synth = match &lifted.defs[1] {
Def::Fn(f) => f,
_ => panic!("expected synthetic FnDef"),
};
assert!(
synth.name.starts_with("helper$lr_"),
"lifted name `{}` should start with `helper$lr_`",
synth.name
);
// Lifted ty must be Forall(a). Fn(Int, a, Int) -> a.
// Original LetRec params: [Int]; captures (BTreeSet order):
// x: a then z: Int (alphabetical). Ret: a.
match &synth.ty {
Type::Forall { vars, body } => {
assert_eq!(
vars,
&vec!["a".to_string()],
"Forall vars must mirror enclosing fn's vars; got {:?}",
vars
);
match body.as_ref() {
Type::Fn { params, ret, .. } => {
assert_eq!(params.len(), 3, "expected k + x + z params");
assert!(
matches!(&params[0], Type::Con { name, .. } if name == "Int"),
"first param: original k:Int; got {:?}",
params[0]
);
// Captures are in BTreeSet order, so x (a-typed) comes before z (Int).
assert!(
matches!(&params[1], Type::Var { name } if name == "a"),
"second param: x: a; got {:?}",
params[1]
);
assert!(
matches!(&params[2], Type::Con { name, .. } if name == "Int"),
"third param: z: Int; got {:?}",
params[2]
);
assert!(
matches!(ret.as_ref(), Type::Var { name } if name == "a"),
"ret: a; got {:?}",
ret
);
}
other => panic!("Forall body must be Fn; got {:?}", other),
}
}
other => panic!(
"lifted type must be Forall (mirroring enclosing); got {:?}",
other
),
}
assert_eq!(
synth.params,
vec!["k".to_string(), "x".to_string(), "z".to_string()]
);
}
}
+61 -3
View File
@@ -130,6 +130,7 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
counter,
module_names: &mut module_names,
lifted: Vec::new(),
current_def_forall_vars: Vec::new(),
};
let _ = &mut counter; // counter lives in lifter from here on
@@ -164,7 +165,17 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
}
}
}
// Iter 16b.6: stash the enclosing fn's Forall.vars (or
// empty for a monomorphic enclosing fn) for the LetRec
// arm. Save and restore so it never leaks across defs.
let saved_forall_vars =
std::mem::take(&mut lifter.current_def_forall_vars);
lifter.current_def_forall_vars = match &f.ty {
Type::Forall { vars, .. } => vars.clone(),
_ => Vec::new(),
};
f.body = lifter.lift_in_term(&f.body, &mut locals, &f.name)?;
lifter.current_def_forall_vars = saved_forall_vars;
for v in rigids_added {
lifter.env.rigid_vars.remove(&v);
}
@@ -184,11 +195,22 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
/// State for a single-module lift pass. Mirrors `Desugarer` in
/// `ailang-core::desugar` but resolves capture types via `synth`
/// instead of relying on a `ScopeEntry` map.
///
/// Iter 16b.6 field:
/// - `current_def_forall_vars` carries the enclosing fn's
/// `Type::Forall.vars` while lifting its body. Empty for monomorphic
/// enclosing fns. Read by the LetRec arm to wrap the synthetic
/// lifted fn's signature in `Type::Forall` mirroring the enclosing
/// fn — the lifted fn enters codegen's monomorphisation queue at
/// every call site, specialising at the same type args as its host.
struct Lifter<'a> {
env: Env,
counter: u64,
module_names: &'a mut BTreeSet<String>,
lifted: Vec<Def>,
/// Iter 16b.6: enclosing fn's Forall.vars (or empty if mono).
/// Reset on entry to each `Def::Fn`.
current_def_forall_vars: Vec<String>,
}
impl<'a> Lifter<'a> {
@@ -393,6 +415,22 @@ impl<'a> Lifter<'a> {
// is built below after we know `lifted_name` and `extras`.
let in_has_value_use = find_non_callee_use(&lifted_in, name).is_some();
// Iter 16b.6: reject name-as-value in `in_term` when the
// enclosing fn is polymorphic — the eta-Lam wrap would
// need to be `Forall`-quantified, but `Term::Lam` has no
// such slot. Closure conversion with polymorphism is the
// proper fix; queued as `closure-poly` (informally
// 16b.5b). Mono enclosing fn + name-as-value-in-in-term
// continues 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 `{name}` is not yet \
supported in a polymorphic enclosing fn — closure conversion \
with polymorphism would be required. Queued separately as \
`closure-poly` (informally 16b.5b)."
);
}
// Recompute captures of the lifted body against the
// current `locals` (deterministic order via BTreeSet).
let mut local_bound: BTreeSet<String> = BTreeSet::new();
@@ -422,7 +460,19 @@ impl<'a> Lifter<'a> {
}
// Build the lifted FnDef.
let augmented_ty = match ty {
//
// Iter 16b.6: when the enclosing fn is polymorphic, wrap
// the augmented Fn in a `Type::Forall` mirroring the
// enclosing fn's type vars (`current_def_forall_vars`).
// The capture types may mention any of those vars; the
// Forall binds them. Codegen's mono pipeline (Iter
// 12b/14a) specialises `f$lr_N` at every call site with
// the same type args as the enclosing fn's current
// mono — see `apply_subst_to_type`, which substitutes
// through both the original params and the appended
// capture types uniformly because they're all `Type::Fn`
// params at the AST level.
let inner_augmented_ty = match ty {
Type::Fn { params: ps, ret, effects } => {
let mut new_ps = ps.clone();
for (_, t) in &capture_types {
@@ -435,14 +485,22 @@ impl<'a> Lifter<'a> {
}
}
Type::Forall { .. } => panic!(
"Iter 16b.3 invariant: LetRec `{name}` has Forall type at lift; \
desugar should have rejected (queued for 16b.6)"
"Iter 16b.6 invariant: LetRec `{name}` has its OWN Forall type at \
lift; LetRec doesn't quantify, only the enclosing fn does"
),
other => panic!(
"Iter 16b.3 invariant: LetRec `{name}` non-Fn/Forall type {:?}",
other
),
};
let augmented_ty = if self.current_def_forall_vars.is_empty() {
inner_augmented_ty
} else {
Type::Forall {
vars: self.current_def_forall_vars.clone(),
body: Box::new(inner_augmented_ty),
}
};
let mut augmented_params = params.clone();
for (cn, _) in &capture_types {
augmented_params.push(cn.clone());
+284 -25
View File
@@ -180,43 +180,63 @@ pub fn desugar_module(m: &Module) -> Module {
used,
lifted: Vec::new(),
module_top_names,
current_def_forall_vars: Vec::new(),
};
let mut out = m.clone();
for def in &mut out.defs {
match def {
Def::Fn(f) => {
// Iter 16b.2: build initial scope from fn-params with
// their declared types (peeled out of `f.ty`). For a
// `Type::Forall`-quantified fn, the param types may
// mention outer type vars; constructing a lifted
// `Forall` signature is out of scope for 16b.2, so
// every param of a polymorphic enclosing fn enters
// the scope as `LetBound` — captures of those names
// by an inner LetRec are rejected at the same site
// as let-binding captures.
// Iter 16b.2 / 16b.6: build initial scope from fn-params
// with their declared types (peeled out of `f.ty`).
//
// 16b.6: a `Type::Forall { vars, body: Fn(ptys, ...) }`
// enclosing fn is now supported. Its param types may
// mention `vars` — that's fine, the lifted fn becomes
// `Forall(vars, Fn(ptys ++ capture_tys, ...))` so the
// type vars are still bound at the lift site. Fn-params
// of a Forall enclosing fn therefore enter the scope as
// `KnownType`, not `LetBound` (the 16b.2 fallback was
// overly conservative). The enclosing fn's `Forall.vars`
// are stashed in `Desugarer.current_def_forall_vars` for
// the LetRec arm to read.
let mut scope: BTreeMap<String, ScopeEntry> = BTreeMap::new();
match &f.ty {
Type::Fn { params: ptys, .. } if ptys.len() == f.params.len() => {
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()));
}
}
Type::Forall { .. } => {
for p in &f.params {
scope.insert(p.clone(), ScopeEntry::LetBound);
}
}
_ => {
// Defensive: if the fn type is malformed (e.g.
// arity mismatch, non-Fn). Fall back to LetBound
// for every param so captures are rejected
// cleanly rather than silently mistyped.
// Defensive: malformed fn type (arity mismatch,
// non-Fn). Fall back to LetBound for every param
// so captures are rejected cleanly rather than
// silently mistyped.
for p in &f.params {
scope.insert(p.clone(), ScopeEntry::LetBound);
}
}
}
// Iter 16b.6: stash the enclosing fn's Forall.vars (or
// empty for a mono enclosing fn) for the LetRec arm.
let saved = std::mem::take(&mut d.current_def_forall_vars);
d.current_def_forall_vars = match &f.ty {
Type::Forall { vars, .. } => vars.clone(),
_ => Vec::new(),
};
f.body = d.desugar_term(&f.body, &scope);
d.current_def_forall_vars = saved;
}
Def::Const(c) => {
let scope: BTreeMap<String, ScopeEntry> = BTreeMap::new();
@@ -329,11 +349,23 @@ fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<String>) {
/// capture analyser uses it to classify free vars; the fresh-name
/// generator [`fresh_lifted`](Self::fresh_lifted) consults it so
/// later lifts cannot collide with earlier ones.
///
/// Iter 16b.6 field:
/// - `current_def_forall_vars` carries the enclosing fn's
/// `Type::Forall.vars` while desugaring its body. Empty for
/// monomorphic enclosing fns. Read by the LetRec lifter to wrap
/// the synthetic lifted fn's signature in `Type::Forall` mirroring
/// the enclosing fn — so the lifted fn enters codegen's
/// monomorphisation queue at every call site of the enclosing fn,
/// specialising at the same type args as its host.
struct Desugarer {
counter: u64,
used: BTreeSet<String>,
lifted: Vec<Def>,
module_top_names: BTreeSet<String>,
/// Iter 16b.6: the enclosing fn's Forall.vars (or empty if mono).
/// Reset on entry to each `Def::Fn`.
current_def_forall_vars: Vec<String>,
}
impl Desugarer {
@@ -556,6 +588,25 @@ impl Desugarer {
}
let in_has_value_use = find_non_callee_use(&desugared_in, name).is_some();
// Iter 16b.6: reject name-as-value in `in_term` when the
// enclosing fn is polymorphic. The 16b.5 wrap synthesises
// a `Term::Lam` whose AST has no `Forall` slot, so
// wrapping a polymorphic lifted fn into a monomorphic Lam
// would lose the type vars. Solving this needs closure
// conversion with polymorphism (queue tag `closure-poly`
// / informally `16b.5b`). Other 16b.5 use cases — name-
// as-value in a MONOMORPHIC enclosing fn — continue to
// work via the eta-Lam wrap.
if in_has_value_use && !self.current_def_forall_vars.is_empty() {
panic!(
"Iter 16b.6: name-as-value of LetRec `{}` is not yet supported in \
a polymorphic enclosing fn — closure conversion with \
polymorphism would be required. Queued separately as \
`closure-poly` (informally 16b.5b).",
name
);
}
// Capture detection (against the *outer* scope, before
// the body-scope extension).
let mut local_bound: BTreeSet<String> = BTreeSet::new();
@@ -633,9 +684,22 @@ impl Desugarer {
.collect();
// Build augmented type: original Fn with capture types
// appended to `params`. A Forall LetRec is rejected
// (would need synthesised Forall — out of scope).
let augmented_ty = match ty {
// appended to `params`.
//
// Iter 16b.6: when the ENCLOSING fn is polymorphic
// (`current_def_forall_vars` non-empty), wrap the
// augmented Fn in a `Type::Forall` mirroring the
// enclosing fn's type vars. The capture types may
// mention any of those vars; that's fine — the Forall
// binds them. At every call site of the LetRec name
// inside the enclosing fn's body, codegen's Iter
// 12b/14a monomorphisation specialises `f$lr_N` at the
// same type args as the enclosing fn's current mono.
//
// The LetRec's own `ty` (`Type::Fn` — never `Forall`,
// since LetRec itself doesn't quantify) is the inner
// type.
let inner_augmented_ty = match ty {
Type::Fn { params: ps, ret, effects } => {
let mut new_ps = ps.clone();
for (_, t) in &capture_types {
@@ -648,8 +712,9 @@ impl Desugarer {
}
}
Type::Forall { .. } => panic!(
"Iter 16b.3: LetRec `{}` has a Forall type; captures from a \
polymorphic context are not supported. Queued for 16b.6.",
"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!(
@@ -657,6 +722,14 @@ impl Desugarer {
name, other
),
};
let augmented_ty = if self.current_def_forall_vars.is_empty() {
inner_augmented_ty
} else {
Type::Forall {
vars: self.current_def_forall_vars.clone(),
body: Box::new(inner_augmented_ty),
}
};
// Augmented param-name list: original params + capture
// names (using the captured variable names directly so
// the lifted body's references already resolve
@@ -2030,6 +2103,192 @@ mod tests {
}
}
/// Iter 16b.6: a LetRec inside a polymorphic enclosing fn gets its
/// fn-param captures lifted with a `Forall(vars, Fn(...))` augmented
/// signature, mirroring the enclosing fn's type vars. Without 16b.6,
/// the desugar pass would have classified the fn-params as
/// `LetBound` (16b.2 fallback) and deferred to `lift_letrecs`; with
/// 16b.6, the fn-params are `KnownType` and the fast-path lift fires
/// here, producing a `Forall`-typed synthetic FnDef.
#[test]
fn let_rec_capture_under_polymorphic_enclosing_fn_lifts_to_forall_fn() {
// (fn id_n_times : Forall(a). Fn(Int, a) -> a
// (params n x)
// (body
// (let-rec loop : Fn(Int, a) -> a (params k acc)
// (body (if (== k 0) acc (app loop (- k 1) acc)))
// (in (app loop n x)))))
// Captures: none (all references inside the body are to params
// of `loop` itself or top-level builtins). To force a capture
// of an `a`-typed param, use the simpler shape:
//
// (fn rec_id : Forall(a). Fn(a) -> a
// (params x)
// (body
// (let-rec loop : Fn(Int) -> a (params k)
// (body (if (== k 0) x (app loop (- k 1))))
// (in (app loop 1)))))
//
// Here `loop` captures `x: a` from the enclosing fn's params.
// Without 16b.6, `x` would be ScopeEntry::LetBound (because the
// enclosing fn is Forall) and the LetRec would defer; with
// 16b.6, `x` is KnownType and the lift fires here.
let letrec = Term::LetRec {
name: "loop".into(),
ty: Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
},
params: vec!["k".into()],
body: Box::new(Term::If {
cond: Box::new(Term::App {
callee: Box::new(Term::Var { name: "==".into() }),
args: vec![
Term::Var { name: "k".into() },
Term::Lit { lit: Literal::Int { value: 0 } },
],
tail: false,
}),
then: Box::new(Term::Var { name: "x".into() }),
else_: Box::new(Term::App {
callee: Box::new(Term::Var { name: "loop".into() }),
args: vec![Term::App {
callee: Box::new(Term::Var { name: "-".into() }),
args: vec![
Term::Var { name: "k".into() },
Term::Lit { lit: Literal::Int { value: 1 } },
],
tail: false,
}],
tail: false,
}),
}),
in_term: Box::new(Term::App {
callee: Box::new(Term::Var { name: "loop".into() }),
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
tail: false,
}),
};
let m = Module {
schema: crate::SCHEMA.to_string(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "rec_id".into(),
ty: Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
}),
},
params: vec!["x".into()],
body: letrec,
doc: None,
})],
};
let out = desugar_module(&m);
assert_eq!(
out.defs.len(),
2,
"expected one lifted fn appended; got {:?}",
out.defs.iter().map(|d| d.name()).collect::<Vec<_>>()
);
let lifted = match &out.defs[1] {
Def::Fn(fd) => fd,
_ => unreachable!(),
};
assert!(
lifted.name.starts_with("loop$lr_"),
"unexpected lifted-name shape: {}",
lifted.name
);
// Lifted type must be Forall(a). Fn(Int, a) -> a (k + x capture).
match &lifted.ty {
Type::Forall { vars, body } => {
assert_eq!(vars, &vec!["a".to_string()], "Forall vars must mirror enclosing");
match body.as_ref() {
Type::Fn { params, ret, .. } => {
assert_eq!(params.len(), 2, "expected Int + a-typed capture");
assert!(
matches!(&params[0], Type::Con { name, .. } if name == "Int"),
"first param must be Int (loop's own k); got {:?}",
params[0]
);
assert!(
matches!(&params[1], Type::Var { name } if name == "a"),
"second param must be the captured `x: a`; got {:?}",
params[1]
);
assert!(
matches!(ret.as_ref(), Type::Var { name } if name == "a"),
"ret must be `a`; got {:?}",
ret
);
}
other => panic!("Forall body must be Fn; got {:?}", other),
}
}
other => panic!("lifted type must be Forall; got {:?}", other),
}
// Lifted params: original "k" + captured "x".
assert_eq!(lifted.params, vec!["k".to_string(), "x".to_string()]);
}
/// Iter 16b.6: a LetRec inside a polymorphic enclosing fn whose
/// `in_term` uses the LetRec name as a value (not as a callee) is
/// rejected. The 16b.5 eta-Lam wrap can't quantify `Forall.vars`
/// because `Term::Lam` has no Forall slot — wrapping a polymorphic
/// lifted fn into a monomorphic Lam loses the type vars. Queued
/// separately as `closure-poly` (informally 16b.5b).
#[test]
#[should_panic(expected = "16b.6")]
fn let_rec_name_as_value_in_polymorphic_enclosing_fn_panics() {
// Same shape as `let_rec_name_as_value_in_in_term_wraps_to_eta_lam`
// but the enclosing fn is Forall-quantified.
let letrec = Term::LetRec {
name: "f".into(),
ty: Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
},
params: vec!["k".into()],
body: Box::new(Term::Var { name: "x".into() }),
in_term: Box::new(Term::Let {
name: "g".into(),
value: Box::new(Term::Var { name: "f".into() }), // value-position f
body: Box::new(Term::App {
callee: Box::new(Term::Var { name: "g".into() }),
args: vec![Term::Lit { lit: Literal::Int { value: 1 } }],
tail: false,
}),
}),
};
let m = Module {
schema: crate::SCHEMA.to_string(),
name: "t".into(),
imports: vec![],
defs: vec![Def::Fn(FnDef {
name: "outer".into(),
ty: Type::Forall {
vars: vec!["a".into()],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
}),
},
params: vec!["x".into()],
body: letrec,
doc: None,
})],
};
let _ = desugar_module(&m);
}
/// Iter 16c: walks a term, returns true iff any [`Pattern::Lit`] is
/// reachable in any [`Term::Match`] arm. After 16c desugaring, the
/// invariant is `false` for every desugared output — `Pattern::Lit`