Iter 8a: closure-pair ABI for fn-values
Every fn-value is now a `ptr` to a closure pair `{ ptr thunk, ptr env }`,
not a raw fn-pointer. Top-level fns get an auto-generated adapter
`@ail_<m>_<f>_adapter(ptr %_env, params...)` that ignores env and
forwards to the real fn, plus a static closure constant
`@ail_<m>_<f>_clos = { @adapter, null }`. References to a fn as a
value return the closure-pair address.
Indirect calls now GEP the thunk + env slots, load both, and call
`thunk(env, args...)`. Direct calls to statically-known callees stay
on the original fast path (no adapter).
This is the ABI groundwork for Iter 8b lambdas: a captured-env closure
will reuse the same value shape, just with a non-null env produced by
malloc.
Tests: 48 still green. IR snapshots refreshed (every fn now carries an
adapter + static closure pair). Iter 7's hof.ail.json prints 42
unchanged — `apply(inc, 41)` now hands `@ail_hof_inc_clos` to apply,
which unpacks and indirect-calls as designed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -428,9 +428,58 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
self.body
|
||||
.push_str(&format!(" ret {val_ty} {val}\n}}\n\n"));
|
||||
|
||||
// Iter 8a: emit closure-pair scaffold (adapter + static closure)
|
||||
// for this fn. The adapter takes an extra `ptr %_env` (ignored,
|
||||
// null sentinel for top-level fns) and forwards to the real fn.
|
||||
// The static closure pair `{ adapter_ptr, null }` is the value
|
||||
// produced when this fn is referenced as a `Term::Var` value
|
||||
// (closure-pair pointer ABI).
|
||||
self.emit_adapter_and_static_closure(&f.name, &llvm_param_tys, &llvm_ret);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Iter 8a: closure-pair scaffold for a top-level fn. Always emitted
|
||||
/// (one wrapper per fn), so cross-module references just use the
|
||||
/// `<m>_<f>_clos` symbol without coordination.
|
||||
fn emit_adapter_and_static_closure(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
param_tys: &[String],
|
||||
ret_ty: &str,
|
||||
) {
|
||||
let m = self.module_name;
|
||||
// Adapter: `(ptr %_env, params...) -> ret` calls the real fn,
|
||||
// returning whatever it returned.
|
||||
let mut adapter = format!(
|
||||
"define {ret} @ail_{m}_{fn_name}_adapter(ptr %_env",
|
||||
ret = ret_ty,
|
||||
);
|
||||
for (i, pty) in param_tys.iter().enumerate() {
|
||||
adapter.push_str(&format!(", {pty} %a{i}"));
|
||||
}
|
||||
adapter.push_str(") {\nentry:\n");
|
||||
let mut call_args = String::new();
|
||||
for (i, pty) in param_tys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
call_args.push_str(", ");
|
||||
}
|
||||
call_args.push_str(&format!("{pty} %a{i}"));
|
||||
}
|
||||
adapter.push_str(&format!(
|
||||
" %r = call {ret} @ail_{m}_{fn_name}({call_args})\n",
|
||||
ret = ret_ty,
|
||||
));
|
||||
adapter.push_str(&format!(" ret {ret} %r\n}}\n\n", ret = ret_ty));
|
||||
self.body.push_str(&adapter);
|
||||
|
||||
// Static closure pair: `{ adapter_ptr, null }`. The address of
|
||||
// this global IS the fn-value that escapes to other code.
|
||||
self.header.push_str(&format!(
|
||||
"@ail_{m}_{fn_name}_clos = private unnamed_addr constant {{ ptr, ptr }} {{ ptr @ail_{m}_{fn_name}_adapter, ptr null }}\n"
|
||||
));
|
||||
}
|
||||
|
||||
/// Lowers a term to (SSA value string, LLVM type).
|
||||
fn lower_term(&mut self, t: &Term) -> Result<(String, String)> {
|
||||
match t {
|
||||
@@ -894,8 +943,11 @@ impl<'a> Emitter<'a> {
|
||||
Ok((dst, sig.ret.clone()))
|
||||
}
|
||||
|
||||
/// Iter 7: indirect call through a fn-pointer SSA value. Sig must
|
||||
/// already be known (sidetable lookup happened in the caller).
|
||||
/// Iter 8a: indirect call through a closure-pair pointer. The
|
||||
/// callee SSA points at `{ ptr thunk, ptr env }`; we GEP+load both
|
||||
/// halves and call `thunk(env, args...)`. The user-visible `sig`
|
||||
/// describes only the user-level params/ret — the env_ptr is
|
||||
/// inserted by codegen, transparent to the source language.
|
||||
fn emit_indirect_call(
|
||||
&mut self,
|
||||
callee_ssa: &str,
|
||||
@@ -919,16 +971,38 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
compiled.push((v, vty));
|
||||
}
|
||||
let arglist = compiled
|
||||
.iter()
|
||||
.map(|(v, t)| format!("{t} {v}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let dst = self.fresh_ssa();
|
||||
// LLVM indirect-call form: `call <ret>(<param-tys>) <ptr>(<args>)`.
|
||||
let param_tys = sig.params.join(", ");
|
||||
// Unpack the closure pair: thunk pointer at offset 0, env pointer
|
||||
// at offset 8. Use a typed GEP through `{ ptr, ptr }` so the
|
||||
// offsets are computed correctly across targets.
|
||||
let thunk_p = self.fresh_ssa();
|
||||
let thunk = self.fresh_ssa();
|
||||
let env_p = self.fresh_ssa();
|
||||
let env = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call {ret} ({ptys}) {callee_ssa}({arglist})\n",
|
||||
" {thunk_p} = getelementptr inbounds {{ ptr, ptr }}, ptr {callee_ssa}, i64 0, i32 0\n"
|
||||
));
|
||||
self.body
|
||||
.push_str(&format!(" {thunk} = load ptr, ptr {thunk_p}\n"));
|
||||
self.body.push_str(&format!(
|
||||
" {env_p} = getelementptr inbounds {{ ptr, ptr }}, ptr {callee_ssa}, i64 0, i32 1\n"
|
||||
));
|
||||
self.body
|
||||
.push_str(&format!(" {env} = load ptr, ptr {env_p}\n"));
|
||||
|
||||
// Build the actual call. The thunk's signature is `(ptr, params...)`
|
||||
// — env_ptr is the implicit first arg, transparent to the user.
|
||||
let mut arglist = format!("ptr {env}");
|
||||
for (v, t) in &compiled {
|
||||
arglist.push_str(&format!(", {t} {v}"));
|
||||
}
|
||||
let mut param_tys = String::from("ptr");
|
||||
for pt in &sig.params {
|
||||
param_tys.push_str(", ");
|
||||
param_tys.push_str(pt);
|
||||
}
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = call {ret} ({ptys}) {thunk}({arglist})\n",
|
||||
ret = sig.ret,
|
||||
ptys = param_tys,
|
||||
));
|
||||
@@ -951,23 +1025,28 @@ impl<'a> Emitter<'a> {
|
||||
.is_some_and(|m| m.contains_key(name))
|
||||
}
|
||||
|
||||
/// Iter 7: resolve `name` to a top-level fn-pointer (`@ail_<m>_<def>`)
|
||||
/// + its sig, or return None. Mirrors the dispatch in `lower_app` for
|
||||
/// fn refs only — operators / `not` are not first-class values, so
|
||||
/// `is_static_callee` excludes them here.
|
||||
/// Iter 8a: resolve `name` to a top-level fn-value, i.e. the address
|
||||
/// of its static closure pair `@ail_<m>_<def>_clos`, plus the user-
|
||||
/// visible FnSig (params/ret WITHOUT the env_ptr — that's added at
|
||||
/// the call site by the closure ABI). Returns None if the name does
|
||||
/// not refer to a top-level fn. Operators / `not` are not first-
|
||||
/// class values; `is_static_callee` filters them earlier.
|
||||
fn resolve_top_level_fn(&self, name: &str) -> Option<(String, FnSig)> {
|
||||
if name.matches('.').count() == 1 {
|
||||
let (prefix, suffix) = name.split_once('.')?;
|
||||
let target = self.import_map.get(prefix)?;
|
||||
let sig = self.module_user_fns.get(target)?.get(suffix)?.clone();
|
||||
return Some((format!("@ail_{target}_{suffix}"), sig));
|
||||
return Some((format!("@ail_{target}_{suffix}_clos"), sig));
|
||||
}
|
||||
let sig = self
|
||||
.module_user_fns
|
||||
.get(self.module_name)?
|
||||
.get(name)?
|
||||
.clone();
|
||||
Some((format!("@ail_{module}_{name}", module = self.module_name), sig))
|
||||
Some((
|
||||
format!("@ail_{module}_{name}_clos", module = self.module_name),
|
||||
sig,
|
||||
))
|
||||
}
|
||||
|
||||
fn lower_effect_op(&mut self, op: &str, args: &[Term]) -> Result<(String, String)> {
|
||||
|
||||
Reference in New Issue
Block a user