P5.3: on_build(cb) build-callback registrar; callback takes BuildOptions

Per user design: on_build(build) is the build-callback registrar (a free fn),
generalizing set_post_link_callback — the callback is (opt: BuildOptions) ->
bool and the compiler invokes it post-codegen WITH the BuildOptions handle.

- VM: callCompilerFn 'on_build' arm + legacy handleOnBuild, both set
  post_link_callback_fn + a new BuildConfig.post_link_takes_options flag.
- comptime_vm: runEntry refactored to runEntryArgs(extra) (implicit ctx +
  explicit args); new public runBuildCallback(..., pass_options) passes the
  opaque BuildOptions handle (one word) after the ctx. The fat-config
  marshaling fear is moot — the handle is a single null-sentinel word.
- core.invokeByFuncId/invokeByName take pass_options (was an unused args
  slice); main.zig passes comp.getPostLinkTakesOptions().
- build.sx: on_build decl (set_post_link_callback kept for now).

Smoke test examples/1664-platform-on-build-callback (AOT): #run on_build(build)
with build :: (opt: BuildOptions) -> bool; the callback is invoked with the
handle arg (runEntryArgs param-count match) and runs the primitives.

Benign .ir churn (37 snapshots: type table +1 for the on_build fn type +
global renumber; behavior identical). 705/0 both gates.
This commit is contained in:
agra
2026-06-19 08:47:05 +03:00
parent d8affd45e8
commit 9cbee5e4bd
48 changed files with 35175 additions and 34932 deletions

View File

@@ -223,6 +223,37 @@ pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.
};
}
/// Run a post-link build callback on the VM (the post-codegen build driver — see
/// `core.invokeByFuncId`). Like `tryEval`, but for a callback that may take the
/// opaque `BuildOptions` handle as an explicit arg (the `on_build(cb)` form,
/// `cb: (opt: BuildOptions) -> bool`): when `pass_options` is set, the handle (a
/// null sentinel — the real state is the threaded `BuildConfig`) is passed after
/// the implicit ctx. Returns null on a bail (`last_bail_reason` names the cause).
pub fn runBuildCallback(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*interp_mod.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8), pass_options: bool) ?Value {
last_bail_reason = null;
const func = module.getFunction(func_id);
if (func.is_extern or func.blocks.items.len == 0) {
last_bail_reason = "extern / no body";
return null;
}
var vm = Vm.init(gpa);
defer vm.deinit();
vm.table = &module.types;
vm.module = module;
vm.build_config = build_config;
vm.source_map = source_map;
const extra: []const Reg = if (pass_options) &.{null_addr} else &.{};
const reg = vm.runEntryArgs(func_id, extra) catch |err| {
last_bail_reason = vm.detail orelse @errorName(err);
return null;
};
if (func.ret == .void or func.ret == .noreturn) return .void_val;
return vm.regToValue(gpa, &module.types, reg, func.ret) catch |err| {
last_bail_reason = vm.detail orelse @errorName(err);
return null;
};
}
// ── Executor ────────────────────────────────────────────────────────────────
//
// Walks the SAME SSA IR the legacy interpreter (`interp.zig`) walks, but over
@@ -327,18 +358,27 @@ pub const Vm = struct {
/// materialized ctx is zeroed; a body that ignores it runs, one that uses the
/// allocator hits unported `call_indirect` and bails.
fn runEntry(self: *Vm, func_id: FuncId) Error!Reg {
return self.runEntryArgs(func_id, &.{});
}
/// Run a comptime entry with the materialized implicit `*Context` (when the
/// function has one) PREPENDED to `extra` explicit arg words. A nullary
/// const-init / `#run` passes `extra = &.{}`; a post-link build callback of
/// the `on_build` form passes the opaque `BuildOptions` handle.
fn runEntryArgs(self: *Vm, func_id: FuncId, extra: []const Reg) Error!Reg {
const module = self.module orelse return self.failMsg("comptime VM: entry run needs a module");
const func = module.getFunction(func_id);
var argbuf: [1]Reg = undefined;
var args: []const Reg = &.{};
var argbuf: std.ArrayList(Reg) = .empty;
defer argbuf.deinit(self.gpa);
if (func.has_implicit_ctx) {
if (func.params.len != 1) return self.failMsg("comptime VM: has_implicit_ctx with non-ctx params");
argbuf[0] = try self.materializeDefaultContext(module);
args = argbuf[0..1];
argbuf.append(self.gpa, try self.materializeDefaultContext(module)) catch @panic("comptime VM: out of memory (entry args)");
}
for (extra) |a| argbuf.append(self.gpa, a) catch @panic("comptime VM: out of memory (entry args)");
if (argbuf.items.len != func.params.len)
return self.failMsg("comptime VM: entry arg count mismatch (ctx + explicit args vs params)");
self.call_stack.append(self.gpa, func_id) catch @panic("comptime VM: out of memory (call stack)");
defer _ = self.call_stack.pop();
return self.run(func, args);
return self.run(func, argbuf.items);
}
/// Materialize the default `Context` in flat memory and return its address —
@@ -1452,6 +1492,20 @@ pub const Vm = struct {
bc.post_link_callback_fn = fid;
return @as(Reg, null_addr);
}
// `on_build(cb)` — register the build callback (the Phase 5 form, `cb:
// (opt: BuildOptions) -> bool`). Like `set_post_link_callback` but a free
// fn (cb is arg 0, no self) and the callback receives the `BuildOptions`
// handle when invoked (the `post_link_takes_options` flag drives that).
if (std.mem.eql(u8, name, "on_build")) {
if (args.len != 1) return self.failMsg("comptime on_build: expected (cb)");
const bc = self.build_config orelse
return self.failMsg("comptime on_build: no build config threaded into the VM");
const fid = funcRefToId(frame.get(args[0].index())) orelse
return self.failMsg("comptime on_build: cb arg is not a function value");
bc.post_link_callback_fn = fid;
bc.post_link_takes_options = true;
return @as(Reg, null_addr);
}
// ── build-pipeline metadata queries (Phase 5.2) ─────────────────────
// Read-only: the compiler answers them from the `BuildConfig` `main.zig`
// forwards before the post-link callback runs. Each builds a fresh