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:
@@ -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!(¶ms[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!(¶ms[1], Type::Var { name } if name == "a"),
|
||||
"second param: x: a; got {:?}",
|
||||
params[1]
|
||||
);
|
||||
assert!(
|
||||
matches!(¶ms[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()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user