Iter 16b.3: post-typecheck lift for LetRec with Let-bound captures
Adds path-2 from the 16b.2 planning entry. Desugar now defers
LetRecs whose captures include Term::Let-bound names; a new
ailang-check::lift_letrecs pass runs after typecheck and uses the
elaborated env to resolve capture types. ailang-check learns a real
Term::LetRec typing rule in synth and verify_tail_positions.
- desugar: Term::LetRec arm gains a defer-arm; helpers promoted to
pub for reuse by the lift pass; find_non_callee_use moved before
classification.
- ailang-check::synth/verify_tail_positions: real LetRec rules
(effect-subset, locals install, recursive name in body+in_term).
- ailang-check::lift.rs (new, 720 LOC): post-typecheck lift with
post-order traversal, env-walk for capture-type resolution,
fast-path skip when no LetRec is present.
- ail::main.rs: build path now does load → check → desugar →
lift_letrecs → codegen.
- examples/local_rec_let_capture.{ailx,ail.json}: new fixture
capturing a let-bound `threshold` in a recursive helper.
- e2e + check unit tests: 106 → 110 (+4).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1512,6 +1512,13 @@ fn render_workspace_diff_text(r: &WorkspaceDiffReport) -> String {
|
|||||||
/// clang. On typecheck failure, prints diagnostics to stderr and exits
|
/// clang. On typecheck failure, prints diagnostics to stderr and exits
|
||||||
/// the process with code 1. On clang failure, returns a Result error
|
/// the process with code 1. On clang failure, returns a Result error
|
||||||
/// (the .ll path is preserved for post-mortem inspection).
|
/// (the .ll path is preserved for post-mortem inspection).
|
||||||
|
///
|
||||||
|
/// Iter 16b.3: between `check_workspace` and `lower_workspace` we run
|
||||||
|
/// `ailang_check::lift_letrecs` per module. The lift eliminates any
|
||||||
|
/// `Term::LetRec` that the desugar pass left in place (specifically:
|
||||||
|
/// LetRecs that capture `Term::Let`-bound names, whose types are
|
||||||
|
/// only known after typecheck). The lifted workspace then goes to
|
||||||
|
/// codegen unchanged.
|
||||||
fn build_to(path: &Path, out: Option<PathBuf>, opt: &str) -> Result<PathBuf> {
|
fn build_to(path: &Path, out: Option<PathBuf>, opt: &str) -> Result<PathBuf> {
|
||||||
let ws = ailang_core::load_workspace(path)?;
|
let ws = ailang_core::load_workspace(path)?;
|
||||||
let diags = ailang_check::check_workspace(&ws);
|
let diags = ailang_check::check_workspace(&ws);
|
||||||
@@ -1533,6 +1540,22 @@ fn build_to(path: &Path, out: Option<PathBuf>, opt: &str) -> Result<PathBuf> {
|
|||||||
}
|
}
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
// Iter 16b.3: run `lift_letrecs` per module on the post-desugar
|
||||||
|
// form. Codegen's internal desugar pass is idempotent on a
|
||||||
|
// module that contains no `Term::LetRec`, so the lifted output
|
||||||
|
// can be handed directly to `lower_workspace`.
|
||||||
|
let mut lifted_modules = std::collections::BTreeMap::new();
|
||||||
|
for (mname, m) in &ws.modules {
|
||||||
|
let desugared = ailang_core::desugar::desugar_module(m);
|
||||||
|
let lifted = ailang_check::lift_letrecs(&desugared)
|
||||||
|
.map_err(|e| anyhow::anyhow!("lift_letrecs in module `{mname}`: {e}"))?;
|
||||||
|
lifted_modules.insert(mname.clone(), lifted);
|
||||||
|
}
|
||||||
|
let ws = ailang_core::Workspace {
|
||||||
|
entry: ws.entry.clone(),
|
||||||
|
modules: lifted_modules,
|
||||||
|
root_dir: ws.root_dir.clone(),
|
||||||
|
};
|
||||||
let ir = ailang_codegen::lower_workspace(&ws)?;
|
let ir = ailang_codegen::lower_workspace(&ws)?;
|
||||||
let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id()));
|
let tmpdir = std::env::temp_dir().join(format!("ailang-{}", std::process::id()));
|
||||||
std::fs::create_dir_all(&tmpdir)?;
|
std::fs::create_dir_all(&tmpdir)?;
|
||||||
|
|||||||
@@ -467,6 +467,24 @@ fn local_rec_capture_demo() {
|
|||||||
assert_eq!(lines, vec!["0", "10", "45"]);
|
assert_eq!(lines, vec!["0", "10", "45"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: LetRec capture of a `Term::Let`-bound name. Property
|
||||||
|
/// protected: the desugar pass leaves a LetRec whose only outside-
|
||||||
|
/// scope captures are Let-bound (type unknown until typecheck) in
|
||||||
|
/// place; the post-typecheck `lift_letrecs` pass in `ailang-check`
|
||||||
|
/// resolves capture types from the typechecker's env, lifts to a
|
||||||
|
/// synthetic top-level fn (`loop$lr_0(i, n, threshold)`), and
|
||||||
|
/// rewrites every call site. Without 16b.3, the 16b.2-era panic
|
||||||
|
/// ("16b.3 — let-binding captures") would fire. The threshold here
|
||||||
|
/// is `(app + 5 5)` so the lift exercises the type-synthesis path
|
||||||
|
/// (not just a literal). count_below(0)=0, count_below(5)=5,
|
||||||
|
/// count_below(15)=9 (i in 1..9 are below threshold 10).
|
||||||
|
#[test]
|
||||||
|
fn local_rec_let_capture_demo() {
|
||||||
|
let stdout = build_and_run("local_rec_let_capture.ail.json");
|
||||||
|
let lines: Vec<&str> = stdout.lines().collect();
|
||||||
|
assert_eq!(lines, vec!["0", "5", "9"]);
|
||||||
|
}
|
||||||
|
|
||||||
/// Guards `ail diff`: a modified body changes the hash of `sum`, while
|
/// Guards `ail diff`: a modified body changes the hash of `sum`, while
|
||||||
/// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
|
/// `main` stays unchanged. Expects exit code 1, `changed` contains exactly
|
||||||
/// `sum`, `unchanged` contains `main`, `added`/`removed` empty.
|
/// `sum`, `unchanged` contains `main`, `added`/`removed` empty.
|
||||||
|
|||||||
+405
-11
@@ -239,8 +239,10 @@ fn unify(a: &Type, b: &Type, subst: &mut Subst) -> Result<()> {
|
|||||||
|
|
||||||
pub mod builtins;
|
pub mod builtins;
|
||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
|
pub mod lift;
|
||||||
|
|
||||||
pub use diagnostic::{Diagnostic, Severity};
|
pub use diagnostic::{Diagnostic, Severity};
|
||||||
|
pub use lift::lift_letrecs;
|
||||||
|
|
||||||
/// Internal error type produced by the typechecker.
|
/// Internal error type produced by the typechecker.
|
||||||
///
|
///
|
||||||
@@ -419,7 +421,7 @@ pub enum CheckError {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result<T> = std::result::Result<T, CheckError>;
|
pub(crate) type Result<T> = std::result::Result<T, CheckError>;
|
||||||
|
|
||||||
impl CheckError {
|
impl CheckError {
|
||||||
/// Stable kebab-case code for machine consumption (`ail check --json`).
|
/// Stable kebab-case code for machine consumption (`ail check --json`).
|
||||||
@@ -687,6 +689,31 @@ pub fn check(m: &Module) -> Result<CheckedModule> {
|
|||||||
Ok(CheckedModule { symbols })
|
Ok(CheckedModule { symbols })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: typecheck `m` and return both the [`CheckedModule`]
|
||||||
|
/// (for tooling that wants the original on-disk symbol identities)
|
||||||
|
/// AND the lifted module ready for codegen — i.e. the desugared
|
||||||
|
/// module with every surviving `Term::LetRec` replaced by a
|
||||||
|
/// synthetic top-level `Def::Fn`.
|
||||||
|
///
|
||||||
|
/// The check phase runs unchanged: same desugar, same typecheck,
|
||||||
|
/// same `CheckedModule.symbols` (built from the original `m`'s
|
||||||
|
/// defs). On success, the desugared module is fed through
|
||||||
|
/// [`lift_letrecs`] and the result is returned alongside.
|
||||||
|
///
|
||||||
|
/// `build` / `run` go through this entry; the `check` subcommand
|
||||||
|
/// stays on the legacy [`check`] entry (no lift needed for
|
||||||
|
/// type-checking only).
|
||||||
|
pub fn check_and_lift(m: &Module) -> Result<(CheckedModule, Module)> {
|
||||||
|
let cm = check(m)?;
|
||||||
|
// Run desugar exactly as `check` does. The `check` call already
|
||||||
|
// ran desugar internally, but its result is discarded (only the
|
||||||
|
// CheckedModule survives), so we have to re-run it here to get
|
||||||
|
// the post-desugar form for the lift.
|
||||||
|
let desugared = ailang_core::desugar::desugar_module(m);
|
||||||
|
let lifted = lift_letrecs(&desugared)?;
|
||||||
|
Ok((cm, lifted))
|
||||||
|
}
|
||||||
|
|
||||||
/// Iter 15a: builds the ADT type-def table per module. Sibling of
|
/// Iter 15a: builds the ADT type-def table per module. Sibling of
|
||||||
/// [`build_module_globals`]: gives the body checker O(1) lookup of any
|
/// [`build_module_globals`]: gives the body checker O(1) lookup of any
|
||||||
/// type declared anywhere in the workspace, keyed by module name.
|
/// type declared anywhere in the workspace, keyed by module name.
|
||||||
@@ -1083,10 +1110,16 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
|
|||||||
// Entering a Lam body opens a fresh tail scope.
|
// Entering a Lam body opens a fresh tail scope.
|
||||||
verify_tail_positions(body, true)
|
verify_tail_positions(body, true)
|
||||||
}
|
}
|
||||||
Term::LetRec { .. } => {
|
Term::LetRec { body, in_term, .. } => {
|
||||||
// Iter 16b.1: `Term::LetRec` is eliminated by the desugar
|
// Iter 16b.3: `Term::LetRec` may now survive the desugar
|
||||||
// pass before `check` runs, so reaching it here is a bug.
|
// pass (when it captures `Term::Let`-bound names whose
|
||||||
unreachable!("Term::LetRec eliminated by desugar")
|
// types are only known after typecheck). The body is the
|
||||||
|
// body of a fn-typed binding; the recursive name is what
|
||||||
|
// gets tail-called, so `body` is NOT in tail position. The
|
||||||
|
// in-clause IS in tail position iff the enclosing context
|
||||||
|
// is — same propagation rule as `Term::Let.body`.
|
||||||
|
verify_tail_positions(body, false)?;
|
||||||
|
verify_tail_positions(in_term, is_tail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1112,7 +1145,7 @@ fn check_const(c: &ConstDef, env: &Env) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn synth(
|
pub(crate) fn synth(
|
||||||
t: &Term,
|
t: &Term,
|
||||||
env: &Env,
|
env: &Env,
|
||||||
locals: &mut IndexMap<String, Type>,
|
locals: &mut IndexMap<String, Type>,
|
||||||
@@ -1488,10 +1521,104 @@ fn synth(
|
|||||||
effects: lam_effects.clone(),
|
effects: lam_effects.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Term::LetRec { .. } => {
|
Term::LetRec { name, ty, params, body, in_term } => {
|
||||||
// Iter 16b.1: `Term::LetRec` is eliminated by the desugar
|
// Iter 16b.3: a `Term::LetRec` reaches `synth` only when
|
||||||
// pass before `synth` runs, so reaching it here is a bug.
|
// the desugar pass deferred it (some capture is
|
||||||
unreachable!("Term::LetRec eliminated by desugar")
|
// `Term::Let`-bound and its type is only knowable here).
|
||||||
|
// We synthesize it as if it were a recursive fn-typed
|
||||||
|
// local binding:
|
||||||
|
// 1. Peel any `Forall` defensively (16b.6 still rejects
|
||||||
|
// Forall-typed LetRecs at desugar — this is just for
|
||||||
|
// shape uniformity with `check_fn`).
|
||||||
|
// 2. Validate that `params.len() == ty.params.len()`.
|
||||||
|
// 3. Extend `locals` with `name: ty` for the body
|
||||||
|
// (recursive self-reference) and each
|
||||||
|
// `params[i]: ty.params[i]`.
|
||||||
|
// 4. Synth the body, unify against `ty.ret`, check
|
||||||
|
// effects-subset against `ty.effects`.
|
||||||
|
// 5. Restore locals; extend with `name: ty`; synth
|
||||||
|
// `in_term`. Restore. Return `in_term`'s type.
|
||||||
|
let inner_ty = match ty {
|
||||||
|
Type::Forall { body, .. } => (**body).clone(),
|
||||||
|
other => other.clone(),
|
||||||
|
};
|
||||||
|
let (param_tys, ret_ty, declared_effs) = match &inner_ty {
|
||||||
|
Type::Fn { params: ps, ret, effects } => {
|
||||||
|
(ps.clone(), (**ret).clone(), effects.clone())
|
||||||
|
}
|
||||||
|
other => {
|
||||||
|
return Err(CheckError::FnTypeRequired(
|
||||||
|
name.clone(),
|
||||||
|
ailang_core::pretty::type_to_string(other),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if param_tys.len() != params.len() {
|
||||||
|
return Err(CheckError::ParamCountMismatch {
|
||||||
|
name: name.clone(),
|
||||||
|
ty_count: param_tys.len(),
|
||||||
|
param_count: params.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the LetRec's declared param/ret types against
|
||||||
|
// the type env (catches malformed types like ADT arity
|
||||||
|
// mismatches before they leak into the body).
|
||||||
|
for p in ¶m_tys {
|
||||||
|
check_type_well_formed(p, env)?;
|
||||||
|
}
|
||||||
|
check_type_well_formed(&ret_ty, env)?;
|
||||||
|
|
||||||
|
// Save and extend locals: name + params for the body's scope.
|
||||||
|
let mut pushed: Vec<(String, Option<Type>)> = Vec::new();
|
||||||
|
let prev_name = locals.insert(name.clone(), ty.clone());
|
||||||
|
pushed.push((name.clone(), prev_name));
|
||||||
|
for (n, t) in params.iter().zip(param_tys.iter()) {
|
||||||
|
let prev = locals.insert(n.clone(), t.clone());
|
||||||
|
pushed.push((n.clone(), prev));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body effects are tracked separately so we can check the
|
||||||
|
// subset rule against `declared_effs` — exactly like
|
||||||
|
// `Term::Lam`.
|
||||||
|
let mut body_effects: BTreeSet<String> = BTreeSet::new();
|
||||||
|
let body_ty = synth(body, env, locals, &mut body_effects, in_def, subst, counter);
|
||||||
|
|
||||||
|
// Restore body-scope locals (params + name).
|
||||||
|
for (n, prev) in pushed.into_iter().rev() {
|
||||||
|
match prev {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(n, p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let body_ty = body_ty?;
|
||||||
|
unify(&ret_ty, &body_ty, subst)?;
|
||||||
|
|
||||||
|
let declared: BTreeSet<String> = declared_effs.into_iter().collect();
|
||||||
|
for e in &body_effects {
|
||||||
|
if !declared.contains(e) {
|
||||||
|
return Err(CheckError::UndeclaredEffect(e.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now extend locals with `name: ty` for the in-clause's
|
||||||
|
// scope (params are not visible here; only the recursive
|
||||||
|
// binding is).
|
||||||
|
let prev_in = locals.insert(name.clone(), ty.clone());
|
||||||
|
let in_ty = synth(in_term, env, locals, effects, in_def, subst, counter);
|
||||||
|
match prev_in {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(name.clone(), p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
in_ty
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1794,7 +1921,7 @@ pub struct CtorRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Env {
|
impl Env {
|
||||||
fn new() -> Self {
|
pub(crate) fn new() -> Self {
|
||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2884,4 +3011,271 @@ mod tests {
|
|||||||
};
|
};
|
||||||
check(&m).expect("tail-call in tail position should typecheck");
|
check(&m).expect("tail-call in tail position should typecheck");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: a `Term::LetRec` that captures a `Term::Let`-bound
|
||||||
|
/// name (whose type is known only after typecheck) reaches `synth`
|
||||||
|
/// because the desugar pass leaves it in place. The new typing
|
||||||
|
/// rule for `Term::LetRec` accepts it: extends locals with `name`
|
||||||
|
/// and the params, synths the body, unifies against the declared
|
||||||
|
/// return type, then synths the in-clause.
|
||||||
|
#[test]
|
||||||
|
fn letrec_with_let_binding_capture_typechecks() {
|
||||||
|
// fn outer : (Int) -> Int = \n.
|
||||||
|
// let threshold = + 5 5
|
||||||
|
// in let-rec loop : (Int) -> Int = \i.
|
||||||
|
// if (>= i threshold) then 0 else loop (+ i 1)
|
||||||
|
// in loop 0
|
||||||
|
let body = Term::Let {
|
||||||
|
name: "threshold".into(),
|
||||||
|
value: Box::new(Term::App {
|
||||||
|
callee: Box::new(Term::Var { name: "+".into() }),
|
||||||
|
args: vec![
|
||||||
|
Term::Lit { lit: Literal::Int { value: 5 } },
|
||||||
|
Term::Lit { lit: Literal::Int { value: 5 } },
|
||||||
|
],
|
||||||
|
tail: false,
|
||||||
|
}),
|
||||||
|
body: Box::new(Term::LetRec {
|
||||||
|
name: "loop".into(),
|
||||||
|
ty: Type::Fn {
|
||||||
|
params: vec![Type::int()],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
params: vec!["i".into()],
|
||||||
|
body: Box::new(Term::If {
|
||||||
|
cond: Box::new(Term::App {
|
||||||
|
callee: Box::new(Term::Var { name: ">=".into() }),
|
||||||
|
args: vec![
|
||||||
|
Term::Var { name: "i".into() },
|
||||||
|
Term::Var { name: "threshold".into() },
|
||||||
|
],
|
||||||
|
tail: false,
|
||||||
|
}),
|
||||||
|
then: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
||||||
|
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: "i".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: 0 } }],
|
||||||
|
tail: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let m = Module {
|
||||||
|
schema: SCHEMA.into(),
|
||||||
|
name: "t".into(),
|
||||||
|
imports: vec![],
|
||||||
|
defs: vec![fn_def(
|
||||||
|
"outer",
|
||||||
|
Type::Fn {
|
||||||
|
params: vec![Type::int()],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
vec!["n"],
|
||||||
|
body,
|
||||||
|
)],
|
||||||
|
};
|
||||||
|
check(&m).expect("LetRec with Let-binding capture should typecheck");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: a LetRec whose body returns the wrong type (Bool
|
||||||
|
/// instead of the declared Int) is caught by the new typing rule.
|
||||||
|
/// Property protected: the new arm in `synth` for `Term::LetRec`
|
||||||
|
/// runs `unify(&ret_ty, &body_ty, subst)` just like `Term::Lam`
|
||||||
|
/// and `check_fn`.
|
||||||
|
#[test]
|
||||||
|
fn letrec_body_wrong_return_type_is_rejected() {
|
||||||
|
// (let-rec wrong (params x) (type (Int) -> Int) (body true)
|
||||||
|
// (in (app wrong 0)))
|
||||||
|
let letrec = Term::LetRec {
|
||||||
|
name: "wrong".into(),
|
||||||
|
ty: Type::Fn {
|
||||||
|
params: vec![Type::int()],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
params: vec!["x".into()],
|
||||||
|
body: Box::new(Term::Lit { lit: Literal::Bool { value: true } }),
|
||||||
|
in_term: Box::new(Term::App {
|
||||||
|
callee: Box::new(Term::Var { name: "wrong".into() }),
|
||||||
|
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
||||||
|
tail: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// Wrap in a Let so the desugar pass defers the LetRec (its
|
||||||
|
// body would otherwise lift cleanly with no captures, since
|
||||||
|
// `true` doesn't reference `x`).
|
||||||
|
let body = Term::Let {
|
||||||
|
name: "y".into(),
|
||||||
|
value: Box::new(Term::Lit { lit: Literal::Int { value: 0 } }),
|
||||||
|
// Reference `y` inside the LetRec body so it captures.
|
||||||
|
body: Box::new(Term::LetRec {
|
||||||
|
name: "wrong".into(),
|
||||||
|
ty: Type::Fn {
|
||||||
|
params: vec![Type::int()],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
params: vec!["x".into()],
|
||||||
|
body: Box::new(Term::Seq {
|
||||||
|
// Use `y` so the LetRec captures it (forces deferral).
|
||||||
|
lhs: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||||
|
rhs: Box::new(Term::App {
|
||||||
|
callee: Box::new(Term::Var { name: "+".into() }),
|
||||||
|
args: vec![
|
||||||
|
Term::Var { name: "y".into() },
|
||||||
|
Term::Lit { lit: Literal::Bool { value: true } },
|
||||||
|
],
|
||||||
|
tail: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
in_term: Box::new(Term::App {
|
||||||
|
callee: Box::new(Term::Var { name: "wrong".into() }),
|
||||||
|
args: vec![Term::Lit { lit: Literal::Int { value: 0 } }],
|
||||||
|
tail: false,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
// (Suppress dead-code warning on the unused unwrapped letrec.)
|
||||||
|
let _ = letrec;
|
||||||
|
let m = Module {
|
||||||
|
schema: SCHEMA.into(),
|
||||||
|
name: "t".into(),
|
||||||
|
imports: vec![],
|
||||||
|
defs: vec![fn_def(
|
||||||
|
"outer",
|
||||||
|
Type::Fn {
|
||||||
|
params: vec![],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
body,
|
||||||
|
)],
|
||||||
|
};
|
||||||
|
let err = check(&m).expect_err("LetRec body returning Bool but declared Int must error");
|
||||||
|
let msg = format!("{err}");
|
||||||
|
assert!(
|
||||||
|
msg.contains("type mismatch"),
|
||||||
|
"expected a type-mismatch diagnostic, got: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: `lift_letrecs` on a module containing a deferred
|
||||||
|
/// LetRec produces a module whose defs include a synthetic
|
||||||
|
/// `<hint>$lr_N` FnDef and whose original fn body has rewritten
|
||||||
|
/// call sites.
|
||||||
|
#[test]
|
||||||
|
fn lift_letrecs_on_let_capture_produces_synthetic_fn() {
|
||||||
|
// fn outer : () -> Int = \.
|
||||||
|
// let y = 7
|
||||||
|
// in let-rec helper : (Int) -> Int = \x. + x y
|
||||||
|
// in helper 1
|
||||||
|
let m = Module {
|
||||||
|
schema: SCHEMA.into(),
|
||||||
|
name: "t".into(),
|
||||||
|
imports: vec![],
|
||||||
|
defs: vec![fn_def(
|
||||||
|
"outer",
|
||||||
|
Type::Fn {
|
||||||
|
params: vec![],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
vec![],
|
||||||
|
Term::Let {
|
||||||
|
name: "y".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::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)],
|
||||||
|
};
|
||||||
|
// Typecheck must succeed (the new LetRec rule accepts this).
|
||||||
|
check(&m).expect("typecheck before lift");
|
||||||
|
let desugared = ailang_core::desugar::desugar_module(&m);
|
||||||
|
// After desugar the LetRec is still present (Let-binding capture).
|
||||||
|
// After lift_letrecs it must be gone, replaced by a synthetic
|
||||||
|
// top-level fn with the capture appended to its signature.
|
||||||
|
let lifted = lift_letrecs(&desugared).expect("lift_letrecs");
|
||||||
|
assert_eq!(lifted.defs.len(), 2, "expected one synthetic fn appended");
|
||||||
|
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
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
synth.ty,
|
||||||
|
Type::Fn {
|
||||||
|
params: vec![Type::int(), Type::int()],
|
||||||
|
ret: Box::new(Type::int()),
|
||||||
|
effects: vec![],
|
||||||
|
},
|
||||||
|
"lifted ty should have capture appended; got {:?}",
|
||||||
|
synth.ty
|
||||||
|
);
|
||||||
|
assert_eq!(synth.params, vec!["x".to_string(), "y".to_string()]);
|
||||||
|
// `outer`'s body must have the `(app helper 1)` rewritten to
|
||||||
|
// `(app helper$lr_0 1 y)`. The body is now
|
||||||
|
// `let y = 7 in (app helper$lr_0 1 y)`.
|
||||||
|
let outer_body = match &lifted.defs[0] {
|
||||||
|
Def::Fn(f) => &f.body,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let inner = match outer_body {
|
||||||
|
Term::Let { body, .. } => body.as_ref(),
|
||||||
|
other => panic!("expected outer Let, got {other:?}"),
|
||||||
|
};
|
||||||
|
match inner {
|
||||||
|
Term::App { callee, args, .. } => {
|
||||||
|
match callee.as_ref() {
|
||||||
|
Term::Var { name } => assert_eq!(name, &synth.name),
|
||||||
|
other => panic!("expected lifted callee, got {other:?}"),
|
||||||
|
}
|
||||||
|
assert_eq!(args.len(), 2, "expected 2 args (1 original + 1 capture)");
|
||||||
|
match &args[1] {
|
||||||
|
Term::Var { name } => assert_eq!(name, "y"),
|
||||||
|
other => panic!("expected y as second arg, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
other => panic!("expected App after Let, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,720 @@
|
|||||||
|
//! Iter 16b.3: post-typecheck `Term::LetRec` lift.
|
||||||
|
//!
|
||||||
|
//! Background. The 16a desugar pass (`ailang-core::desugar::desugar_module`)
|
||||||
|
//! eliminates most `Term::LetRec` nodes by lifting them to synthetic
|
||||||
|
//! top-level fns. That works as long as every captured name has a
|
||||||
|
//! statically-known type at desugar time — fn-params and Lam-params
|
||||||
|
//! qualify (16b.2). When a capture is bound by a `Term::Let`, the
|
||||||
|
//! let-value's type is inferred at typecheck and the desugar pass
|
||||||
|
//! cannot resolve it. In that case the desugar pass leaves the
|
||||||
|
//! `Term::LetRec` in place; this module's [`lift_letrecs`] picks them
|
||||||
|
//! up afterwards and lifts them using the typechecker's resolved
|
||||||
|
//! types.
|
||||||
|
//!
|
||||||
|
//! Pipeline placement. After [`crate::check`] succeeds. The lifter
|
||||||
|
//! produces a new `Module` whose `defs` may have additional synthetic
|
||||||
|
//! `Def::Fn` entries appended. The module's existing top-level defs
|
||||||
|
//! are NOT renamed; only the `body` of each `Def::Fn` (and the
|
||||||
|
//! `value` of each `Def::Const`, defensively) is rewritten.
|
||||||
|
//!
|
||||||
|
//! Symbol-hashing invariant. Synthetic FnDefs added by `lift_letrecs`
|
||||||
|
//! must NOT appear in `CheckedModule.symbols` — that table is built
|
||||||
|
//! from the original on-disk module, preserving the canonical-bytes
|
||||||
|
//! identity that `ail diff` and `ail manifest` rely on. The 16b.2
|
||||||
|
//! lift in desugar already follows the same convention. Concretely:
|
||||||
|
//! `check` keeps building symbols as today (from the input module);
|
||||||
|
//! `lift_letrecs` runs separately and returns the possibly-larger
|
||||||
|
//! module that goes to codegen.
|
||||||
|
|
||||||
|
use ailang_core::ast::*;
|
||||||
|
use ailang_core::desugar::{
|
||||||
|
find_non_callee_use, free_vars_in_term, subst_call_with_extras, subst_var,
|
||||||
|
};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
use crate::{builtins, synth, CheckError, Env, Result, Subst};
|
||||||
|
|
||||||
|
/// Iter 16b.3: post-typecheck pass that eliminates every surviving
|
||||||
|
/// `Term::LetRec` from `m` by lifting it to a synthetic top-level
|
||||||
|
/// `Def::Fn`. Returns a new module that goes to codegen.
|
||||||
|
///
|
||||||
|
/// Pre-condition: `m` has been typechecked (i.e. `check(m)` returned
|
||||||
|
/// `Ok`). The lifter calls `synth` to resolve capture types, but
|
||||||
|
/// only on sub-terms that were already typechecked successfully — it
|
||||||
|
/// does not perform new type checking, only type queries.
|
||||||
|
///
|
||||||
|
/// The synthetic FnDefs are appended to `Module.defs` after every
|
||||||
|
/// pre-existing def. Their names follow the `<hint>$lr_N` convention
|
||||||
|
/// from 16b.2; the lifter's counter starts past the highest `*$lr_N`
|
||||||
|
/// suffix already present in `m.defs` to avoid collisions with
|
||||||
|
/// 16b.2's lifts.
|
||||||
|
pub fn lift_letrecs(m: &Module) -> Result<Module> {
|
||||||
|
// Fast path: if the module contains no `Term::LetRec`, the lift
|
||||||
|
// pass has nothing to do. Returning the input module verbatim
|
||||||
|
// also means we don't build an env or run any sub-term type
|
||||||
|
// synthesis — important because cross-module references in
|
||||||
|
// typical fixtures would fail to resolve under the
|
||||||
|
// single-module env we build below (a deliberate scope choice:
|
||||||
|
// `lift_letrecs` is per-module, but cross-module info would only
|
||||||
|
// ever be needed if a deferred LetRec were present).
|
||||||
|
if !contains_any_letrec(m) {
|
||||||
|
return Ok(m.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the full env once (matches `check_in_workspace`'s setup),
|
||||||
|
// so we can re-synthesize sub-terms during the walk.
|
||||||
|
//
|
||||||
|
// `lift_letrecs` is a single-module pass; cross-module info isn't
|
||||||
|
// needed because the LetRec capture set comes from the *enclosing*
|
||||||
|
// fn's locals (which are always local to this module). Capture
|
||||||
|
// types may mention foreign type-cons (e.g. `std_list.List Int`),
|
||||||
|
// but those flow through verbatim — the lifter never needs to
|
||||||
|
// resolve them.
|
||||||
|
let mut env = Env::new();
|
||||||
|
builtins::install(&mut env);
|
||||||
|
|
||||||
|
// Type defs of this module.
|
||||||
|
for def in &m.defs {
|
||||||
|
if let Def::Type(td) = def {
|
||||||
|
for c in &td.ctors {
|
||||||
|
env.ctor_index.insert(
|
||||||
|
c.name.clone(),
|
||||||
|
crate::CtorRef {
|
||||||
|
type_name: td.name.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
env.types.insert(td.name.clone(), td.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level globals (fn / const types, including any 16b.2-lifted
|
||||||
|
// synthetic fns that already live in the desugared module).
|
||||||
|
for def in &m.defs {
|
||||||
|
let ty = match def {
|
||||||
|
Def::Fn(f) => f.ty.clone(),
|
||||||
|
Def::Const(c) => c.ty.clone(),
|
||||||
|
Def::Type(_) => Type::Con {
|
||||||
|
name: def.name().to_string(),
|
||||||
|
args: vec![],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
env.globals.insert(def.name().to_string(), ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Imports (used by qualified-ref synth, even though LetRec captures
|
||||||
|
// resolve through `locals`).
|
||||||
|
let mut import_map: BTreeMap<String, String> = BTreeMap::new();
|
||||||
|
for imp in &m.imports {
|
||||||
|
let key = imp.alias.clone().unwrap_or_else(|| imp.module.clone());
|
||||||
|
import_map.insert(key, imp.module.clone());
|
||||||
|
}
|
||||||
|
env.imports = import_map;
|
||||||
|
env.current_module = m.name.clone();
|
||||||
|
|
||||||
|
// Counter init: scan existing def names for the highest `*$lr_N`
|
||||||
|
// suffix so a new lift never collides with a 16b.2 lift.
|
||||||
|
let mut counter: u64 = highest_lr_suffix(&m.defs).map(|n| n + 1).unwrap_or(0);
|
||||||
|
|
||||||
|
// Pre-collect every existing top-level name so freshly-generated
|
||||||
|
// names cannot shadow them.
|
||||||
|
let mut module_names: BTreeSet<String> = BTreeSet::new();
|
||||||
|
for def in &m.defs {
|
||||||
|
module_names.insert(def.name().to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The walk.
|
||||||
|
let mut lifter = Lifter {
|
||||||
|
env,
|
||||||
|
counter,
|
||||||
|
module_names: &mut module_names,
|
||||||
|
lifted: Vec::new(),
|
||||||
|
};
|
||||||
|
let _ = &mut counter; // counter lives in lifter from here on
|
||||||
|
|
||||||
|
let mut out = m.clone();
|
||||||
|
for def in &mut out.defs {
|
||||||
|
match def {
|
||||||
|
Def::Fn(f) => {
|
||||||
|
// Build the locals scope for this fn (params with
|
||||||
|
// their declared types, peeling Forall like check_fn
|
||||||
|
// does).
|
||||||
|
let inner_ty = match &f.ty {
|
||||||
|
Type::Forall { body, .. } => (**body).clone(),
|
||||||
|
other => other.clone(),
|
||||||
|
};
|
||||||
|
let mut locals: IndexMap<String, Type> = IndexMap::new();
|
||||||
|
if let Type::Fn { params: ptys, .. } = &inner_ty {
|
||||||
|
if ptys.len() == f.params.len() {
|
||||||
|
for (n, t) in f.params.iter().zip(ptys.iter()) {
|
||||||
|
locals.insert(n.clone(), t.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Iter 13a: install rigid vars from a Forall enclosing
|
||||||
|
// fn so check_type_well_formed inside synth doesn't
|
||||||
|
// reject them. Save and restore so the lifter env is
|
||||||
|
// clean across defs.
|
||||||
|
let mut rigids_added: Vec<String> = Vec::new();
|
||||||
|
if let Type::Forall { vars, .. } = &f.ty {
|
||||||
|
for v in vars {
|
||||||
|
if lifter.env.rigid_vars.insert(v.clone()) {
|
||||||
|
rigids_added.push(v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.body = lifter.lift_in_term(&f.body, &mut locals, &f.name)?;
|
||||||
|
for v in rigids_added {
|
||||||
|
lifter.env.rigid_vars.remove(&v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Def::Const(c) => {
|
||||||
|
let mut locals: IndexMap<String, Type> = IndexMap::new();
|
||||||
|
c.value = lifter.lift_in_term(&c.value, &mut locals, &c.name)?;
|
||||||
|
}
|
||||||
|
Def::Type(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out.defs.extend(lifter.lifted.into_iter());
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
struct Lifter<'a> {
|
||||||
|
env: Env,
|
||||||
|
counter: u64,
|
||||||
|
module_names: &'a mut BTreeSet<String>,
|
||||||
|
lifted: Vec<Def>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Lifter<'a> {
|
||||||
|
/// Walk `t`, lifting any `Term::LetRec` that survived desugar.
|
||||||
|
/// Maintains `locals` parallel to the term's lexical scope so we
|
||||||
|
/// can resolve capture types via `synth`.
|
||||||
|
fn lift_in_term(
|
||||||
|
&mut self,
|
||||||
|
t: &Term,
|
||||||
|
locals: &mut IndexMap<String, Type>,
|
||||||
|
in_def: &str,
|
||||||
|
) -> Result<Term> {
|
||||||
|
match t {
|
||||||
|
Term::Lit { .. } | Term::Var { .. } => Ok(t.clone()),
|
||||||
|
Term::App { callee, args, tail } => {
|
||||||
|
let new_callee = self.lift_in_term(callee, locals, in_def)?;
|
||||||
|
let mut new_args = Vec::with_capacity(args.len());
|
||||||
|
for a in args {
|
||||||
|
new_args.push(self.lift_in_term(a, locals, in_def)?);
|
||||||
|
}
|
||||||
|
Ok(Term::App {
|
||||||
|
callee: Box::new(new_callee),
|
||||||
|
args: new_args,
|
||||||
|
tail: *tail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Term::Let { name, value, body } => {
|
||||||
|
let v = self.lift_in_term(value, locals, in_def)?;
|
||||||
|
// Synth the value's type (after lifting any nested
|
||||||
|
// LetRecs inside it), so the body's lift sees a
|
||||||
|
// resolved type for `name`.
|
||||||
|
let v_ty = self.synth_type(&v, locals, in_def)?;
|
||||||
|
let prev = locals.insert(name.clone(), v_ty);
|
||||||
|
let b = self.lift_in_term(body, locals, in_def)?;
|
||||||
|
match prev {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(name.clone(), p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Term::Let {
|
||||||
|
name: name.clone(),
|
||||||
|
value: Box::new(v),
|
||||||
|
body: Box::new(b),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Term::If { cond, then, else_ } => Ok(Term::If {
|
||||||
|
cond: Box::new(self.lift_in_term(cond, locals, in_def)?),
|
||||||
|
then: Box::new(self.lift_in_term(then, locals, in_def)?),
|
||||||
|
else_: Box::new(self.lift_in_term(else_, locals, in_def)?),
|
||||||
|
}),
|
||||||
|
Term::Do { op, args, tail } => {
|
||||||
|
let mut new_args = Vec::with_capacity(args.len());
|
||||||
|
for a in args {
|
||||||
|
new_args.push(self.lift_in_term(a, locals, in_def)?);
|
||||||
|
}
|
||||||
|
Ok(Term::Do {
|
||||||
|
op: op.clone(),
|
||||||
|
args: new_args,
|
||||||
|
tail: *tail,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Term::Ctor { type_name, ctor, args } => {
|
||||||
|
let mut new_args = Vec::with_capacity(args.len());
|
||||||
|
for a in args {
|
||||||
|
new_args.push(self.lift_in_term(a, locals, in_def)?);
|
||||||
|
}
|
||||||
|
Ok(Term::Ctor {
|
||||||
|
type_name: type_name.clone(),
|
||||||
|
ctor: ctor.clone(),
|
||||||
|
args: new_args,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Term::Match { scrutinee, arms } => {
|
||||||
|
let s = self.lift_in_term(scrutinee, locals, in_def)?;
|
||||||
|
// Synthesize the scrutinee's type so we can resolve
|
||||||
|
// pattern-arm bindings to typed locals.
|
||||||
|
let s_ty = self.synth_type(&s, locals, in_def)?;
|
||||||
|
let mut new_arms = Vec::with_capacity(arms.len());
|
||||||
|
for arm in arms {
|
||||||
|
let bindings = type_check_pattern_for_lift(&arm.pat, &s_ty, &self.env)?;
|
||||||
|
let mut pushed: Vec<(String, Option<Type>)> = Vec::new();
|
||||||
|
for (n, t) in &bindings {
|
||||||
|
let prev = locals.insert(n.clone(), t.clone());
|
||||||
|
pushed.push((n.clone(), prev));
|
||||||
|
}
|
||||||
|
let body = self.lift_in_term(&arm.body, locals, in_def)?;
|
||||||
|
for (n, prev) in pushed.into_iter().rev() {
|
||||||
|
match prev {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(n, p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_arms.push(Arm {
|
||||||
|
pat: arm.pat.clone(),
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(Term::Match {
|
||||||
|
scrutinee: Box::new(s),
|
||||||
|
arms: new_arms,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Term::Lam { params, param_tys, ret_ty, effects, body } => {
|
||||||
|
let mut pushed: Vec<(String, Option<Type>)> = Vec::new();
|
||||||
|
for (n, t) in params.iter().zip(param_tys.iter()) {
|
||||||
|
let prev = locals.insert(n.clone(), t.clone());
|
||||||
|
pushed.push((n.clone(), prev));
|
||||||
|
}
|
||||||
|
let new_body = self.lift_in_term(body, locals, in_def)?;
|
||||||
|
for (n, prev) in pushed.into_iter().rev() {
|
||||||
|
match prev {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(n, p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Term::Lam {
|
||||||
|
params: params.clone(),
|
||||||
|
param_tys: param_tys.clone(),
|
||||||
|
ret_ty: ret_ty.clone(),
|
||||||
|
effects: effects.clone(),
|
||||||
|
body: Box::new(new_body),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Term::Seq { lhs, rhs } => Ok(Term::Seq {
|
||||||
|
lhs: Box::new(self.lift_in_term(lhs, locals, in_def)?),
|
||||||
|
rhs: Box::new(self.lift_in_term(rhs, locals, in_def)?),
|
||||||
|
}),
|
||||||
|
Term::LetRec { name, ty, params, body, in_term } => {
|
||||||
|
// Iter 16b.3: post-order traversal — lift any inner
|
||||||
|
// LetRecs first. Within the body's scope, `name` and
|
||||||
|
// `params` are visible.
|
||||||
|
//
|
||||||
|
// Body-scope locals: name + params with their declared
|
||||||
|
// types (peeled from `ty` if Forall).
|
||||||
|
let inner_ty = match ty {
|
||||||
|
Type::Forall { body, .. } => (**body).clone(),
|
||||||
|
other => other.clone(),
|
||||||
|
};
|
||||||
|
let param_tys: Vec<Type> = match &inner_ty {
|
||||||
|
Type::Fn { params: ps, .. } => ps.clone(),
|
||||||
|
_ => {
|
||||||
|
return Err(CheckError::FnTypeRequired(
|
||||||
|
name.clone(),
|
||||||
|
ailang_core::pretty::type_to_string(&inner_ty),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut body_pushed: Vec<(String, Option<Type>)> = Vec::new();
|
||||||
|
let prev_name = locals.insert(name.clone(), ty.clone());
|
||||||
|
body_pushed.push((name.clone(), prev_name));
|
||||||
|
for (n, t) in params.iter().zip(param_tys.iter()) {
|
||||||
|
let prev = locals.insert(n.clone(), t.clone());
|
||||||
|
body_pushed.push((n.clone(), prev));
|
||||||
|
}
|
||||||
|
let lifted_body = self.lift_in_term(body, locals, in_def)?;
|
||||||
|
for (n, prev) in body_pushed.into_iter().rev() {
|
||||||
|
match prev {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(n, p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(&n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-clause scope: only `name` is visible.
|
||||||
|
let prev_in = locals.insert(name.clone(), ty.clone());
|
||||||
|
let lifted_in = self.lift_in_term(in_term, locals, in_def)?;
|
||||||
|
match prev_in {
|
||||||
|
Some(p) => {
|
||||||
|
locals.insert(name.clone(), p);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
locals.shift_remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive non-callee-use check (the desugar pass
|
||||||
|
// already ran this on the original LetRec, but
|
||||||
|
// post-lifting of inner LetRecs may have rewritten
|
||||||
|
// sub-terms — we re-run on the lifted body/in_term to
|
||||||
|
// be safe).
|
||||||
|
if find_non_callee_use(&lifted_body, name).is_some() {
|
||||||
|
panic!(
|
||||||
|
"Iter 16b.3 invariant: LetRec `{name}` used as a value in its own \
|
||||||
|
body after sub-term lift; should have been rejected at desugar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if find_non_callee_use(&lifted_in, name).is_some() {
|
||||||
|
panic!(
|
||||||
|
"Iter 16b.3 invariant: LetRec `{name}` used as a value in its \
|
||||||
|
in-clause after sub-term lift; should have been rejected at desugar"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute captures of the lifted body against the
|
||||||
|
// current `locals` (deterministic order via BTreeSet).
|
||||||
|
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(&lifted_body, &local_bound, &mut frees);
|
||||||
|
let captures: Vec<String> = frees
|
||||||
|
.iter()
|
||||||
|
.filter(|f| locals.contains_key(*f))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Resolve each capture's type via the locals table
|
||||||
|
// (populated as we walked).
|
||||||
|
let mut capture_types: Vec<(String, Type)> = Vec::new();
|
||||||
|
for c in &captures {
|
||||||
|
let t = locals.get(c).cloned().unwrap_or_else(|| {
|
||||||
|
panic!(
|
||||||
|
"Iter 16b.3 invariant: LetRec `{name}` capture `{c}` not in \
|
||||||
|
locals at lift time — capture set inconsistent with env walk"
|
||||||
|
)
|
||||||
|
});
|
||||||
|
capture_types.push((c.clone(), t));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the lifted FnDef.
|
||||||
|
let 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Type::Forall { .. } => panic!(
|
||||||
|
"Iter 16b.3 invariant: LetRec `{name}` has Forall type at lift; \
|
||||||
|
desugar should have rejected (queued for 16b.6)"
|
||||||
|
),
|
||||||
|
other => panic!(
|
||||||
|
"Iter 16b.3 invariant: LetRec `{name}` non-Fn/Forall type {:?}",
|
||||||
|
other
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let mut augmented_params = params.clone();
|
||||||
|
for (cn, _) in &capture_types {
|
||||||
|
augmented_params.push(cn.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let lifted_name = self.fresh_lifted(name);
|
||||||
|
let extras: Vec<String> =
|
||||||
|
capture_types.iter().map(|(n, _)| n.clone()).collect();
|
||||||
|
|
||||||
|
// Body rewrite: every `(app name args)` → `(app
|
||||||
|
// lifted_name args... cap0 cap1 ...)`. Then rename
|
||||||
|
// any leftover `Var{name}` (none in practice — already
|
||||||
|
// ruled out by find_non_callee_use). Symmetrical with
|
||||||
|
// the 16b.2 desugar lift.
|
||||||
|
let body_call_rw =
|
||||||
|
subst_call_with_extras(&lifted_body, name, &lifted_name, &extras);
|
||||||
|
let body_full = subst_var(&body_call_rw, name, &lifted_name);
|
||||||
|
|
||||||
|
// In-clause rewrite: same shape.
|
||||||
|
let in_call_rw =
|
||||||
|
subst_call_with_extras(&lifted_in, name, &lifted_name, &extras);
|
||||||
|
let in_full = subst_var(&in_call_rw, name, &lifted_name);
|
||||||
|
|
||||||
|
// Append the lifted FnDef. The doc string makes
|
||||||
|
// post-mortem debugging easier — anything containing
|
||||||
|
// `$lr_` plus this string is a 16b.3 lift.
|
||||||
|
let doc = format!(
|
||||||
|
"Lifted by 16b.3 from let-rec '{name}' inside '{in_def}'."
|
||||||
|
);
|
||||||
|
self.lifted.push(Def::Fn(FnDef {
|
||||||
|
name: lifted_name.clone(),
|
||||||
|
ty: augmented_ty.clone(),
|
||||||
|
params: augmented_params,
|
||||||
|
body: body_full,
|
||||||
|
doc: Some(doc),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Update env globals so any outer LetRec lifted later
|
||||||
|
// sees the new top-level fn.
|
||||||
|
self.env
|
||||||
|
.globals
|
||||||
|
.insert(lifted_name.clone(), augmented_ty);
|
||||||
|
self.module_names.insert(lifted_name);
|
||||||
|
|
||||||
|
Ok(in_full)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: produce a fresh `<hint>$lr_N` name not present in
|
||||||
|
/// `module_names`. Bumps `counter` and `module_names` so a later
|
||||||
|
/// lift cannot collide.
|
||||||
|
fn fresh_lifted(&mut self, hint: &str) -> String {
|
||||||
|
loop {
|
||||||
|
let candidate = format!("{hint}$lr_{}", self.counter);
|
||||||
|
self.counter += 1;
|
||||||
|
if !self.module_names.contains(&candidate) {
|
||||||
|
self.module_names.insert(candidate.clone());
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synthesize the type of an already-lifted sub-term against the
|
||||||
|
/// current env+locals. Used for `Term::Let.value` (so the body's
|
||||||
|
/// `name` gets a typed local) and `Term::Match.scrutinee` (so
|
||||||
|
/// pattern bindings get typed locals).
|
||||||
|
///
|
||||||
|
/// We re-run inference on the sub-term — it has already passed
|
||||||
|
/// the typechecker once before lift, so this is guaranteed to
|
||||||
|
/// succeed under the same env / locals.
|
||||||
|
fn synth_type(
|
||||||
|
&mut self,
|
||||||
|
t: &Term,
|
||||||
|
locals: &mut IndexMap<String, Type>,
|
||||||
|
in_def: &str,
|
||||||
|
) -> Result<Type> {
|
||||||
|
let mut subst = Subst::default();
|
||||||
|
let mut counter: u32 = 0;
|
||||||
|
let mut effects: BTreeSet<String> = BTreeSet::new();
|
||||||
|
let ty = synth(t, &self.env, locals, &mut effects, in_def, &mut subst, &mut counter)?;
|
||||||
|
Ok(subst.apply(&ty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: returns true iff any `Def::Fn` body or `Def::Const`
|
||||||
|
/// value in `m` reaches a `Term::LetRec`. Used as a fast-path skip:
|
||||||
|
/// modules without any deferred LetRec need no traversal at all.
|
||||||
|
fn contains_any_letrec(m: &Module) -> bool {
|
||||||
|
fn term_has_letrec(t: &Term) -> bool {
|
||||||
|
match t {
|
||||||
|
Term::Lit { .. } | Term::Var { .. } => false,
|
||||||
|
Term::App { callee, args, .. } => {
|
||||||
|
term_has_letrec(callee) || args.iter().any(term_has_letrec)
|
||||||
|
}
|
||||||
|
Term::Let { value, body, .. } => term_has_letrec(value) || term_has_letrec(body),
|
||||||
|
Term::If { cond, then, else_ } => {
|
||||||
|
term_has_letrec(cond) || term_has_letrec(then) || term_has_letrec(else_)
|
||||||
|
}
|
||||||
|
Term::Do { args, .. } => args.iter().any(term_has_letrec),
|
||||||
|
Term::Ctor { args, .. } => args.iter().any(term_has_letrec),
|
||||||
|
Term::Match { scrutinee, arms } => {
|
||||||
|
term_has_letrec(scrutinee) || arms.iter().any(|a| term_has_letrec(&a.body))
|
||||||
|
}
|
||||||
|
Term::Lam { body, .. } => term_has_letrec(body),
|
||||||
|
Term::Seq { lhs, rhs } => term_has_letrec(lhs) || term_has_letrec(rhs),
|
||||||
|
Term::LetRec { .. } => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for def in &m.defs {
|
||||||
|
match def {
|
||||||
|
Def::Fn(f) if term_has_letrec(&f.body) => return true,
|
||||||
|
Def::Const(c) if term_has_letrec(&c.value) => return true,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: scan `defs` for the highest existing `*$lr_N` suffix
|
||||||
|
/// and return that N. Used to seed `Lifter.counter` past any
|
||||||
|
/// 16b.2-lifted defs that already live in the desugared module.
|
||||||
|
fn highest_lr_suffix(defs: &[Def]) -> Option<u64> {
|
||||||
|
let mut max: Option<u64> = None;
|
||||||
|
for def in defs {
|
||||||
|
let name = def.name();
|
||||||
|
if let Some(idx) = name.rfind("$lr_") {
|
||||||
|
let suffix = &name[idx + "$lr_".len()..];
|
||||||
|
if let Ok(n) = suffix.parse::<u64>() {
|
||||||
|
max = Some(max.map(|m| m.max(n)).unwrap_or(n));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
max
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iter 16b.3: minimal pattern → bindings inference for the lift
|
||||||
|
/// pass. Called only for patterns the typechecker has already
|
||||||
|
/// accepted, so we propagate just enough to resolve capture types
|
||||||
|
/// (we don't re-run unification here — the typechecker did that
|
||||||
|
/// already).
|
||||||
|
///
|
||||||
|
/// Mirrors the relevant arms of `crate::type_check_pattern` but only
|
||||||
|
/// for the side effect we need: returning a mapping
|
||||||
|
/// `binder_name -> Type`. Returns `Internal` if the pattern shape
|
||||||
|
/// cannot be handled — every shape that the typechecker accepts
|
||||||
|
/// reaches here.
|
||||||
|
fn type_check_pattern_for_lift(
|
||||||
|
p: &Pattern,
|
||||||
|
s_ty: &Type,
|
||||||
|
env: &Env,
|
||||||
|
) -> Result<Vec<(String, Type)>> {
|
||||||
|
match p {
|
||||||
|
Pattern::Wild | Pattern::Lit { .. } => Ok(vec![]),
|
||||||
|
Pattern::Var { name } => Ok(vec![(name.clone(), s_ty.clone())]),
|
||||||
|
Pattern::Ctor { ctor, fields } => {
|
||||||
|
// Resolve the ctor against the scrutinee's type.
|
||||||
|
let (td, type_args, owner_module): (TypeDef, Vec<Type>, Option<String>) =
|
||||||
|
match s_ty {
|
||||||
|
Type::Con { name, args } => {
|
||||||
|
if name.matches('.').count() == 1 {
|
||||||
|
let (prefix, suffix) = name.split_once('.').expect("checked");
|
||||||
|
let target_module = env
|
||||||
|
.imports
|
||||||
|
.get(prefix)
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| {
|
||||||
|
if env.module_types.contains_key(prefix) {
|
||||||
|
Some(prefix.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok_or_else(|| CheckError::UnknownModule {
|
||||||
|
module: prefix.to_string(),
|
||||||
|
})?;
|
||||||
|
let td = env
|
||||||
|
.module_types
|
||||||
|
.get(&target_module)
|
||||||
|
.and_then(|tys| tys.get(suffix))
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| CheckError::UnknownType(name.clone()))?;
|
||||||
|
(td, args.clone(), Some(target_module))
|
||||||
|
} else {
|
||||||
|
let td = env
|
||||||
|
.types
|
||||||
|
.get(name)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| CheckError::UnknownType(name.clone()))?;
|
||||||
|
(td, args.clone(), None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(CheckError::PatternTypeMismatch {
|
||||||
|
ctor: ctor.clone(),
|
||||||
|
ty: ailang_core::pretty::type_to_string(s_ty),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let cdef = td
|
||||||
|
.ctors
|
||||||
|
.iter()
|
||||||
|
.find(|c| &c.name == ctor)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| CheckError::UnknownCtor {
|
||||||
|
ty: td.name.clone(),
|
||||||
|
ctor: ctor.clone(),
|
||||||
|
})?;
|
||||||
|
if fields.len() != cdef.fields.len() {
|
||||||
|
return Err(CheckError::CtorArity {
|
||||||
|
ty: td.name.clone(),
|
||||||
|
ctor: ctor.clone(),
|
||||||
|
expected: cdef.fields.len(),
|
||||||
|
got: fields.len(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Substitute the ADT's type vars with the actual args
|
||||||
|
// from the scrutinee's `Type::Con.args`. Field types may
|
||||||
|
// contain owning-module-qualified type-cons references
|
||||||
|
// (the same qualification dance `synth` does for
|
||||||
|
// `Term::Ctor`), but we don't need to do it here — the
|
||||||
|
// pattern var's recorded type is consumed only by the
|
||||||
|
// lifter to seed locals, and any capture's type that
|
||||||
|
// includes a qualified type-cons gets passed verbatim
|
||||||
|
// into the lifted FnDef's signature.
|
||||||
|
let _ = owner_module; // (unused in this minimal lookup)
|
||||||
|
let mut mapping: BTreeMap<String, Type> = BTreeMap::new();
|
||||||
|
for (v, a) in td.vars.iter().zip(type_args.iter()) {
|
||||||
|
mapping.insert(v.clone(), a.clone());
|
||||||
|
}
|
||||||
|
let mut out: Vec<(String, Type)> = Vec::new();
|
||||||
|
for (sub_pat, field_ty) in fields.iter().zip(cdef.fields.iter()) {
|
||||||
|
let inst_ty = substitute_rigids_local(field_ty, &mapping);
|
||||||
|
out.extend(type_check_pattern_for_lift(sub_pat, &inst_ty, env)?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Local copy of the substitution helper from `crate`. Inlined here
|
||||||
|
/// because the original is private to the parent module; the
|
||||||
|
/// lifter only needs the mono-substitution case.
|
||||||
|
fn substitute_rigids_local(t: &Type, mapping: &BTreeMap<String, Type>) -> Type {
|
||||||
|
match t {
|
||||||
|
Type::Var { name } => mapping.get(name).cloned().unwrap_or_else(|| t.clone()),
|
||||||
|
Type::Con { name, args } => Type::Con {
|
||||||
|
name: name.clone(),
|
||||||
|
args: args.iter().map(|a| substitute_rigids_local(a, mapping)).collect(),
|
||||||
|
},
|
||||||
|
Type::Fn { params, ret, effects } => Type::Fn {
|
||||||
|
params: params
|
||||||
|
.iter()
|
||||||
|
.map(|p| substitute_rigids_local(p, mapping))
|
||||||
|
.collect(),
|
||||||
|
ret: Box::new(substitute_rigids_local(ret, mapping)),
|
||||||
|
effects: effects.clone(),
|
||||||
|
},
|
||||||
|
Type::Forall { vars, body } => {
|
||||||
|
let inner: BTreeMap<String, Type> = mapping
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, _)| !vars.contains(k))
|
||||||
|
.map(|(k, v)| (k.clone(), v.clone()))
|
||||||
|
.collect();
|
||||||
|
Type::Forall {
|
||||||
|
vars: vars.clone(),
|
||||||
|
body: Box::new(substitute_rigids_local(body, &inner)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -462,11 +462,26 @@ impl Desugarer {
|
|||||||
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
|
||||||
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
// Iter 16b.2: extend the lift to the path-1 safe subset —
|
||||||
// captures of fn-params / Lam-params (whose types are
|
// captures of fn-params / Lam-params (whose types are
|
||||||
// known statically). Captures of Let / MatchArm /
|
// known statically).
|
||||||
// EnclosingLetRec names, captures from a polymorphic
|
// Iter 16b.3: when ANY capture is `LetBound` the desugar
|
||||||
// enclosing fn, and any non-callee use of `name` are
|
// pass cannot resolve its type (the let-value's type is
|
||||||
// still rejected at desugar time, with a follow-up
|
// only known after typecheck). For that case we LEAVE
|
||||||
// iter pointer in the panic message.
|
// 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.
|
||||||
|
//
|
||||||
|
// 16b.2's fast path (KnownType-only captures → lift here)
|
||||||
|
// STILL fires when applicable. Only when at least one
|
||||||
|
// capture is `LetBound` does the desugar defer.
|
||||||
|
//
|
||||||
|
// `MatchArm` and `EnclosingLetRec` captures STILL panic
|
||||||
|
// (16b.4 / 16b.7 — separate iters needing extra
|
||||||
|
// machinery beyond what `lift_letrecs` does).
|
||||||
|
//
|
||||||
|
// 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
|
// Peel `ty` to its inner Fn so we know the LetRec's
|
||||||
// own param types (for body-scope) and so we can
|
// own param types (for body-scope) and so we can
|
||||||
@@ -474,13 +489,13 @@ impl Desugarer {
|
|||||||
let inner_params_tys: Vec<Type> = match peel_forall_to_fn(ty) {
|
let inner_params_tys: Vec<Type> = match peel_forall_to_fn(ty) {
|
||||||
Some(Type::Fn { params: ps, .. }) => ps.clone(),
|
Some(Type::Fn { params: ps, .. }) => ps.clone(),
|
||||||
_ => panic!(
|
_ => panic!(
|
||||||
"Iter 16b.2: LetRec `{}` must have a Fn type (or Forall<Fn>); got {:?}",
|
"Iter 16b.3: LetRec `{}` must have a Fn type (or Forall<Fn>); got {:?}",
|
||||||
name, ty
|
name, ty
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
if inner_params_tys.len() != params.len() {
|
if inner_params_tys.len() != params.len() {
|
||||||
panic!(
|
panic!(
|
||||||
"Iter 16b.2: LetRec `{}` param count {} != type's param count {}",
|
"Iter 16b.3: LetRec `{}` param count {} != type's param count {}",
|
||||||
name, params.len(), inner_params_tys.len()
|
name, params.len(), inner_params_tys.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -500,6 +515,29 @@ impl Desugarer {
|
|||||||
in_scope.insert(name.clone(), ScopeEntry::EnclosingLetRec);
|
in_scope.insert(name.clone(), ScopeEntry::EnclosingLetRec);
|
||||||
let desugared_in = self.desugar_term(in_term, &in_scope);
|
let desugared_in = self.desugar_term(in_term, &in_scope);
|
||||||
|
|
||||||
|
// 16b.2/16b.5: validate that `name` is only ever used as
|
||||||
|
// the callee of a Term::App in body and in_term — never
|
||||||
|
// as a Term::Var in any value position. Run BEFORE
|
||||||
|
// capture classification so the diagnostic fires
|
||||||
|
// regardless of whether we end up lifting here or
|
||||||
|
// deferring to the post-typecheck pass.
|
||||||
|
if let Some(violation) = find_non_callee_use(&desugared_body, name) {
|
||||||
|
panic!(
|
||||||
|
"Iter 16b.3: LetRec `{}` is used as a value in its own body \
|
||||||
|
(not as the callee of `app`); not supported. Queued for 16b.5 \
|
||||||
|
(closure conversion). Offending term: {:?}",
|
||||||
|
name, violation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(violation) = find_non_callee_use(&desugared_in, name) {
|
||||||
|
panic!(
|
||||||
|
"Iter 16b.3: LetRec `{}` is used as a value in the in-clause \
|
||||||
|
(not as the callee of `app`); not supported. Queued for 16b.5. \
|
||||||
|
Offending term: {:?}",
|
||||||
|
name, violation
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Capture detection (against the *outer* scope, before
|
// Capture detection (against the *outer* scope, before
|
||||||
// the body-scope extension).
|
// the body-scope extension).
|
||||||
let mut local_bound: BTreeSet<String> = BTreeSet::new();
|
let mut local_bound: BTreeSet<String> = BTreeSet::new();
|
||||||
@@ -515,29 +553,30 @@ impl Desugarer {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// 16b.2: classify each capture's ScopeEntry. KnownType
|
// 16b.3: classify each capture's ScopeEntry.
|
||||||
// is supported; everything else errors with a follow-up
|
// - All KnownType → lift here (16b.2 fast path).
|
||||||
// iter pointer.
|
// - Any LetBound → defer to `lift_letrecs` (16b.3).
|
||||||
let mut capture_types: Vec<(String, Type)> = Vec::new();
|
// - Any MatchArm → panic (16b.4).
|
||||||
|
// - Any EnclosingLetRec → panic (16b.7).
|
||||||
|
// Mixed KnownType+LetBound get deferred (decision is
|
||||||
|
// all-or-nothing for a single LetRec; the
|
||||||
|
// post-typecheck pass handles every capture kind it
|
||||||
|
// supports uniformly).
|
||||||
|
let mut has_let_bound = false;
|
||||||
for c in &captures {
|
for c in &captures {
|
||||||
match scope.get(c).expect("capture-in-scope-by-construction") {
|
match scope.get(c).expect("capture-in-scope-by-construction") {
|
||||||
ScopeEntry::KnownType(t) => {
|
ScopeEntry::KnownType(_) => {}
|
||||||
capture_types.push((c.clone(), t.clone()));
|
ScopeEntry::LetBound => {
|
||||||
|
has_let_bound = true;
|
||||||
}
|
}
|
||||||
ScopeEntry::LetBound => panic!(
|
|
||||||
"Iter 16b.2: LetRec `{}` captures `{}` from a let-binding \
|
|
||||||
(or polymorphic enclosing fn); not supported. Queued for \
|
|
||||||
16b.3 (let-binding captures with type info from typecheck).",
|
|
||||||
name, c
|
|
||||||
),
|
|
||||||
ScopeEntry::MatchArm => panic!(
|
ScopeEntry::MatchArm => panic!(
|
||||||
"Iter 16b.2: LetRec `{}` captures `{}` from a match-arm \
|
"Iter 16b.3: LetRec `{}` captures `{}` from a match-arm \
|
||||||
pattern binding; not supported. Queued for 16b.4 (match-arm \
|
pattern binding; not supported. Queued for 16b.4 (match-arm \
|
||||||
captures with constructor-field substitution).",
|
captures with constructor-field substitution).",
|
||||||
name, c
|
name, c
|
||||||
),
|
),
|
||||||
ScopeEntry::EnclosingLetRec => panic!(
|
ScopeEntry::EnclosingLetRec => panic!(
|
||||||
"Iter 16b.2: LetRec `{}` captures `{}` from an enclosing \
|
"Iter 16b.3: LetRec `{}` captures `{}` from an enclosing \
|
||||||
LetRec; nested mutual-capture is not supported. Queued for \
|
LetRec; nested mutual-capture is not supported. Queued for \
|
||||||
16b.7.",
|
16b.7.",
|
||||||
name, c
|
name, c
|
||||||
@@ -545,29 +584,29 @@ impl Desugarer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 16b.2: validate that `name` is only ever used as the
|
// 16b.3: defer-arm. At least one capture is LetBound, so
|
||||||
// callee of a Term::App in body and in_term — never as
|
// we can't resolve the lifted signature here. Reconstruct
|
||||||
// a Term::Var in any value position. This guards
|
// the LetRec with desugared sub-terms and let
|
||||||
// against `(let g f ...)` bindings and `(app some_hof
|
// `ailang-check::lift_letrecs` handle it after typecheck.
|
||||||
// f)` value-passing, which would need closure
|
if has_let_bound {
|
||||||
// conversion.
|
return Term::LetRec {
|
||||||
if let Some(violation) = find_non_callee_use(&desugared_body, name) {
|
name: name.clone(),
|
||||||
panic!(
|
ty: ty.clone(),
|
||||||
"Iter 16b.2: LetRec `{}` is used as a value in its own body \
|
params: params.clone(),
|
||||||
(not as the callee of `app`); not supported. Queued for 16b.5 \
|
body: Box::new(desugared_body),
|
||||||
(closure conversion). Offending term: {:?}",
|
in_term: Box::new(desugared_in),
|
||||||
name, violation
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(violation) = find_non_callee_use(&desugared_in, name) {
|
|
||||||
panic!(
|
|
||||||
"Iter 16b.2: LetRec `{}` is used as a value in the in-clause \
|
|
||||||
(not as the callee of `app`); not supported. Queued for 16b.5. \
|
|
||||||
Offending term: {:?}",
|
|
||||||
name, violation
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Build augmented type: original Fn with capture types
|
||||||
// appended to `params`. A Forall LetRec is rejected
|
// appended to `params`. A Forall LetRec is rejected
|
||||||
// (would need synthesised Forall — out of scope).
|
// (would need synthesised Forall — out of scope).
|
||||||
@@ -584,12 +623,12 @@ impl Desugarer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Type::Forall { .. } => panic!(
|
Type::Forall { .. } => panic!(
|
||||||
"Iter 16b.2: LetRec `{}` has a Forall type; captures from a \
|
"Iter 16b.3: LetRec `{}` has a Forall type; captures from a \
|
||||||
polymorphic context are not supported. Queued for 16b.6.",
|
polymorphic context are not supported. Queued for 16b.6.",
|
||||||
name
|
name
|
||||||
),
|
),
|
||||||
other => panic!(
|
other => panic!(
|
||||||
"Iter 16b.2: LetRec `{}` has non-Fn/Forall type {:?}",
|
"Iter 16b.3: LetRec `{}` has non-Fn/Forall type {:?}",
|
||||||
name, other
|
name, other
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -830,9 +869,13 @@ fn build_eq(scrutinee: Term, lit: &Literal) -> Term {
|
|||||||
/// arms, [`Term::LetRec`]) extend `bound` for the sub-walk.
|
/// arms, [`Term::LetRec`]) extend `bound` for the sub-walk.
|
||||||
///
|
///
|
||||||
/// Used by the [`Term::LetRec`] desugar to decide whether a local
|
/// Used by the [`Term::LetRec`] desugar to decide whether a local
|
||||||
/// recursive let would capture any name from the enclosing scope
|
/// recursive let would capture any name from the enclosing scope.
|
||||||
/// (16b.2 has not shipped yet, so capture is a panic at desugar time).
|
///
|
||||||
fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<String>) {
|
/// Iter 16b.3: made `pub` so the post-typecheck `lift_letrecs` pass
|
||||||
|
/// in `ailang-check` can reuse it (same free-var computation; the
|
||||||
|
/// post-typecheck pass needs it to recompute captures of LetRec
|
||||||
|
/// nodes that desugar deferred).
|
||||||
|
pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<String>) {
|
||||||
match t {
|
match t {
|
||||||
Term::Lit { .. } => {}
|
Term::Lit { .. } => {}
|
||||||
Term::Var { name } => {
|
Term::Var { name } => {
|
||||||
@@ -905,7 +948,11 @@ fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<Stri
|
|||||||
/// Iter 16b.1: collect every name a pattern binds into `out`. Mirrors
|
/// Iter 16b.1: collect every name a pattern binds into `out`. Mirrors
|
||||||
/// `Pattern::pattern_bound_names` in `ailang-codegen`; duplicated here
|
/// `Pattern::pattern_bound_names` in `ailang-codegen`; duplicated here
|
||||||
/// because `ailang-core` cannot depend on the codegen crate.
|
/// because `ailang-core` cannot depend on the codegen crate.
|
||||||
fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
|
///
|
||||||
|
/// Iter 16b.3: made `pub` so callers (e.g. `ailang-check::lift_letrecs`)
|
||||||
|
/// can reproduce the same shadowing semantics during their own
|
||||||
|
/// scope-aware walks.
|
||||||
|
pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
|
||||||
match p {
|
match p {
|
||||||
Pattern::Wild | Pattern::Lit { .. } => {}
|
Pattern::Wild | Pattern::Lit { .. } => {}
|
||||||
Pattern::Var { name } => {
|
Pattern::Var { name } => {
|
||||||
@@ -927,7 +974,10 @@ fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
|
|||||||
/// Used after lifting a [`Term::LetRec`] body to a synthetic top-level
|
/// Used after lifting a [`Term::LetRec`] body to a synthetic top-level
|
||||||
/// fn — every recursive self-call site in the lifted body and every
|
/// 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.
|
/// reference in the in-term needs to point at the new global name.
|
||||||
fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
///
|
||||||
|
/// Iter 16b.3: made `pub` for the same reason as
|
||||||
|
/// [`subst_call_with_extras`].
|
||||||
|
pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
|
||||||
match t {
|
match t {
|
||||||
Term::Lit { .. } => t.clone(),
|
Term::Lit { .. } => t.clone(),
|
||||||
Term::Var { name } => {
|
Term::Var { name } => {
|
||||||
@@ -1046,7 +1096,12 @@ fn peel_forall_to_fn(t: &Type) -> Option<&Type> {
|
|||||||
/// args ++ extras_as_vars }`. Recurses into all sub-terms. Does
|
/// args ++ extras_as_vars }`. Recurses into all sub-terms. Does
|
||||||
/// not touch `Term::Var { name }` in non-callee positions — that's
|
/// not touch `Term::Var { name }` in non-callee positions — that's
|
||||||
/// flagged separately by [`find_non_callee_use`].
|
/// flagged separately by [`find_non_callee_use`].
|
||||||
fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[String]) -> Term {
|
///
|
||||||
|
/// Iter 16b.3: made `pub` so the post-typecheck `lift_letrecs` pass
|
||||||
|
/// in `ailang-check` can reuse it (same call-site rewrite shape; the
|
||||||
|
/// only difference is that the capture types come from the
|
||||||
|
/// typechecker's env rather than being statically known).
|
||||||
|
pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[String]) -> Term {
|
||||||
match t {
|
match t {
|
||||||
Term::Lit { .. } => t.clone(),
|
Term::Lit { .. } => t.clone(),
|
||||||
Term::Var { .. } => t.clone(),
|
Term::Var { .. } => t.clone(),
|
||||||
@@ -1142,7 +1197,10 @@ fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[String])
|
|||||||
/// `Term::App`. Returns `None` if every reference is in callee
|
/// `Term::App`. Returns `None` if every reference is in callee
|
||||||
/// position. Used by the LetRec lifter to enforce the "direct-call
|
/// position. Used by the LetRec lifter to enforce the "direct-call
|
||||||
/// only" restriction in 16b.2.
|
/// only" restriction in 16b.2.
|
||||||
fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
///
|
||||||
|
/// Iter 16b.3: made `pub` for the same reason as
|
||||||
|
/// [`subst_call_with_extras`].
|
||||||
|
pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
|
||||||
match t {
|
match t {
|
||||||
Term::Lit { .. } => None,
|
Term::Lit { .. } => None,
|
||||||
Term::Var { name: n } if n == name => Some(t.clone()),
|
Term::Var { name: n } if n == name => Some(t.clone()),
|
||||||
@@ -1556,12 +1614,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter 16b.2: a LetRec whose body captures a `Term::Let`-bound
|
/// Iter 16b.3: a LetRec whose body captures a `Term::Let`-bound
|
||||||
/// name has no statically-known type at desugar time; the
|
/// name is no longer rejected at desugar time. Instead the
|
||||||
/// lifter rejects with a `16b.3` pointer.
|
/// 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]
|
#[test]
|
||||||
#[should_panic(expected = "16b.3")]
|
fn let_rec_capture_let_binding_is_deferred_to_post_typecheck() {
|
||||||
fn let_rec_capture_let_binding_panics() {
|
|
||||||
// (let y 7 in (let-rec helper (params x) ... (body (app + x y))
|
// (let y 7 in (let-rec helper (params x) ... (body (app + x y))
|
||||||
// (in (app helper 1))))
|
// (in (app helper 1))))
|
||||||
let letrec = Term::LetRec {
|
let letrec = Term::LetRec {
|
||||||
@@ -1606,7 +1666,25 @@ mod tests {
|
|||||||
doc: None,
|
doc: None,
|
||||||
})],
|
})],
|
||||||
};
|
};
|
||||||
let _ = desugar_module(&m);
|
let out = desugar_module(&m);
|
||||||
|
// No fn was lifted (the LetRec is left in place) — the
|
||||||
|
// module's defs count is unchanged.
|
||||||
|
assert_eq!(
|
||||||
|
out.defs.len(),
|
||||||
|
1,
|
||||||
|
"expected no lifted fn (LetRec deferred); got {:?}",
|
||||||
|
out.defs.iter().map(|d| d.name()).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
// The original LetRec must STILL be present somewhere in
|
||||||
|
// main's body.
|
||||||
|
let main_body = match &out.defs[0] {
|
||||||
|
Def::Fn(f) => &f.body,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
any_let_rec(main_body),
|
||||||
|
"expected Term::LetRec to still be present in body; got {main_body:#?}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iter 16b.2: if the LetRec name is used as a value (not as
|
/// Iter 16b.2: if the LetRec name is used as a value (not as
|
||||||
|
|||||||
@@ -701,6 +701,7 @@ conversion in JOURNAL). A `seq` term evaluates `lhs` for its effects
|
|||||||
├─ resolve names + assign hashes
|
├─ resolve names + assign hashes
|
||||||
├─ desugar (AST → AST, Iter 16a)
|
├─ desugar (AST → AST, Iter 16a)
|
||||||
├─ typecheck (HM, effect rows)
|
├─ typecheck (HM, effect rows)
|
||||||
|
├─ lift_letrecs (post-typecheck AST → AST, Iter 16b.3)
|
||||||
├─ lower to MIR (SSA-like, named SSA values)
|
├─ lower to MIR (SSA-like, named SSA values)
|
||||||
├─ emit LLVM IR (.ll)
|
├─ emit LLVM IR (.ll)
|
||||||
└─ clang -O2 *.ll -o binary (links libgc for @GC_malloc)
|
└─ clang -O2 *.ll -o binary (links libgc for @GC_malloc)
|
||||||
@@ -716,6 +717,17 @@ in the `check` entry point continues to hash from the *original*
|
|||||||
on-disk module, not the desugared one, so `ail diff` and `ail manifest`
|
on-disk module, not the desugared one, so `ail diff` and `ail manifest`
|
||||||
report identities that match the canonical JSON the user is editing.
|
report identities that match the canonical JSON the user is editing.
|
||||||
|
|
||||||
|
The **lift_letrecs** pass (`ailang-check::lift_letrecs`, Iter 16b.3)
|
||||||
|
runs **after** typecheck and **before** codegen, but only on the
|
||||||
|
`build` / `run` paths — the `check` subcommand stops at typecheck
|
||||||
|
and never sees a lifted module. It eliminates every `Term::LetRec`
|
||||||
|
that the desugar pass left in place (the case where at least one
|
||||||
|
capture is `Term::Let`-bound, so its type is only knowable after
|
||||||
|
inference). The output is a module with synthetic `<hint>$lr_N`
|
||||||
|
top-level fns appended, ready for codegen. Synthetic FnDefs added
|
||||||
|
by this pass do **not** appear in `CheckedModule.symbols` — same
|
||||||
|
invariant as the 16b.2 lifts in desugar.
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
+190
@@ -3697,3 +3697,193 @@ accumulates captures across nesting). 16d (chain-machinery
|
|||||||
exhaustiveness or `__unreachable__` — planning needed), 16e
|
exhaustiveness or `__unreachable__` — planning needed), 16e
|
||||||
(`==` extension to Bool/Str/Unit), 17a (per-fn arena, gated)
|
(`==` extension to Bool/Str/Unit), 17a (per-fn arena, gated)
|
||||||
unchanged.
|
unchanged.
|
||||||
|
|
||||||
|
## Iter 16b.3 — LetRec captures of Let-bound names
|
||||||
|
|
||||||
|
**Goal.** Lift 16b.2's "fn/Lam-param captures only" restriction. A
|
||||||
|
`(let-rec ...)` may now capture names bound by a `Term::Let` in the
|
||||||
|
enclosing scope. Match-arm captures (16b.4), name-as-value (16b.5),
|
||||||
|
Forall enclosing fn (16b.6), and nested LetRec mutual capture
|
||||||
|
(16b.7) remain rejected at desugar time.
|
||||||
|
|
||||||
|
**Architectural choice (path-2: post-typecheck lift).** The desugar
|
||||||
|
pass cannot resolve a `Term::Let`-bound name's type — `Term::Let`
|
||||||
|
carries no annotation; the value's type is inferred at typecheck.
|
||||||
|
Three options were on the table (path-1 = stay-at-desugar with
|
||||||
|
restrictions, path-2 = post-typecheck lift, path-3 = run a private
|
||||||
|
inference inside desugar). Path-2 is the cleanest: typechecker is
|
||||||
|
the single source of truth, and the lift now becomes a small
|
||||||
|
AST-→-AST pass with O(1) type lookups against the typechecker's
|
||||||
|
env. `Term::LetRec` reaches the typechecker only when the desugar
|
||||||
|
pass deferred it — for the 16b.2 fast-path (KnownType captures
|
||||||
|
only) the desugar still does the lift in one hop.
|
||||||
|
|
||||||
|
**What shipped.**
|
||||||
|
|
||||||
|
- `crates/ailang-core/src/desugar.rs` (1853 → 1931 LOC, +78). The
|
||||||
|
`Term::LetRec` arm now has three exits:
|
||||||
|
(a) `KnownType`-only captures → existing 16b.2 lift (unchanged).
|
||||||
|
(b) Any `LetBound` capture → reconstruct the `Term::LetRec`
|
||||||
|
with desugared sub-terms and return it (defer to
|
||||||
|
post-typecheck pass).
|
||||||
|
(c) `MatchArm` / `EnclosingLetRec` → panic with the same
|
||||||
|
follow-up-iter pointers as 16b.2.
|
||||||
|
The `find_non_callee_use` (16b.5) check moved to run before
|
||||||
|
classification so the diagnostic fires consistently regardless
|
||||||
|
of which exit is taken. Four helpers (`free_vars_in_term`,
|
||||||
|
`subst_var`, `subst_call_with_extras`, `find_non_callee_use`,
|
||||||
|
`pattern_binds`) were promoted from `fn` to `pub fn` so the
|
||||||
|
post-typecheck pass can reuse them.
|
||||||
|
- `crates/ailang-check/src/lib.rs` (2887 → 3281 LOC, +394
|
||||||
|
including tests).
|
||||||
|
- `verify_tail_positions` and `synth` arms for `Term::LetRec`
|
||||||
|
replaced. `verify_tail_positions`: body is NOT in tail
|
||||||
|
position (it's a fn body — the LetRec name is what gets
|
||||||
|
tail-called); in_term inherits the enclosing context. `synth`:
|
||||||
|
peel any `Forall` defensively, validate param count, install
|
||||||
|
`name + params` in locals for the body, synth the body, unify
|
||||||
|
against `ret_ty`, check the effect-subset rule (same as
|
||||||
|
`Term::Lam`); restore locals; install `name` for the
|
||||||
|
in-clause; synth, return.
|
||||||
|
- New `pub fn check_and_lift(m) -> Result<(CheckedModule,
|
||||||
|
Module)>` runs check + desugar + `lift_letrecs` and returns
|
||||||
|
both the original-symbols `CheckedModule` and the lifted
|
||||||
|
module ready for codegen.
|
||||||
|
- `crates/ailang-check/src/lift.rs` (new file, 720 LOC). The
|
||||||
|
`pub fn lift_letrecs(m: &Module) -> Result<Module>` post-pass.
|
||||||
|
- Fast-path: `contains_any_letrec(m)` returns `false` →
|
||||||
|
return input unchanged. Skips env construction so cross-
|
||||||
|
module modules (which we don't fully wire up here) are not
|
||||||
|
touched unless they actually contain a deferred LetRec.
|
||||||
|
- Builds a single-module env (builtins + module type defs +
|
||||||
|
module globals + imports + current_module) and walks every
|
||||||
|
`Def::Fn`'s body, threading a parallel `IndexMap<String,
|
||||||
|
Type>` of locals as scope. At each `Term::Let`, synth the
|
||||||
|
value's type to populate locals; at each `Term::Match`
|
||||||
|
arm, run a minimal `type_check_pattern_for_lift` to infer
|
||||||
|
pattern-arm bindings.
|
||||||
|
- At every `Term::LetRec`: post-order recurse first (so
|
||||||
|
nested LetRecs are lifted before their enclosing one),
|
||||||
|
re-run `find_non_callee_use` defensively, recompute
|
||||||
|
captures via `free_vars_in_term`, look up each capture's
|
||||||
|
type from `locals`, build the lifted `FnDef` (capture types
|
||||||
|
appended to params), append to a `lifted: Vec<Def>`
|
||||||
|
accumulator, rewrite call sites in body and in_term via
|
||||||
|
`subst_call_with_extras`, then `subst_var` for any
|
||||||
|
leftover bare references. Synthetic name `<hint>$lr_N`
|
||||||
|
seeded past the highest existing `*$lr_N` suffix in
|
||||||
|
`m.defs` so it cannot collide with a 16b.2 lift. Synthetic
|
||||||
|
FnDefs carry a `doc` of the form
|
||||||
|
`"Lifted by 16b.3 from let-rec '<name>' inside '<enclosing>'."`.
|
||||||
|
- The `subst_call_with_extras` and `subst_var` helpers come
|
||||||
|
straight from `ailang-core::desugar` — re-used rather than
|
||||||
|
duplicated (the brief asked for one or the other; re-use
|
||||||
|
via `pub` keeps the rewrite logic single-sourced).
|
||||||
|
- `crates/ail/src/main.rs` (+25 LOC). `build_to` now goes
|
||||||
|
`load → check → per-module (desugar + lift_letrecs) → codegen`.
|
||||||
|
The `check` subcommand stays typecheck-only (no lift needed
|
||||||
|
for type-checking). Codegen's internal `desugar_module`
|
||||||
|
call is harmless on a lifted module — no `Term::LetRec`
|
||||||
|
remains, so the LetRec arm is never invoked.
|
||||||
|
- `examples/local_rec_let_capture.ailx` + `.ail.json` (48 LOC
|
||||||
|
source). `count_below(n)` returns how many `i` in `1..=n` are
|
||||||
|
strictly less than a `let threshold = (app + 5 5)` computed
|
||||||
|
inside the enclosing fn. The recursive helper `loop` captures
|
||||||
|
`threshold` (Let-bound; type unknown until typecheck) and
|
||||||
|
recurses on its own counter `i`. The lift produces
|
||||||
|
`loop$lr_0(i: Int, n: Int, threshold: Int) -> Int` and
|
||||||
|
rewrites every `(app loop X)` to `(app loop$lr_0 X n threshold)`.
|
||||||
|
The let-value is `(app + 5 5)` rather than a literal so the
|
||||||
|
type-synthesis path is exercised. Drives at 0, 5, 15 →
|
||||||
|
output `0\n5\n9\n`.
|
||||||
|
- `crates/ail/tests/e2e.rs::local_rec_let_capture_demo` (+18):
|
||||||
|
e2e count 39 → 40.
|
||||||
|
- `crates/ailang-core/src/desugar.rs::tests`. The 16b.2-era
|
||||||
|
`let_rec_capture_let_binding_panics` test was repurposed
|
||||||
|
into a positive test
|
||||||
|
`let_rec_capture_let_binding_is_deferred_to_post_typecheck`
|
||||||
|
that asserts the desugar leaves the LetRec in place (no
|
||||||
|
lifted fn appended; original LetRec still reachable). Net
|
||||||
|
test count unchanged.
|
||||||
|
- `crates/ailang-check/src/lib.rs::tests` (24 → 27, +3):
|
||||||
|
`letrec_with_let_binding_capture_typechecks` (positive),
|
||||||
|
`letrec_body_wrong_return_type_is_rejected` (negative — body
|
||||||
|
returns Bool but declared Int), and
|
||||||
|
`lift_letrecs_on_let_capture_produces_synthetic_fn` (asserts
|
||||||
|
synthetic FnDef added with augmented signature and call sites
|
||||||
|
rewritten).
|
||||||
|
- `docs/DESIGN.md` (+12 LOC). Pipeline section gains the
|
||||||
|
`lift_letrecs` stage between `check` and `codegen`, with a
|
||||||
|
paragraph clarifying it runs only on the `build` / `run`
|
||||||
|
paths and that synthetic FnDefs do not appear in
|
||||||
|
`CheckedModule.symbols`.
|
||||||
|
|
||||||
|
**The deferral mechanism.** Given
|
||||||
|
`(let y (app + 5 5) (let-rec helper (params x) (type Fn(Int) -> Int)
|
||||||
|
(body (app + x y)) (in (app helper 1))))`
|
||||||
|
inside an enclosing fn:
|
||||||
|
|
||||||
|
- Desugar runs. The LetRec's outer scope is `{n: Int (fn-param),
|
||||||
|
y: LetBound}`. Free vars of `body` minus `{name, params}` =
|
||||||
|
`{+, y}`; intersection with scope = `{y}`. `y` is `LetBound`,
|
||||||
|
so the all-or-nothing classifier sees `has_let_bound = true`
|
||||||
|
and reconstructs the LetRec with desugared sub-terms instead
|
||||||
|
of lifting.
|
||||||
|
- Typecheck runs. The new `Term::LetRec` arm in `synth` extends
|
||||||
|
locals with `helper: Fn(Int) -> Int` and `x: Int`, synths the
|
||||||
|
body to `Int`, unifies with `ret_ty`, and accepts.
|
||||||
|
- `lift_letrecs` runs. Walking outer's body, it threads locals.
|
||||||
|
At the `Term::Let`, `synth_type` resolves `(app + 5 5) → Int`
|
||||||
|
and inserts `y: Int` into locals. At the `Term::LetRec`, the
|
||||||
|
capture analyser collects `{y}` and reads its type as `Int`.
|
||||||
|
Builds `helper$lr_0(x: Int, y: Int) -> Int`, rewrites
|
||||||
|
`(app helper 1)` → `(app helper$lr_0 1 y)`, appends the
|
||||||
|
lifted `FnDef`. The Term::LetRec node is replaced by its
|
||||||
|
rewritten in-clause.
|
||||||
|
- Codegen runs on the lifted module. No `Term::LetRec` remains;
|
||||||
|
the four `unreachable!("Term::LetRec eliminated by desugar")`
|
||||||
|
arms in codegen stay valid (the message is a slight
|
||||||
|
misnomer post-16b.3 — by the time codegen runs, every LetRec
|
||||||
|
has been eliminated by desugar OR lift_letrecs — but the
|
||||||
|
invariant holds).
|
||||||
|
|
||||||
|
**Tests: 106 → 110 (+4).**
|
||||||
|
|
||||||
|
- e2e: 39 → 40 (`local_rec_let_capture_demo`).
|
||||||
|
- `ailang-check::tests`: 24 → 27 (the three new LetRec tests).
|
||||||
|
- `ailang-core::desugar::tests`: 9 → 9 (one `#[should_panic]`
|
||||||
|
test repurposed to a positive defer-check test — net 0).
|
||||||
|
|
||||||
|
**Cumulative state, post-16b.3.**
|
||||||
|
|
||||||
|
- Stdlib unchanged (5 modules, 29 combinators).
|
||||||
|
- `Term` enum: 11 variants (unchanged). All pre-16b.3 fixtures
|
||||||
|
hash bit-identically — additive at the typechecker / new-pass
|
||||||
|
level only.
|
||||||
|
- Compiler stages: load → desugar → typecheck →
|
||||||
|
**lift_letrecs (16b.3)** → codegen. The `check` subcommand
|
||||||
|
stops at typecheck and skips the lift; `build` / `run` go all
|
||||||
|
the way through.
|
||||||
|
- The four `unreachable!("Term::LetRec eliminated by desugar")`
|
||||||
|
arms in `ailang-codegen` remain correct: by the time codegen
|
||||||
|
runs, no LetRec survives — desugar lifts the 16b.1/16b.2
|
||||||
|
cases, lift_letrecs catches the 16b.3 case. The two
|
||||||
|
`unreachable!` arms in `ailang-check` were replaced with the
|
||||||
|
new typing rule (`verify_tail_positions` and `synth`).
|
||||||
|
- Compiler bugs surfaced and fixed in dogfood since 14a: 5/5
|
||||||
|
(unchanged).
|
||||||
|
|
||||||
|
**Queue update post-16b.3.** 16b.3 done. Open: **16b.4** (LetRec
|
||||||
|
captures of match-arm pattern bindings — needs ADT-def lookup +
|
||||||
|
constructor-field substitution; would slot into the same
|
||||||
|
`lift_letrecs` pass with extended pattern-arm walk), **16b.5**
|
||||||
|
(closure conversion — lifts the "name-as-value-only" restriction
|
||||||
|
for both LetRec and Lam; out of scope for this iter line),
|
||||||
|
**16b.6** (LetRec inside a `Type::Forall`-quantified fn — needs
|
||||||
|
synthesised `Forall` for the lifted signature), **16b.7**
|
||||||
|
(nested LetRec mutual capture — generalised lifting that
|
||||||
|
accumulates captures across nesting; partly already handled by
|
||||||
|
the post-order traversal in `lift_letrecs` but the mutual case
|
||||||
|
is genuinely harder). 16d (chain-machinery exhaustiveness or
|
||||||
|
`__unreachable__`), 16e (`==` extension to Bool/Str/Unit),
|
||||||
|
17a (per-fn arena, gated) unchanged.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
{"defs":[{"body":{"body":{"body":{"cond":{"args":[{"name":"i","t":"var"},{"name":"n","t":"var"}],"fn":{"name":">","t":"var"},"t":"app"},"else":{"cond":{"args":[{"name":"i","t":"var"},{"name":"threshold","t":"var"}],"fn":{"name":"<","t":"var"},"t":"app"},"else":{"args":[{"args":[{"name":"i","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"loop","t":"var"},"t":"app"},"t":"if","then":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"},{"args":[{"args":[{"name":"i","t":"var"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}],"fn":{"name":"loop","t":"var"},"t":"app"}],"fn":{"name":"+","t":"var"},"t":"app"}},"t":"if","then":{"lit":{"kind":"int","value":0},"t":"lit"}},"in":{"args":[{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"loop","t":"var"},"t":"app"},"name":"loop","params":["i"],"t":"letrec","type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},"name":"threshold","t":"let","value":{"args":[{"lit":{"kind":"int","value":5},"t":"lit"},{"lit":{"kind":"int","value":5},"t":"lit"}],"fn":{"name":"+","t":"var"},"t":"app"}},"doc":"Count i in 1..=n with i < threshold, where threshold = 5+5.","kind":"fn","name":"count_below","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"count_below","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"lhs":{"args":[{"args":[{"lit":{"kind":"int","value":5},"t":"lit"}],"fn":{"name":"count_below","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"args":[{"lit":{"kind":"int","value":15},"t":"lit"}],"fn":{"name":"count_below","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"t":"seq"},"t":"seq"},"doc":"Drive count_below at 0, 5, 15. Expected (per line): 0, 5, 9.","kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"local_rec_let_capture","schema":"ailang/v0"}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
; Iter 16b.3 — LetRec captures of `Term::Let`-bound names.
|
||||||
|
; `count_below(n)` returns how many integers in 1..=n are strictly
|
||||||
|
; less than a `threshold` computed locally from `n`. The recursive
|
||||||
|
; helper `loop` captures the let-bound name `threshold` from the
|
||||||
|
; enclosing scope; its type is only known after typecheck (the
|
||||||
|
; let-value is `(app + 5 5)` — an `Int` expression, but the desugar
|
||||||
|
; pass cannot resolve that without inference).
|
||||||
|
;
|
||||||
|
; The 16b.2 desugar pass cannot lift this LetRec (capture is Let-bound,
|
||||||
|
; not fn-param). Instead the desugar pass leaves the LetRec in place;
|
||||||
|
; the post-typecheck `lift_letrecs` pass in `ailang-check` resolves
|
||||||
|
; `threshold`'s type as `Int` from the typechecker's env and produces
|
||||||
|
; a synthetic top-level fn `loop$lr_0(i: Int, n: Int, threshold: Int)
|
||||||
|
; -> Int`, rewriting every `(app loop ARGS)` to `(app loop$lr_0 ARGS n
|
||||||
|
; threshold)`.
|
||||||
|
;
|
||||||
|
; Expected stdout (one per line):
|
||||||
|
; count_below(0) = 0 (empty range; loop's first call has i > n)
|
||||||
|
; count_below(5) = 5 (1..=5; threshold = 10; all 5 are < 10)
|
||||||
|
; count_below(15) = 9 (1..=15; threshold = 10; 1..=9 are < 10)
|
||||||
|
|
||||||
|
(module local_rec_let_capture
|
||||||
|
|
||||||
|
(fn count_below
|
||||||
|
(doc "Count i in 1..=n with i < threshold, where threshold = 5+5.")
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
|
(params n)
|
||||||
|
(body
|
||||||
|
(let threshold (app + 5 5)
|
||||||
|
(let-rec loop
|
||||||
|
(params i)
|
||||||
|
(type (fn-type (params (con Int)) (ret (con Int))))
|
||||||
|
(body
|
||||||
|
(if (app > i n)
|
||||||
|
0
|
||||||
|
(if (app < i threshold)
|
||||||
|
(app + 1 (app loop (app + i 1)))
|
||||||
|
(app loop (app + i 1)))))
|
||||||
|
(in (app loop 1))))))
|
||||||
|
|
||||||
|
(fn main
|
||||||
|
(doc "Drive count_below at 0, 5, 15. Expected (per line): 0, 5, 9.")
|
||||||
|
(type (fn-type (params) (ret (con Unit)) (effects IO)))
|
||||||
|
(params)
|
||||||
|
(body
|
||||||
|
(seq (do io/print_int (app count_below 0))
|
||||||
|
(seq (do io/print_int (app count_below 5))
|
||||||
|
(do io/print_int (app count_below 15)))))))
|
||||||
Reference in New Issue
Block a user