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:
@@ -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!(¶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()]);
|
||||
}
|
||||
|
||||
/// 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`
|
||||
|
||||
Reference in New Issue
Block a user