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:
33
src/core.zig
33
src/core.zig
@@ -188,7 +188,7 @@ pub const Compilation = struct {
|
||||
/// if applicable) to invoke a named sx function. Used for the post-link
|
||||
/// bundling callback. Returns the function's return value, or null if the
|
||||
/// name doesn't resolve to a function in the lowered module.
|
||||
pub fn invokeByName(self: *Compilation, name: []const u8, args: []const ir.Value) !?ir.Value {
|
||||
pub fn invokeByName(self: *Compilation, name: []const u8, pass_options: bool) !?ir.Value {
|
||||
const mod = self.ir_module orelse return null;
|
||||
var found_id: ?ir.FuncId = null;
|
||||
for (mod.functions.items, 0..) |func, i| {
|
||||
@@ -199,14 +199,15 @@ pub const Compilation = struct {
|
||||
}
|
||||
}
|
||||
const fid = found_id orelse return null;
|
||||
return try self.invokeByFuncId(fid, args);
|
||||
return try self.invokeByFuncId(fid, pass_options);
|
||||
}
|
||||
|
||||
/// Re-enter the IR interpreter and call a previously-resolved function
|
||||
/// id. Companion to `invokeByName` — used when the FuncId was captured
|
||||
/// at `#run` time (e.g. by `set_post_link_callback`) and we want to
|
||||
/// invoke it later without name lookup.
|
||||
pub fn invokeByFuncId(self: *Compilation, id: ir.FuncId, args: []const ir.Value) !ir.Value {
|
||||
/// Re-enter the evaluator and call a previously-resolved function id. The
|
||||
/// post-link build callback, captured at `#run` time (by `on_build` /
|
||||
/// `set_post_link_callback`). `pass_options` passes the opaque `BuildOptions`
|
||||
/// handle as the callback's arg (the `on_build(cb)` form, `cb: (opt:
|
||||
/// BuildOptions) -> bool`); false for the legacy no-arg form.
|
||||
pub fn invokeByFuncId(self: *Compilation, id: ir.FuncId, pass_options: bool) !ir.Value {
|
||||
const mod = self.ir_module orelse return error.NoIRModule;
|
||||
// The build driver (post-link callback) runs on the comptime VM — NOT
|
||||
// the legacy interp. The driver allocates Lists, which the legacy interp
|
||||
@@ -215,13 +216,8 @@ pub const Compilation = struct {
|
||||
// callback can't safely re-run on a second evaluator (double execution),
|
||||
// so a VM bail is a hard build error. The bail reason is in
|
||||
// `comptime_vm.last_bail_reason` (surfaced by `main.printInterpBailDiag`).
|
||||
// Post-link callbacks are nullary today (the implicit `*Context` is
|
||||
// materialized by the VM's `runEntry`); a non-empty `args` would need a
|
||||
// VM entry that marshals them, which arrives with the `on_build(config)`
|
||||
// slot (Phase 5.3) — reject it loudly rather than silently drop.
|
||||
if (args.len != 0) return error.ComptimeVmArgsUnsupported;
|
||||
const build_config = if (self.ir_emitter) |*e| &e.build_config else null;
|
||||
return ir.comptime_vm.tryEval(self.allocator, mod, id, build_config, &self.import_sources) orelse
|
||||
return ir.comptime_vm.runBuildCallback(self.allocator, mod, id, build_config, &self.import_sources, pass_options) orelse
|
||||
error.ComptimeVmBail;
|
||||
}
|
||||
|
||||
@@ -249,13 +245,20 @@ pub const Compilation = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get the post-link callback function id (set via
|
||||
/// `BuildOptions.set_post_link_callback(fn)`), if any.
|
||||
/// Get the post-link callback function id (set via `on_build(fn)` or the
|
||||
/// legacy `set_post_link_callback(fn)`), if any.
|
||||
pub fn getPostLinkCallback(self: *Compilation) ?ir.FuncId {
|
||||
if (self.ir_emitter) |*e| return e.build_config.post_link_callback_fn;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Whether the post-link callback takes the `BuildOptions` handle arg (the
|
||||
/// `on_build(cb)` form). Drives the `pass_options` flag at invocation.
|
||||
pub fn getPostLinkTakesOptions(self: *Compilation) bool {
|
||||
if (self.ir_emitter) |*e| return e.build_config.post_link_takes_options;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Get the post-link module name (set via
|
||||
/// `BuildOptions.set_post_link_module("name")`), if any.
|
||||
pub fn getPostLinkModule(self: *Compilation) ?[]const u8 {
|
||||
|
||||
@@ -33,6 +33,11 @@ pub const BuildConfig = struct {
|
||||
/// and invokes this function with no args. A `false` return is
|
||||
/// treated as a build failure.
|
||||
post_link_callback_fn: ?FuncId = null,
|
||||
/// True when the post-link callback was registered via `on_build(cb)` (the
|
||||
/// Phase 5 form, `cb: (opt: BuildOptions) -> bool`) rather than the legacy
|
||||
/// `set_post_link_callback(cb)` (`cb: () -> bool`). When set, the compiler
|
||||
/// invokes the callback with the opaque `BuildOptions` handle as its arg.
|
||||
post_link_takes_options: bool = false,
|
||||
/// Alternative to `post_link_callback_fn`: the qualified name of
|
||||
/// a module whose `bundle_main` function should be called
|
||||
/// post-link.
|
||||
|
||||
@@ -61,6 +61,7 @@ pub const bound_fns = [_]BoundFn{
|
||||
// ── BuildOptions (migrated off `#compiler` onto `abi(.compiler)`) ─────────
|
||||
.{ .sx_name = "build_options", .handler = handleBuildOptions },
|
||||
.{ .sx_name = "set_post_link_callback", .handler = handleSetPostLinkCallback },
|
||||
.{ .sx_name = "on_build", .handler = handleOnBuild },
|
||||
// ── build-pipeline metadata queries (Phase 5.2) ──────────────────────────
|
||||
// VM-only: the post-link callback that calls these always runs on the VM
|
||||
// (`core.invokeByFuncId`), so `comptime_vm.callCompilerFn` services them and
|
||||
@@ -353,3 +354,20 @@ fn handleSetPostLinkCallback(interp: *Interpreter, args: []const Value) InterpEr
|
||||
}
|
||||
return .void_val;
|
||||
}
|
||||
|
||||
/// `on_build(cb)` — register the build callback (the Phase 5 form, a free fn; cb
|
||||
/// is arg 0, and `cb: (opt: BuildOptions) -> bool` so the callback is invoked with
|
||||
/// the `BuildOptions` handle). Sets `post_link_takes_options` to distinguish it
|
||||
/// from the legacy `set_post_link_callback` (`() -> bool`).
|
||||
fn handleOnBuild(interp: *Interpreter, args: []const Value) InterpError!Value {
|
||||
if (args.len != 1) return error.TypeError;
|
||||
const bc = interp.build_config orelse return error.CannotEvalComptime;
|
||||
switch (args[0]) {
|
||||
.func_ref => |id| {
|
||||
bc.post_link_callback_fn = id;
|
||||
bc.post_link_takes_options = true;
|
||||
},
|
||||
else => return error.TypeError,
|
||||
}
|
||||
return .void_val;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -824,7 +824,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
// `set_post_link_module("name")`, re-enter the IR interpreter and
|
||||
// invoke that sx function now. A `false` return fails the build.
|
||||
if (comp.getPostLinkCallback()) |fid| {
|
||||
const ret = comp.invokeByFuncId(fid, &.{}) catch |err| {
|
||||
const ret = comp.invokeByFuncId(fid, comp.getPostLinkTakesOptions()) catch |err| {
|
||||
printInterpBailDiag(&comp, "post-link callback", err);
|
||||
return error.CompileError;
|
||||
};
|
||||
@@ -835,7 +835,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
} else if (comp.getPostLinkModule()) |mod_name| {
|
||||
const qualified = try std.fmt.allocPrint(allocator, "{s}.bundle_main", .{mod_name});
|
||||
defer allocator.free(qualified);
|
||||
const ret_opt = comp.invokeByName(qualified, &.{}) catch |err| {
|
||||
const ret_opt = comp.invokeByName(qualified, false) catch |err| {
|
||||
const label = try std.fmt.allocPrint(allocator, "post-link module '{s}.bundle_main'", .{mod_name});
|
||||
defer allocator.free(label);
|
||||
printInterpBailDiag(&comp, label, err);
|
||||
|
||||
Reference in New Issue
Block a user