comptime VM: host wiring, full corpus parity, build flag, Phase 3 seed

Phase 1.final of the flat-memory comptime VM — wire the host through it,
reach corpus parity, and gate it behind a build flag — plus the first
Phase 3 (compiler-API) step. Default OFF; legacy interpreter unchanged.

Host wiring + hardening:
- Machine accessors return error.OutOfBounds (no debug panic) on bad
  addresses; Frame.get/set bounds-check and bail (no panic) on a malformed
  operand ref (e.g. a ret Ref.none from an unresolved name).
- tryEval routed at both comptime call sites in emit_llvm — the const-init
  fold and the #run side-effect path — with per-eval legacy fallback;
  yields .void_val for void/noreturn entries. Both sites sx_trace_clear()
  before the legacy fallback so a partial VM run that pushed trace frames
  doesn't double-push on re-run.

VM coverage (all corpus const-inits except the inline-asm global):
- Implicit context materialized from the __sx_default_context global; the
  full allocator protocol runs on the VM (context.allocator.alloc ->
  call_indirect -> CAllocator thunk -> libc_malloc -> native flat malloc).
- Native libc memory builtins (malloc/calloc/free/memcpy/memmove/memset)
  on flat memory; f32 stored/loaded as the 4-byte single; signed sub-64-bit
  loads sign-extended; global_get (lazy + memoized); func_ref/call_indirect
  (func-ref encoded fid+1, 0 reserved for null); string/slice fat-pointer
  field access; is_comptime; the failable/error cluster (error_set tuples,
  trace_frame + native sx_trace_push/clear -> raise/catch/or + return traces).

Build flag + Phase 3 seed:
- -Dcomptime-flat (build_opts module) OR SX_COMPTIME_FLAT env enables the VM;
  zig build test -Dcomptime-flat runs the full corpus on the VM (688/0).
- intern/text_of serviced natively on flat memory via Vm.callCompilerFn
  (compiler_welded boundary) — the seed the rest of the compiler-API grows on.

Parity 688/688 gate ON and OFF. Unit tests added throughout. The
lowering-time #insert wiring was explored and reverted (lowering-time IR can
be malformed; full malformed-IR hardening is a prerequisite, deferred).
This commit is contained in:
agra
2026-06-18 08:27:58 +03:00
parent b8f3d6fd78
commit 0367d96d9b
7 changed files with 1142 additions and 108 deletions

View File

@@ -32,6 +32,8 @@ const Module = ir_module.Module;
const interp_mod = @import("interp.zig");
const Interpreter = interp_mod.Interpreter;
const Value = interp_mod.Value;
const comptime_vm = @import("comptime_vm.zig");
const build_opts = @import("build_opts");
// The vendored error-trace ring buffer (library/vendors/sx_trace_runtime/sx_trace.c)
// is linked into the compiler. Comptime `#run` evaluation pushes frames to it via
@@ -113,6 +115,18 @@ pub const LLVMEmitter = struct {
// file or the JIT — the emit-time diagnostic is the surfaced error.
comptime_failed: bool = false,
// When set (env `SX_COMPTIME_FLAT`, → a `-Dcomptime-flat` build flag later),
// comptime const-init folds try the flat-memory VM (`comptime_vm.tryEval`)
// first and fall back to the legacy tagged interpreter on null. Default OFF so
// the corpus is unaffected until the VM reaches parity (Phase 1.final step d).
comptime_flat: bool = false,
// When set (env `SX_COMPTIME_FLAT_TRACE`, only meaningful with `comptime_flat`),
// each comptime const-init reports to stderr whether the VM handled it or fell
// back to the legacy interpreter (with the bail reason) — the coverage signal
// for porting the next ops. Default OFF.
comptime_flat_trace: bool = false,
// Allocator for temporary bookkeeping
alloc: Allocator,
@@ -321,6 +335,10 @@ pub const LLVMEmitter = struct {
.build_config = .{},
.di_files = std.StringHashMap(c.LLVMMetadataRef).init(alloc),
.frame_str_cache = std.StringHashMap(c.LLVMValueRef).init(alloc),
// Enabled by the `-Dcomptime-flat` build flag OR the `SX_COMPTIME_FLAT`
// env var (either turns it on); default OFF (legacy interpreter).
.comptime_flat = build_opts.comptime_flat or std.c.getenv("SX_COMPTIME_FLAT") != null,
.comptime_flat_trace = std.c.getenv("SX_COMPTIME_FLAT_TRACE") != null,
};
}
@@ -845,19 +863,39 @@ pub const LLVMEmitter = struct {
Interpreter.last_bail_op = null;
Interpreter.last_bail_builtin = null;
Interpreter.last_bail_detail = null;
const result = 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 "<unknown>";
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;
// 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.
const vm_result: ?Value = if (self.comptime_flat)
comptime_vm.tryEval(self.alloc, self.ir_mod, func_id)
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 "<unknown>" });
}
const result = vm_result orelse fallback: {
// 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 "<unknown>";
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;
};
};
// Route #run `print` output to fd 1 so it joins the
// JIT-executed runtime's stream. Same call site shape as
@@ -925,17 +963,40 @@ pub const LLVMEmitter = struct {
Interpreter.last_bail_builtin = null;
Interpreter.last_bail_detail = null;
sx_trace_clear();
const result = 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 "<unknown>";
const detail = Interpreter.last_bail_detail orelse "";
const sep: []const u8 = if (detail.len > 0) ": " else "";
// 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.
const vm_result: ?Value = if (self.comptime_flat)
comptime_vm.tryEval(self.alloc, self.ir_mod, func_id)
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 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;
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 "<unknown>" });
}
}
const result = vm_result orelse fallback: {
// 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 "<unknown>";
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;
};
};
// A bare failable `NAME :: #run f();`: the comptime function
// returns the failable tuple; split it. Escaping error →