P5.1: post-link build driver runs on the comptime VM (no fallback)

core.invokeByFuncId routes the post-link callback through comptime_vm.tryEval
instead of the legacy Interpreter. REQUIRED because the sx build driver
allocates/grows Lists, which the legacy interp can't do at comptime (issue
0141: struct_get: base has no fields); the VM can. No fallback (a
side-effecting post-link callback can't double-execute): a VM bail is a hard
build error (comptime_vm.last_bail_reason, surfaced by printInterpBailDiag).
BuildConfig + import_sources threaded in; non-empty args rejected loudly.
flushInterpOutput deleted (VM out writes direct via host-FFI).

Smoke test examples/1661-platform-post-link-vm-list (AOT): a post-link
callback grows a List to 3 + returns len==3, so the build succeeds (exit 0)
only via the VM. First corpus coverage of the post-link path.

702/0 both gates.
This commit is contained in:
agra
2026-06-19 07:20:42 +03:00
parent 2060373c16
commit 7cba33ea6d
9 changed files with 99 additions and 30 deletions

View File

@@ -80,11 +80,18 @@ with ONE welded mechanism. Branch: `reify` (off `master`). Update after every st
> (2026-06-18, user): the 37-hook BuildOptions port is DEAD — DRIVE THE BUILD PIPELINE FROM SX** (newest Log > (2026-06-18, user): the 37-hook BuildOptions port is DEAD — DRIVE THE BUILD PIPELINE FROM SX** (newest Log
> entry + `PLAN-COMPILER-VM.md` → Phase 5). `BuildConfig` becomes plain sx data; the compiler shrinks to a few > entry + `PLAN-COMPILER-VM.md` → Phase 5). `BuildConfig` becomes plain sx data; the compiler shrinks to a few
> `abi(.compiler)` primitives (`emit_object`/`link`/queries, explicit args, `-> !` not bool) + an `on_build` > `abi(.compiler)` primitives (`emit_object`/`link`/queries, explicit args, `-> !` not bool) + an `on_build`
> slot (stdlib `default_build`, user override `#run on_build = build;`). **NEXT — P5.1 (= 4E):** route the > slot (stdlib `default_build`, user override `#run on_build = build;`). **P5.1 (= 4E) DONE (2026-06-19, newest
> post-codegen / `on_build` invocation through the VM (`core.invokeByFuncId` → VM) — REQUIRED (the sx driver > Log entry):** `core.invokeByFuncId` (the post-link build-driver invocation) now runs the callback on the VM
> allocates `List`s; legacy interp can't — 0141, verified) — + dedicated bundle smoke tests. Then P5.2 > with **NO fallback** (a side-effecting callback can't double-execute); `BuildConfig` + `import_sources` threaded
> (primitives) · P5.3 (`on_build` slot) · P5.4 (sx `default_build` + delete `#compiler`/`compiler_call`/ > in; VM bail → hard build error (`comptime_vm.last_bail_reason` surfaced by `main.printInterpBailDiag`). Smoke
> `compiler_hooks` + the S5a `build_options`/`set_post_link_callback`). > test `1661-platform-post-link-vm-list` (AOT) — a post-link callback that GROWS a `List` (0141: works on VM,
> bails on legacy with `struct_get`); build succeeds (exit 0) only via the VM. `flushInterpOutput` deleted (VM
> writes `out` direct via host-FFI). **702/0 both gates.** **NEXT — P5.2:** expose `emit_object` + `link`
> (reuse `target.zig`) + metadata queries (`c_object_paths`/`link_libraries`/host-triple) as `abi(.compiler)`
> primitives taking EXPLICIT args. Then P5.3 (`on_build` slot — invoke WITH the `BuildConfig` arg; needs a VM
> entry that marshals args, the gap `invokeByFuncId` rejects today) · P5.4 (sx `default_build` + delete
> `#compiler`/`compiler_call`/`compiler_hooks` + the S5a `build_options`/`set_post_link_callback`) — P5.4 kills
> the 4 strict `compiler_call` bails (1609/1614/1615/1616).
> **FINAL atomic step (4F):** (`out` already done — VM-native via libc `write`) handle `interp_print_frames` + > **FINAL atomic step (4F):** (`out` already done — VM-native via libc `write`) handle `interp_print_frames` +
> flip strict-to-default (remove the fallback) + delete `interp.zig`/`Value` + re-express `define`/`make_enum`. > flip strict-to-default (remove the fallback) + delete `interp.zig`/`Value` + re-express `define`/`make_enum`.
> See `PLAN-COMPILER-VM.md` → Phase 4 for the full plan + top risks (bundler test coverage). > See `PLAN-COMPILER-VM.md` → Phase 4 for the full plan + top risks (bundler test coverage).
@@ -396,6 +403,27 @@ when reached (sentinels or accessor fns; see the design doc Risks).
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
## Log ## Log
- **P5.1 (= 4E) — the post-link build driver runs on the VM (NO fallback); smoke test 1661 (2026-06-19).**
Phase 5 step 1: `core.invokeByFuncId` — the post-codegen / post-link callback invocation `main.zig` fires after
`target.link` — now routes the callback through the **comptime VM** (`comptime_vm.tryEval`) instead of the
legacy `Interpreter`. **REQUIRED** because the sx build driver allocates/grows `List`s, which the legacy interp
can't do at comptime (issue 0141: `struct_get: base has no fields`); the VM can. **NO fallback** (user
directive): a side-effecting post-link callback can't safely re-run on a second evaluator (double execution),
so a VM bail is a HARD build error — `error.ComptimeVmBail`, with the reason in `comptime_vm.last_bail_reason`
(now surfaced by `main.printInterpBailDiag`, which previously only read the legacy interp's `last_bail_*`
statics). `BuildConfig` (`&emitter.build_config`) + `import_sources` are threaded into the VM call. Deleted the
now-dead `flushInterpOutput` (the VM writes `out` directly via host-FFI — no buffer to flush). Non-empty `args`
rejected loudly (`error.ComptimeVmArgsUnsupported`) — the `on_build(config)` arg-passing entry arrives in P5.3.
**Verification:** a probe with a List-growing post-link callback FAILS on the pre-change legacy path
(`sx build` exit 1, `OutOfBounds (op=struct_get)`) and SUCCEEDS after the change (exit 0). Formalized as
`examples/1661-platform-post-link-vm-list` (`{ "aot": true }`): the callback grows a `List` to 3 + returns
`len == 3`; the build links cleanly (exit 0) and the binary prints `runtime main`. AOT snapshots the binary's
streams (build stdout discarded), so the VM-success is pinned via exit 0 + `runtime main` — a legacy regression
would flip the build to exit 1 and mismatch. **No corpus example fires post-link** (none had AOT sidecars; the
platform examples register a callback at `#run` time but run JIT) — so `invokeByFuncId` was previously untested
by the corpus; 1661 is the first coverage. The 4 strict `compiler_call` bails (1609/1614/1615/1616) are
UNAFFECTED — they bail at `#run configure()` on still-`#compiler` accessors (`set_bundle_path` etc.), killed by
P5.4, not here. **702/0 both gates.**
- **4B (VM-native diagnostics) — the metatype negative tests (1179/1180) render proper diagnostics under strict; strict gap-bails now ONLY `compiler_call` (2026-06-19).** - **4B (VM-native diagnostics) — the metatype negative tests (1179/1180) render proper diagnostics under strict; strict gap-bails now ONLY `compiler_call` (2026-06-19).**
The legacy and VM both BAIL on a `define()` validation failure with an identical detail string; only the The legacy and VM both BAIL on a `define()` validation failure with an identical detail string; only the
host's STRICT rendering differed (generic "bailed on the VM (strict)" vs the proper "comptime type host's STRICT rendering differed (generic "bailed on the VM (strict)" vs the proper "comptime type

View File

@@ -497,9 +497,13 @@ The compiler's whole post-IR role: codegen → build the CLI-derived `BuildConfi
`on_build(config)` on the VM; a `raise` fails the build. Plain `sx run` fires none of it. `on_build(config)` on the VM; a `raise` fails the build. Plain `sx run` fires none of it.
**Steps (each its own green step; depends on 4E first):** **Steps (each its own green step; depends on 4E first):**
- **P5.1 — 4E prereq:** route the post-codegen / `on_build` invocation through the **VM** (`core.invokeByFuncId` - **P5.1 — 4E prereq — DONE (2026-06-19).** `core.invokeByFuncId` routes the post-link callback through the
→ VM). REQUIRED because the driver allocates (`List`) and the legacy interp can't (0141 — verified: comptime **VM** (`comptime_vm.tryEval`), NO fallback (a side-effecting callback can't double-execute): a bail is a hard
`List` growth works on the VM, fails on legacy). Add dedicated bundle smoke tests (no corpus coverage today). build error (`comptime_vm.last_bail_reason` surfaced by `main.printInterpBailDiag`). `BuildConfig` +
`import_sources` threaded in; `flushInterpOutput` deleted (VM `out` writes direct via host-FFI). Smoke test
`examples/1661-platform-post-link-vm-list` (AOT): a post-link callback GROWS a `List` (0141 — works on the VM,
bails on legacy with `struct_get`), so the build succeeds (exit 0) only via the VM. Non-empty callback `args`
rejected loudly (the `on_build(config)` arg-marshaling entry is P5.3). **702/0 both gates.**
- **P5.2 — primitives:** expose `emit_object` + `link` (reuse `target.zig` linker) + metadata queries - **P5.2 — primitives:** expose `emit_object` + `link` (reuse `target.zig` linker) + metadata queries
(`c_object_paths`/`link_libraries`/host-triple) as `abi(.compiler)` fns taking explicit args. (`c_object_paths`/`link_libraries`/host-triple) as `abi(.compiler)` fns taking explicit args.
- **P5.3 — `on_build` slot:** a comptime-assignable compiler slot (GENERALIZES today's `post_link_callback_fn`: - **P5.3 — `on_build` slot:** a comptime-assignable compiler slot (GENERALIZES today's `post_link_callback_fn`:

View File

@@ -0,0 +1,33 @@
#import "modules/std.sx";
#import "modules/build.sx";
// P5.1 smoke test — the post-link build driver runs on the comptime VM,
// not the legacy interpreter.
//
// The callback allocates and GROWS a `List` (three appends). Comptime List
// growth works only on the VM; the legacy interp fails it (issue 0141:
// `struct_get: base has no fields`). So this build SUCCEEDS only because
// `core.invokeByFuncId` routes the post-link callback through the VM.
//
// AOT note: the corpus snapshots an AOT example's *executed binary* streams,
// and the build step's own stdout is discarded — so the callback's success is
// observed via the build EXIT CODE. On the VM the callback returns true, the
// build links cleanly (exit 0), and the binary runs → "runtime main". If the
// driver regressed to the legacy interp the callback would bail, the build
// would exit non-zero, and this snapshot would fail.
post_link :: () -> bool abi(.compiler) {
names := List(string).{};
names.append("alpha");
names.append("beta");
names.append("gamma");
return names.len == 3;
}
configure :: () abi(.compiler) {
opts := build_options();
opts.set_post_link_callback(post_link);
}
#run configure();
main :: () { print("runtime main\n"); }

View File

@@ -0,0 +1 @@
{ "aot": true }

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
runtime main

View File

@@ -208,28 +208,21 @@ pub const Compilation = struct {
/// invoke it later without name lookup. /// invoke it later without name lookup.
pub fn invokeByFuncId(self: *Compilation, id: ir.FuncId, args: []const ir.Value) !ir.Value { pub fn invokeByFuncId(self: *Compilation, id: ir.FuncId, args: []const ir.Value) !ir.Value {
const mod = self.ir_module orelse return error.NoIRModule; const mod = self.ir_module orelse return error.NoIRModule;
var interp = ir.Interpreter.init(mod, self.allocator); // The build driver (post-link callback) runs on the comptime VM — NOT
defer interp.deinit(); // the legacy interp. The driver allocates Lists, which the legacy interp
interp.setSourceMap(&self.import_sources); // cannot grow at comptime (issue 0141: `struct_get: base has no fields`);
if (self.ir_emitter) |*e| interp.build_config = &e.build_config; // the VM can. There is **no fallback**: a side-effecting post-link
ir.Interpreter.last_bail_op = null; // callback can't safely re-run on a second evaluator (double execution),
ir.Interpreter.last_bail_builtin = null; // so a VM bail is a hard build error. The bail reason is in
ir.Interpreter.last_bail_detail = null; // `comptime_vm.last_bail_reason` (surfaced by `main.printInterpBailDiag`).
const result = interp.call(id, args) catch |err| { // Post-link callbacks are nullary today (the implicit `*Context` is
flushInterpOutput(interp.output.items); // materialized by the VM's `runEntry`); a non-empty `args` would need a
return err; // VM entry that marshals them, which arrives with the `on_build(config)`
}; // slot (Phase 5.3) — reject it loudly rather than silently drop.
flushInterpOutput(interp.output.items); if (args.len != 0) return error.ComptimeVmArgsUnsupported;
return result; 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
error.ComptimeVmBail;
/// #run / post-link callback `print` output lands here. Routes to
/// fd 1 (stdout) so it joins the JIT-executed runtime's output
/// stream — the user wrote `print(...)` in both call sites, so
/// the stream split is invisible to them. issue-0047.
fn flushInterpOutput(bytes: []const u8) void {
if (bytes.len == 0) return;
_ = std.c.write(1, bytes.ptr, bytes.len);
} }
/// Get link flags accumulated from #run build blocks. /// Get link flags accumulated from #run build blocks.

View File

@@ -462,6 +462,13 @@ fn deriveOutputName(input_path: []const u8) []const u8 {
/// and the source location (line:col) when the interpreter captured them. /// and the source location (line:col) when the interpreter captured them.
fn printInterpBailDiag(comp: *const sx.core.Compilation, label: []const u8, err: anyerror) void { fn printInterpBailDiag(comp: *const sx.core.Compilation, label: []const u8, err: anyerror) void {
const op = sx.ir.Interpreter.last_bail_op orelse { const op = sx.ir.Interpreter.last_bail_op orelse {
// The post-link build driver runs on the comptime VM (core.invokeByFuncId),
// so a bail there sets `comptime_vm.last_bail_reason`, not the legacy
// interp's statics. Surface that reason when present.
if (sx.ir.comptime_vm.last_bail_reason) |reason| {
std.debug.print("error: {s} failed: {s}: {s}\n", .{ label, @errorName(err), reason });
return;
}
std.debug.print("error: {s} failed: {s}\n", .{ label, @errorName(err) }); std.debug.print("error: {s} failed: {s}\n", .{ label, @errorName(err) });
return; return;
}; };