From 7cba33ea6dd823a80018bcec912cca1ab72f0e1c Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 19 Jun 2026 07:20:42 +0300 Subject: [PATCH] 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. --- current/CHECKPOINT-COMPILER-API.md | 38 ++++++++++++++++--- current/PLAN-COMPILER-VM.md | 10 +++-- examples/1661-platform-post-link-vm-list.sx | 33 ++++++++++++++++ .../1661-platform-post-link-vm-list.build | 1 + .../1661-platform-post-link-vm-list.exit | 1 + .../1661-platform-post-link-vm-list.stderr | 1 + .../1661-platform-post-link-vm-list.stdout | 1 + src/core.zig | 37 ++++++++---------- src/main.zig | 7 ++++ 9 files changed, 99 insertions(+), 30 deletions(-) create mode 100644 examples/1661-platform-post-link-vm-list.sx create mode 100644 examples/expected/1661-platform-post-link-vm-list.build create mode 100644 examples/expected/1661-platform-post-link-vm-list.exit create mode 100644 examples/expected/1661-platform-post-link-vm-list.stderr create mode 100644 examples/expected/1661-platform-post-link-vm-list.stdout diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index a51b6e9f..a2df98b0 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -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 > 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` -> slot (stdlib `default_build`, user override `#run on_build = build;`). **NEXT — P5.1 (= 4E):** route the -> post-codegen / `on_build` invocation through the VM (`core.invokeByFuncId` → VM) — REQUIRED (the sx driver -> allocates `List`s; legacy interp can't — 0141, verified) — + dedicated bundle smoke tests. Then P5.2 -> (primitives) · P5.3 (`on_build` slot) · P5.4 (sx `default_build` + delete `#compiler`/`compiler_call`/ -> `compiler_hooks` + the S5a `build_options`/`set_post_link_callback`). +> slot (stdlib `default_build`, user override `#run on_build = build;`). **P5.1 (= 4E) DONE (2026-06-19, newest +> Log entry):** `core.invokeByFuncId` (the post-link build-driver invocation) now runs the callback on the VM +> with **NO fallback** (a side-effecting callback can't double-execute); `BuildConfig` + `import_sources` threaded +> in; VM bail → hard build error (`comptime_vm.last_bail_reason` surfaced by `main.printInterpBailDiag`). Smoke +> 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` + > 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). @@ -396,6 +403,27 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## 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).** 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 diff --git a/current/PLAN-COMPILER-VM.md b/current/PLAN-COMPILER-VM.md index 51658787..05b8061a 100644 --- a/current/PLAN-COMPILER-VM.md +++ b/current/PLAN-COMPILER-VM.md @@ -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. **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` - → VM). REQUIRED because the driver allocates (`List`) and the legacy interp can't (0141 — verified: comptime - `List` growth works on the VM, fails on legacy). Add dedicated bundle smoke tests (no corpus coverage today). +- **P5.1 — 4E prereq — DONE (2026-06-19).** `core.invokeByFuncId` routes the post-link callback through the + **VM** (`comptime_vm.tryEval`), NO fallback (a side-effecting callback can't double-execute): a bail is a hard + 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 (`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`: diff --git a/examples/1661-platform-post-link-vm-list.sx b/examples/1661-platform-post-link-vm-list.sx new file mode 100644 index 00000000..c70b8f13 --- /dev/null +++ b/examples/1661-platform-post-link-vm-list.sx @@ -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"); } diff --git a/examples/expected/1661-platform-post-link-vm-list.build b/examples/expected/1661-platform-post-link-vm-list.build new file mode 100644 index 00000000..40462038 --- /dev/null +++ b/examples/expected/1661-platform-post-link-vm-list.build @@ -0,0 +1 @@ +{ "aot": true } diff --git a/examples/expected/1661-platform-post-link-vm-list.exit b/examples/expected/1661-platform-post-link-vm-list.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/1661-platform-post-link-vm-list.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/1661-platform-post-link-vm-list.stderr b/examples/expected/1661-platform-post-link-vm-list.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/1661-platform-post-link-vm-list.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/1661-platform-post-link-vm-list.stdout b/examples/expected/1661-platform-post-link-vm-list.stdout new file mode 100644 index 00000000..6ce5eca1 --- /dev/null +++ b/examples/expected/1661-platform-post-link-vm-list.stdout @@ -0,0 +1 @@ +runtime main diff --git a/src/core.zig b/src/core.zig index 01827069..0bbba2b8 100644 --- a/src/core.zig +++ b/src/core.zig @@ -208,28 +208,21 @@ pub const Compilation = struct { /// invoke it later without name lookup. pub fn invokeByFuncId(self: *Compilation, id: ir.FuncId, args: []const ir.Value) !ir.Value { const mod = self.ir_module orelse return error.NoIRModule; - var interp = ir.Interpreter.init(mod, self.allocator); - defer interp.deinit(); - interp.setSourceMap(&self.import_sources); - if (self.ir_emitter) |*e| interp.build_config = &e.build_config; - ir.Interpreter.last_bail_op = null; - ir.Interpreter.last_bail_builtin = null; - ir.Interpreter.last_bail_detail = null; - const result = interp.call(id, args) catch |err| { - flushInterpOutput(interp.output.items); - return err; - }; - flushInterpOutput(interp.output.items); - return result; - } - - /// #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); + // The build driver (post-link callback) runs on the comptime VM — NOT + // the legacy interp. The driver allocates Lists, which the legacy interp + // cannot grow at comptime (issue 0141: `struct_get: base has no fields`); + // the VM can. There is **no fallback**: 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. 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 + error.ComptimeVmBail; } /// Get link flags accumulated from #run build blocks. diff --git a/src/main.zig b/src/main.zig index cc36d987..2ff381c0 100644 --- a/src/main.zig +++ b/src/main.zig @@ -462,6 +462,13 @@ fn deriveOutputName(input_path: []const u8) []const u8 { /// and the source location (line:col) when the interpreter captured them. fn printInterpBailDiag(comp: *const sx.core.Compilation, label: []const u8, err: anyerror) void { 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) }); return; };