Iter 8b: lambdas with capture (Term::Lam + closure conversion)
Adds anonymous functions to AILang. A lambda value is constructed by
malloc'ing an env struct that holds its captured locals, plus a
closure pair `{ thunk_ptr, env_ptr }` (Iter 8a's ABI). The lambda's
body lifts to a top-level thunk `@ail_<m>_<def>_lam<id>(ptr %env,
params...)` that unpacks captures back into named locals before
running.
AST: new Term::Lam { params, paramTypes, retType, effects, body }.
Serde tag is "lam"; existing modules (no Lam) serialize identically,
so all current hashes stay stable.
Pretty-printer: renders `(\\ (x: T ...) -> R . body)`.
Typecheck (ailang-check): a lambda's type is the declared Type::Fn.
Body's effect set must be a subset of declared effects (no row
polymorphism). Constructing a lambda is pure — only calling it picks
up the declared effects, via the existing App branch.
Codegen (ailang-codegen):
- collect_captures: free-var walk; excludes builtins, current-module
top-level fns, and qualified names.
- lower_lambda: per-fn lam counter, save/restore emitter state, emit
thunk into a deferred queue (flushed after the parent fn), pack env
+ closure pair on heap, register the resulting closure-pair SSA in
the sidetable so subsequent indirect calls find it.
- Capture sigs propagate: a fn-typed capture keeps its FnSig in the
thunk's sidetable so `f(args)` inside the body still indirect-calls.
- Env layout: 8 bytes per capture (typed load/store reads only the
needed bytes; padding wasted but uniform).
Tests: 49 green (was 48). New examples/closure.ail.json + e2e
`closure_captures_let_n` exercises `let n = 3 in apply(\\x. x + n, 39)`
end-to-end and asserts the binary prints 42.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -797,6 +797,48 @@ fn synth(
|
||||
|
||||
Ok(result_ty.expect("checked arms is non-empty"))
|
||||
}
|
||||
Term::Lam { params, param_tys, ret_ty, effects: lam_effects, body } => {
|
||||
// Iter 8b: a lambda's type is the declared `Type::Fn`. Push
|
||||
// params as locals, check the body's type matches `ret_ty`,
|
||||
// and ensure body effects are a subset of the declared set.
|
||||
// (We trust the JSON schema for params.len() == param_tys.len();
|
||||
// serde would have rejected a malformed AST.)
|
||||
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 mut body_effects: BTreeSet<String> = BTreeSet::new();
|
||||
let body_ty = synth(body, env, locals, &mut body_effects, in_def);
|
||||
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?;
|
||||
expect_eq(ret_ty, &body_ty)?;
|
||||
// Constructing a lambda is pure — the body's effects are
|
||||
// sealed into the lambda's type, not propagated to the
|
||||
// outer effect set. Calling the lambda (Term::App) will
|
||||
// pick those effects up via Type::Fn.effects.
|
||||
// Body effects must be a subset of the declared lam_effects.
|
||||
let declared: BTreeSet<String> = lam_effects.iter().cloned().collect();
|
||||
for e in &body_effects {
|
||||
if !declared.contains(e) {
|
||||
return Err(CheckError::UndeclaredEffect(e.clone()));
|
||||
}
|
||||
}
|
||||
Ok(Type::Fn {
|
||||
params: param_tys.clone(),
|
||||
ret: Box::new((**ret_ty).clone()),
|
||||
effects: lam_effects.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user