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:
@@ -128,6 +128,20 @@ pub enum Term {
|
||||
scrutinee: Box<Term>,
|
||||
arms: Vec<Arm>,
|
||||
},
|
||||
/// Anonymous function (Iter 8b). Captures any free variables of
|
||||
/// `body` from the enclosing scope. Param/return types are
|
||||
/// declared inline so the typechecker stays HM-monomorphic on
|
||||
/// the inferred shape.
|
||||
Lam {
|
||||
params: Vec<String>,
|
||||
#[serde(rename = "paramTypes")]
|
||||
param_tys: Vec<Type>,
|
||||
#[serde(rename = "retType")]
|
||||
ret_ty: Box<Type>,
|
||||
#[serde(default)]
|
||||
effects: Vec<String>,
|
||||
body: Box<Term>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -198,6 +198,28 @@ fn term_block(t: &Term, indent: usize) -> String {
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
Term::Lam { params, param_tys, ret_ty, effects, body } => {
|
||||
// (\ [params] :: typed-sig . body)
|
||||
let typed_params: Vec<String> = params
|
||||
.iter()
|
||||
.zip(param_tys.iter())
|
||||
.map(|(n, t)| format!("{n}: {}", type_to_string(t)))
|
||||
.collect();
|
||||
let eff = if effects.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" !{}", effects.join(","))
|
||||
};
|
||||
let mut s = format!(
|
||||
"{pad}(\\ ({}) -> {}{}\n",
|
||||
typed_params.join(" "),
|
||||
type_to_string(ret_ty),
|
||||
eff,
|
||||
);
|
||||
s.push_str(&term_block(body, indent + 2));
|
||||
s.push(')');
|
||||
s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +295,9 @@ fn term_inline(t: &Term) -> String {
|
||||
term_inline(else_)
|
||||
)
|
||||
}
|
||||
Term::Lam { params, .. } => {
|
||||
format!("(\\ {} ...)", params.join(" "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user