From 5d25e23143e5efdb4f4f32f8777b4402efc23e36 Mon Sep 17 00:00:00 2001 From: agra Date: Fri, 19 Jun 2026 16:44:52 +0300 Subject: [PATCH] P5.7 Step A: VM is the sole comptime evaluator at emit-time + type-fn sites (no fallback) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the comptime_flat/need_vm gate and the vm_result-orelse-legacy fallback from emit_llvm.zig (runComptimeSideEffects + emitGlobals const-init) and comptime.zig (runComptimeTypeFunc). The comptime VM now always runs; a bail is always a build-gating diagnostic, never a fallback. Delete the now-moot entryNeedsVm. runComptimeSideEffects drops the Interpreter entirely (VM writes #run output direct to fd 1); emitGlobals keeps a fresh interp_inst only as the valueToLLVMConst materialization context (the regToValue bridge, removed with interp.zig in a later step). #insert (evalComptimeString) still routes through the legacy interp — deferred until interp.zig deletion. Reconcile 1654: the comptime asm-global #run now reports the VM's clean dlsym bail instead of the legacy CannotEvalComptime wrapper (exit still 1). 501/501 unit + 706/0 corpus. --- current/CHECKPOINT-COMPILER-API.md | 32 ++++ .../1654-platform-asm-global-comptime-call.sx | 2 +- ...4-platform-asm-global-comptime-call.stderr | 2 +- src/ir/emit_llvm.zig | 152 +++--------------- src/ir/lower/comptime.zig | 75 ++------- 5 files changed, 73 insertions(+), 190 deletions(-) diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 23901787..5196c39e 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -431,6 +431,38 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **P5.7 Step A — the flip: VM is the SOLE comptime evaluator at the emit-time + type-fn sites; NO fallback + (2026-06-19).** Removed the `if (self.comptime_flat or need_vm)` gate + the `vm_result orelse fallback` + legacy-interp blocks from `emit_llvm.zig` (`runComptimeSideEffects` AND the const-init path in `emitGlobals`) + and from `comptime.zig` `runComptimeTypeFunc` (type-fn). The VM now ALWAYS runs; a bail is ALWAYS a + build-gating diagnostic (`comptime_vm.last_bail_reason`), never a fallback. Deleted `emit_llvm.entryNeedsVm` + (moot — every entry runs on the VM now). `runComptimeSideEffects` no longer creates an `Interpreter` at all + (VM writes `#run` `print` output direct to fd 1 via host-FFI); `emitGlobals` keeps a fresh `interp_inst` ONLY + as the helper context `valueToLLVMConst` uses to materialize the VM's result Value (it evaluates nothing) — + that's the `regToValue` bridge, removed in Step C with `interp.zig`. **`#insert` (`evalComptimeString`) still + uses the legacy interp** — intentionally deferred to Step C (interp.zig still exists); it only needs the + evaluator to bail without crashing (0737's real error is a lowering-time visibility diagnostic). The + `comptime_flat*` LLVMEmitter fields are now set-but-unused (harmless; cosmetic cleanup later). **Snapshot + reconcile:** only `1654` churned — the asm-global `#run` now reports the VM's clean `comptime init of + 'COMPUTED' failed: comptime extern call: symbol not found via dlsym …` instead of the legacy + `CannotEvalComptime (op=call: …)` wrapper (exit still 1); regenerated scoped via `-Dname`. `1179`/`1180` + unchanged (the VM-strict `comptime type construction failed:` wording already matched). **501/501 unit + + 706/0 corpus** (one gate now — `-Dcomptime-flat` is moot but still accepted). NEXT: Step B — delete the + `#compiler` attribute (parse+lower) + the `compiler_call` IR op + `compiler_hooks.zig`. +- **P5.7 Step 0 — strict sweep CLEAN; zero VM gaps to port (2026-06-19).** Gating prerequisite for deleting the + legacy fallback. Confirmed both gates 706/0 + 501/501 unit (gate-OFF and `-Dcomptime-flat`). Then ran every + corpus example (706) under `SX_COMPTIME_FLAT_STRICT=1` (VM, NO fallback) via `.sx-tmp/strict_sweep.sh` — + `sx run` (JIT), `sx build` (aot/bundle), or `sx ir --target` (cross-arch). Only **3** examples emit a VM-bail + signature, ALL expected-failures (strict exit == expected exit), NONE a real gap: **1179** + (`enum has no variants`) + **1180** (`duplicate variant name`) render the SAME `comptime type construction + failed:` diagnostic the VM-strict path in `comptime.zig` already emits (no snapshot churn at the flip); **1654** + (asm-global called at `#run`) — the VM bails cleanly via `callHostExtern` dlsym ("symbol not found … target- + specific binding called at compile time?"), only its `.stderr` WORDING changes (legacy `CannotEvalComptime + (op=call…)` → VM strict form) and must be reconciled at the flip. **Key conclusion (the prompt's flagged risk): + no SUCCESSFUL corpus example relies on the legacy VM→legacy fallback** — every passing example runs natively on + the VM; the only fallback users are the 3 expected-failures above. Removing the fallback is therefore safe. No + ops to port before flipping. NEXT: make `-Dcomptime-flat` permanent + delete the fallback (emit_llvm both sites + + comptime.zig) + remove `entryNeedsVm`, reconcile 1654. - **P5.8 (partial) — real-project validation: m3te + distribution build with the new pipeline (2026-06-19).** Acceptance test for the sx-driven build pipeline. **m3te** (`~/projects/m3te`, an SDL3 match-3 game): migrated its `build.sx` off the deleted API — `configure_build :: ()` → `() abi(.compiler)`, `opts.set_post_link_callback( diff --git a/examples/1654-platform-asm-global-comptime-call.sx b/examples/1654-platform-asm-global-comptime-call.sx index 69acded3..7360cf21 100644 --- a/examples/1654-platform-asm-global-comptime-call.sx +++ b/examples/1654-platform-asm-global-comptime-call.sx @@ -1,6 +1,6 @@ // ASM stream — calling a global-asm symbol at COMPILE TIME (`#run`) fails loud. // A module-asm symbol only exists once the module is assembled+linked; the -// comptime interpreter resolves `extern` calls via host `dlsym` (RTLD_DEFAULT), +// comptime VM resolves `extern` calls via host `dlsym` (RTLD_DEFAULT), // where the symbol is absent — so `#run my_add(…)` cannot evaluate and reports a // clear diagnostic instead of silently misfiring. (Calling the same symbol at // RUNTIME works under both JIT and AOT — see 1648/1653.) The failure is at diff --git a/examples/expected/1654-platform-asm-global-comptime-call.stderr b/examples/expected/1654-platform-asm-global-comptime-call.stderr index 90dad3f4..7910dec1 100644 --- a/examples/expected/1654-platform-asm-global-comptime-call.stderr +++ b/examples/expected/1654-platform-asm-global-comptime-call.stderr @@ -1 +1 @@ -error: comptime init of 'COMPUTED' failed: CannotEvalComptime (op=call: comptime extern call: symbol not found via dlsym (target-specific binding called at compile time?)) +error: comptime init of 'COMPUTED' failed: comptime extern call: symbol not found via dlsym (target-specific binding called at compile time?) diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 5ec967e3..749468fb 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -855,30 +855,6 @@ pub const LLVMEmitter = struct { std.debug.print("help: handle it at the `#run` site — `#run catch (e) {{ ... }}` or `#run or `\n", .{}); } - /// True when comptime entry `func_id` directly calls a compiler-domain / - /// compiler-welded function (or carries a `compiler_call` op). Such an entry - /// MUST run on the comptime VM: the BuildOptions accessors (Phase 5.5) are - /// VM-only (`comptime_vm.callBuildOptionFn`) with no legacy handler, so a - /// legacy-interp run would bail. Routes the `#run` / const-init through the VM - /// (no legacy fallback) regardless of the `-Dcomptime-flat` gate, keeping - /// gate-OFF green until P5.7 retires the legacy interpreter entirely. - fn entryNeedsVm(self: *const LLVMEmitter, func_id: ir_inst.FuncId) bool { - const func = self.ir_mod.getFunction(func_id); - for (func.blocks.items) |blk| { - for (blk.insts.items) |inst| { - switch (inst.op) { - .call => |call_op| { - const callee = self.ir_mod.getFunction(call_op.callee); - if (callee.compiler_welded or callee.is_compiler_domain) return true; - }, - .compiler_call => return true, - else => {}, - } - } - } - return false; - } - /// Run comptime side-effect functions (e.g., `#run main();` at top level). /// These are functions marked `is_comptime = true` with void return that /// aren't associated with any global. They produce compile-time output. @@ -893,70 +869,20 @@ pub const LLVMEmitter = struct { if (!std.mem.startsWith(u8, fname, "__run")) continue; const func_id = ir_inst.FuncId.fromIndex(@intCast(i)); - var interp_inst = Interpreter.init(self.ir_mod, self.alloc); - interp_inst.build_config = &self.build_config; - if (self.import_sources) |sm| interp_inst.setSourceMap(sm); sx_trace_clear(); - Interpreter.last_bail_op = null; - Interpreter.last_bail_builtin = null; - Interpreter.last_bail_detail = null; - // Flat-memory VM fast path (gated by `SX_COMPTIME_FLAT`), same as the - // const-init fold: a VM-handled side-effect that needs no `print`/extern - // runs entirely on the VM (no buffered output); anything it can't handle - // (`print`, an unported op) bails → `null` → the legacy interpreter below. - // A compiler-domain entry (calls a BuildOptions accessor / other - // compiler-welded fn) MUST run on the VM — its primitives have no legacy - // handler (Phase 5.5). Force the VM attempt + no-fallback for it, - // regardless of the `-Dcomptime-flat` gate. - const need_vm = self.entryNeedsVm(func_id); - const vm_result: ?Value = if (self.comptime_flat or need_vm) - comptime_vm.tryEval(self.alloc, self.ir_mod, func_id, &self.build_config, self.import_sources) - else - null; - if (self.comptime_flat and self.comptime_flat_trace) { - if (vm_result != null) - std.debug.print("[comptime-vm] HANDLED run '{s}'\n", .{fname}) - else - std.debug.print("[comptime-vm] fallback run '{s}': {s}\n", .{ fname, comptime_vm.last_bail_reason orelse "" }); - } - const result = vm_result orelse fallback: { - // Strict mode (or a compiler-domain entry): NO fallback — a VM bail - // is a build-gating error naming the reason. - if (self.comptime_flat_strict or need_vm) { - std.debug.print("error: comptime `#run` ({s}) bailed on the VM (strict, no fallback): {s}\n", .{ fname, comptime_vm.last_bail_reason orelse "" }); - self.comptime_failed = true; - break :fallback Value.void_val; - } - // The VM bailed: discard any return-trace frames it pushed before - // bailing (`sx_trace_push` is a side effect on the shared buffer), - // else the legacy re-run double-pushes them (see 1035). - if (self.comptime_flat) sx_trace_clear(); - break :fallback interp_inst.call(func_id, &.{}) catch |err| blk: { - // A comptime `#run` side-effect that bails must NOT silently - // truncate its output and still ship a successful build. - // Surface the bail loudly and fail the build, mirroring the - // const-init path in emitGlobals. Whatever output the run - // produced before the bail is flushed below so the user sees - // where execution stopped. - const op = Interpreter.last_bail_op orelse ""; - const detail = Interpreter.last_bail_detail orelse ""; - const sep: []const u8 = if (detail.len > 0) ": " else ""; - std.debug.print("error: comptime `#run` ({s}) failed: {s} (op={s}{s}{s})\n", .{ fname, @errorName(err), op, sep, detail }); - self.comptime_failed = true; - break :blk Value.void_val; - }; + // The comptime VM is the SOLE evaluator (P5.7) — no legacy fallback. + // A VM-run `#run` side-effect writes its `print` output directly to + // fd 1 via host-FFI (no buffered interp output to flush). A bail is a + // build-gating error naming the reason. + const result = comptime_vm.tryEval(self.alloc, self.ir_mod, func_id, &self.build_config, self.import_sources) orelse { + std.debug.print("error: comptime `#run` ({s}) failed: {s}\n", .{ fname, comptime_vm.last_bail_reason orelse "" }); + self.comptime_failed = true; + continue; }; - // Route #run `print` output to fd 1 so it joins the - // JIT-executed runtime's stream. Same call site shape as - // `core.flushInterpOutput` — see issue-0047. - if (interp_inst.output.items.len > 0) { - _ = std.c.write(1, interp_inst.output.items.ptr, interp_inst.output.items.len); - } // A bare failable `#run f();` whose error escapes → diagnostic + halt. if (self.comptimeErrChannel(func.ret) != null) { _ = self.checkComptimeFailable(result, func.ret, "top-level statement"); } - interp_inst.deinit(); } } @@ -1005,59 +931,25 @@ pub const LLVMEmitter = struct { // Evaluate comptime initializer if present if (global.comptime_func) |func_id| { + // The comptime VM is the SOLE evaluator (P5.7) — no legacy + // fallback. A bail is ALWAYS a build-gating error naming the + // reason. `interp_inst` is a fresh helper context that + // `valueToLLVMConst` uses to materialize the VM's result Value + // (strings / aggregates) — it does NOT evaluate anything. var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; if (self.import_sources) |sm| interp_inst.setSourceMap(sm); - Interpreter.last_bail_op = null; - Interpreter.last_bail_builtin = null; - Interpreter.last_bail_detail = null; sx_trace_clear(); - // Flat-memory VM fast path (gated by `SX_COMPTIME_FLAT`): run the - // comptime initializer on the VM; `null` (unsupported op / any - // bail / implicit-ctx) falls through to the legacy interpreter - // below, which produces the identical result. Default OFF. - // A compiler-domain initializer (reaches a BuildOptions accessor / - // other compiler-welded fn) MUST run on the VM — no legacy handler - // exists (Phase 5.5). Force the VM + no-fallback for it. - const need_vm = self.entryNeedsVm(func_id); - const vm_result: ?Value = if (self.comptime_flat or need_vm) - comptime_vm.tryEval(self.alloc, self.ir_mod, func_id, &self.build_config, self.import_sources) - else - null; - // Coverage trace (gated): report whether the VM handled this - // comptime init or fell back, and why — names what to port next. - if (self.comptime_flat and self.comptime_flat_trace) { + const result = comptime_vm.tryEval(self.alloc, self.ir_mod, func_id, &self.build_config, self.import_sources) orelse { + // Surface the bail loudly instead of silently filling the + // const with zero. Leave the global undef; comptime_failed + // halts the build before it ships. const gname = self.ir_mod.types.getString(global.name); - if (vm_result != null) { - std.debug.print("[comptime-vm] HANDLED init '{s}'\n", .{gname}); - } else { - std.debug.print("[comptime-vm] fallback init '{s}': {s}\n", .{ gname, comptime_vm.last_bail_reason orelse "" }); - } - } - const result = vm_result orelse fallback: { - // Strict mode (or a compiler-domain init): NO fallback — a VM bail - // is a build-gating error. - if (self.comptime_flat_strict or need_vm) { - const gname = self.ir_mod.types.getString(global.name); - std.debug.print("error: comptime init of '{s}' bailed on the VM (strict, no fallback): {s}\n", .{ gname, comptime_vm.last_bail_reason orelse "" }); - self.comptime_failed = true; - break :fallback Value.void_val; - } - // The VM bailed: discard any return-trace frames it pushed - // before bailing, so the legacy re-run doesn't double-push. - if (self.comptime_flat) sx_trace_clear(); - break :fallback interp_inst.call(func_id, &.{}) catch |err| blk: { - // Surface the bail loudly instead of silently filling - // the const with zero. Stale state from a previous - // comptime function would otherwise hide the error. - const op = Interpreter.last_bail_op orelse ""; - const detail = Interpreter.last_bail_detail orelse ""; - const sep: []const u8 = if (detail.len > 0) ": " else ""; - const gname = self.ir_mod.types.getString(global.name); - std.debug.print("error: comptime init of '{s}' failed: {s} (op={s}{s}{s})\n", .{ gname, @errorName(err), op, sep, detail }); - self.comptime_failed = true; - break :blk .void_val; - }; + std.debug.print("error: comptime init of '{s}' failed: {s}\n", .{ gname, comptime_vm.last_bail_reason orelse "" }); + self.comptime_failed = true; + c.LLVMSetInitializer(llvm_global, c.LLVMGetUndef(llvm_ty)); + self.global_map.put(@intCast(i), llvm_global) catch {}; + continue; }; // A bare failable `NAME :: #run f();`: the comptime function // returns the failable tuple; split it. Escaping error → diff --git a/src/ir/lower/comptime.zig b/src/ir/lower/comptime.zig index b517eb27..4bb9a412 100644 --- a/src/ir/lower/comptime.zig +++ b/src/ir/lower/comptime.zig @@ -506,74 +506,33 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty } } - var interp = interp_mod.Interpreter.init(self.module, self.alloc); - defer interp.deinit(); - if (self.diagnostics) |d| if (d.import_sources) |sm| interp.setSourceMap(sm); - interp.setMintTable(&self.module.types); - - // Clear the interp's last-bail channel so a bail HERE is attributable to - // THIS construction (not a stale message from an earlier comptime eval). - interp_mod.Interpreter.last_bail_detail = null; - - // Flat-memory VM fast path (gated by `-Dcomptime-flat` / `SX_COMPTIME_FLAT`), - // the THIRD comptime call site after the two emit-time folds. A type-fn runs - // on the VM; `null` (any bail) falls through to the legacy interpreter below, - // which mints identically. The VM bails BEFORE any table mutation — its - // compiler-WRITE fns (declare_type/register_type/pointer_to) aren't ported to - // `callCompilerFn`, and it can't yet model a `Type` result — so a minting - // type-fn bails at the first write call (no partial mint → no double-mint). - // The VM is hardened against malformed lowering-time IR (it BAILS, never - // panics; see `comptime_vm.refTy`/`badRef`). Today this is near-pure fallback; - // it lights up as `Type` modeling + the VM-native write side land. - // Strict mode implies flat (run the VM, then hard-error instead of falling back). - const comptime_flat = build_opts.comptime_flat or std.c.getenv("SX_COMPTIME_FLAT") != null or - build_opts.comptime_flat_strict or std.c.getenv("SX_COMPTIME_FLAT_STRICT") != null; - const vm_result: ?interp_mod.Value = if (comptime_flat) - comptime_vm.tryEval(self.alloc, self.module, func_id, null, null) - else - null; - if (comptime_flat and std.c.getenv("SX_COMPTIME_FLAT_TRACE") != null) { + // The comptime VM is the SOLE evaluator (P5.7) — no legacy fallback. A + // type-fn runs on the VM; a bail is ALWAYS a build-gating diagnostic, never a + // fallback. The VM is hardened against malformed lowering-time IR (it BAILS, + // never panics; see `comptime_vm.refTy`/`badRef`), and bails BEFORE any table + // mutation, so a failed mint never leaves a partial type. + const vm_result = comptime_vm.tryEval(self.alloc, self.module, func_id, null, null); + if (std.c.getenv("SX_COMPTIME_FLAT_TRACE") != null) { if (vm_result != null) std.debug.print("[comptime-vm] HANDLED type-fn\n", .{}) else - std.debug.print("[comptime-vm] fallback type-fn: {s}\n", .{comptime_vm.last_bail_reason orelse ""}); + std.debug.print("[comptime-vm] BAIL type-fn: {s}\n", .{comptime_vm.last_bail_reason orelse ""}); } if (vm_result) |v| { const tid_vm = v.asTypeId() orelse return null; return checkComptimeTypeResult(self, tid_vm, span); } - // Strict mode: NO fallback — render the VM's bail reason as the SAME - // build-gating diagnostic the non-strict legacy path emits below (the VM and - // legacy set identical detail strings, e.g. "comptime define(): duplicate - // variant name 'x'"), so a comptime type-construction failure (1179/1180) - // produces its proper user diagnostic with no legacy interp in the loop — the - // 4B step toward deleting the fallback. (4B / VM-native diagnostics.) - if (build_opts.comptime_flat_strict or std.c.getenv("SX_COMPTIME_FLAT_STRICT") != null) { - if (self.diagnostics) |d| { - d.addFmt(.err, span, "comptime type construction failed: {s}", .{comptime_vm.last_bail_reason orelse ""}); - } - return null; + // VM bailed: render a build-gating diagnostic naming the reason — NOT poison + // to `.unresolved` silently and let that crash at LLVM emission ("unresolved + // type reached LLVM emission") or hide behind a downstream cascade (issue + // 0140). The VM's bail reason carries the precise cause (e.g. "comptime + // define(): duplicate variant name 'x'"), so a comptime type-construction + // failure (1179/1180) produces its proper user diagnostic. + if (self.diagnostics) |d| { + d.addFmt(.err, span, "comptime type construction failed: {s}", .{comptime_vm.last_bail_reason orelse ""}); } - - const result = interp.call(func_id, &.{}) catch |err| { - // A comptime type construction (declare/define, reflection) that bails - // must surface a build-gating diagnostic naming the reason — NOT poison - // to `.unresolved` silently and let that crash at LLVM emission - // ("unresolved type reached LLVM emission") or hide behind a downstream - // cascade (issue 0140). The interp's `bailDetail` already set the precise - // reason; mirror the `#run` path (emit_llvm.zig) and render it. Returning - // null keeps the `.unresolved` poison for the caller, but now the build - // is gated by a real message, so no unresolved type reaches emission - // unannounced. - if (self.diagnostics) |d| { - const detail = interp_mod.Interpreter.last_bail_detail orelse @errorName(err); - d.addFmt(.err, span, "comptime type construction failed: {s}", .{detail}); - } - return null; - }; - const tid = result.asTypeId() orelse return null; - return checkComptimeTypeResult(self, tid, span); + return null; } /// Post-check a comptime type-construction result (shared by the VM and legacy