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:
@@ -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 →
|
||||
|
||||
Reference in New Issue
Block a user