P5.5: migrate the 35 BuildOptions accessors off #compiler to VM-native abi(.compiler)

`BuildOptions :: struct #compiler { ...35 methods... }` becomes
`BuildOptions :: struct { }` (an opaque null-sentinel handle) plus 35 free
`ufcs (self: BuildOptions, …) abi(.compiler)` decls in build.sx, each serviced
by a new `comptime_vm.callBuildOptionFn` arm (off `callCompilerFn`). No legacy
`compiler_lib` handler: the names are registered in `bound_fns` with a single
bailing stub only so `weldedCompilerFn` accepts them.

- String lifetime: setters dupe the arg into the persistent `Vm.gpa` (the
  Compilation allocator, threaded into both `tryEval` and `runBuildCallback` —
  not the per-eval VM arena) and write/append to the threaded `BuildConfig`.
  Getters read the field/slice or compute the target predicate from the triple.
- Dispatch routing (Option B): a `#run`/const-init entry that directly calls a
  compiler-domain/welded fn (`emit_llvm.entryNeedsVm`) runs on the VM with no
  legacy fallback regardless of the `-Dcomptime-flat` gate, so gate-OFF stays
  green without a legacy BuildOptions handler (P5.7 retires the legacy interp).
- Mark the 5 `platform/bundle.sx` getter-calling helpers `abi(.compiler)` (they
  are comptime-only bundler code; otherwise their now-welded getter calls trip
  the runtime-call gate).
- 37 `.ir` snapshots regenerated (std transitively imports build.sx → string-
  pool/type-table indices shift); verified `.ir`-only, zero behavior-stream diffs.

BuildOptions `compiler_call` strict bails gone (1609/1614/1615 strict-clean);
1616 now bails on a separate, pre-existing unported bitwise/shift VM gap (`shr`),
to port first in P5.6. 703/0 both gates.

Also sweep the outdated "flat memory" terminology to "comptime/byte-addressable"
across comptime_vm + the plan/checkpoint/CLAUDE docs: the comptime VM is
arena-backed, byte-addressable memory where `Addr` is a real host pointer, not a
flat contiguous address space (flag names `-Dcomptime-flat`/`SX_COMPTIME_FLAT` kept).
This commit is contained in:
agra
2026-06-19 13:21:09 +03:00
parent af32c3823c
commit ba28488d99
48 changed files with 13896 additions and 14974 deletions

View File

@@ -10,7 +10,7 @@
//! **Direction note (2026-06-17 pivot).** The byte-weld of TYPES (sx structs whose
//! layout was validated to mirror the compiler's Zig records) was stripped — it
//! bolted a parallel layout regime + hand-marshaling onto a comptime value model
//! that isn't bytes. The replacement is a flat-memory comptime VM where values are
//! that isn't bytes. The replacement is a comptime VM where values are
//! native bytes, so the compiler-API needs no weld/validation/marshaling (Phase 3
//! of the plan re-homes the type/function exposure on that VM). `intern`/`text_of`
//! survive here as the first compiler-call seed: clean scalar host-calls (string in,
@@ -42,7 +42,7 @@ pub const BoundFn = struct {
};
/// The compiler-function export list. The `StringId` round-trip readers are the
/// seed; the type-table API (lookup / register) is re-homed onto the flat-memory
/// seed; the type-table API (lookup / register) is re-homed onto the comptime
/// VM in Phase 3 of `PLAN-COMPILER-VM.md`.
pub const bound_fns = [_]BoundFn{
.{ .sx_name = "intern", .handler = handleIntern },
@@ -75,8 +75,58 @@ pub const bound_fns = [_]BoundFn{
.{ .sx_name = "build_target", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_frameworks", .handler = handleBuildPipelineQuery },
.{ .sx_name = "build_flags", .handler = handleBuildPipelineQuery },
// ── BuildOptions accessors (Phase 5.5) ───────────────────────────────────
// Migrated off the `struct #compiler` hook surface to free `abi(.compiler)`
// functions serviced by `comptime_vm.callCompilerFn`. VM-only: any `#run` /
// const-init reaching them is routed to the VM (emit_llvm `entryNeedsVm`), so
// these legacy stubs are never reached — registered only so `weldedCompilerFn`
// recognizes the names. They bail loudly rather than fabricate a silent result.
.{ .sx_name = "add_link_flag", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "add_framework", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_output_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_wasm_shell", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "add_asset_dir", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "asset_dir_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "asset_dir_src_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "asset_dir_dest_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_post_link_module", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "binary_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_bundle_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_bundle_id", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_codesign_identity", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_provisioning_profile", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "bundle_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "bundle_id", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "codesign_identity", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "provisioning_profile", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "target_triple", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_macos", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_ios", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_ios_device", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_ios_simulator", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "is_android", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_path_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "framework_path_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_manifest_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "set_keystore_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "manifest_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "keystore_path", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "jni_main_count", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "jni_main_runtime_path_at", .handler = handleBuildOptionsAccessor },
.{ .sx_name = "jni_main_java_source_at", .handler = handleBuildOptionsAccessor },
};
/// Legacy-path stub for the Phase 5.5 BuildOptions accessors — see the `bound_fns`
/// comment. Any `#run` / const-init reaching a BuildOptions accessor is routed to
/// the VM (`emit_llvm.entryNeedsVm`), so this is never reached; it bails loudly
/// rather than fabricate a silent result.
fn handleBuildOptionsAccessor(_: *Interpreter, _: []const Value) InterpError!Value {
Interpreter.last_bail_detail = "BuildOptions accessor is VM-only (Phase 5.5); not available on the legacy interpreter";
return error.CannotEvalComptime;
}
/// Legacy-path stub for the Phase 5 build-pipeline primitives — see the
/// `bound_fns` comment. The only caller (the post-link build driver) runs on the
/// VM (`core.invokeByFuncId`), so these legacy handlers are never reached; they
@@ -132,7 +182,7 @@ fn handleTextOf(interp: *Interpreter, args: []const Value) InterpError!Value {
/// union / tagged-union / error-set) by its interned name and return its handle.
/// A name with no matching type yields the dedicated `unresolved` sentinel (a
/// `TypeId` of 0), the codebase-blessed "no type" marker — NOT an `?Type` (a
/// `Type` value is `.any`-typed, which the flat-memory VM does not represent, and
/// `Type` value is `.any`-typed, which the comptime VM does not represent, and
/// an optional can't cross the legacy↔VM eval boundary). The caller checks the
/// handle against 0 / `unresolved`. The VM mirrors this in `comptime_vm.callCompilerFn`.
fn handleFindType(interp: *Interpreter, args: []const Value) InterpError!Value {

View File

@@ -1,4 +1,4 @@
// Tests for the flat-memory comptime machine (Phase 1 of PLAN-COMPILER-VM.md).
// Tests for the byte-addressable comptime machine (Phase 1 of PLAN-COMPILER-VM.md).
const std = @import("std");
const vm = @import("comptime_vm.zig");
@@ -710,7 +710,7 @@ test "comptime_vm exec: payloadless enum_init + enum_tag" {
test "comptime_vm exec: tagged-union enum_init with payload lays out {tag@0, payload@tag_size}" {
// The construction primitive `define` reuses: build `E.value(42)` where
// `E = { value: i64, closed: void }` and verify the flat-memory bytes — tag 0
// `E = { value: i64, closed: void }` and verify the comptime bytes — tag 0
// at offset 0, the i64 payload at offset tag_size (8). Mirrors the LLVM
// `{ header, [N x i8] }` layout the rest of the compiler reads.
const alloc = std.testing.allocator;
@@ -843,7 +843,7 @@ test "comptime_vm exec: f32 store/load round-trips through 4-byte memory" {
try std.testing.expectEqual(@as(i64, 1), toI64(try v.run(&fb.func, &.{})));
}
test "comptime_vm exec: malloc builtin gives usable flat memory; free is a no-op" {
test "comptime_vm exec: malloc builtin gives usable comptime memory; free is a no-op" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
@@ -1282,7 +1282,7 @@ test "comptime_vm bridge: Value <-> Reg round-trips (scalar, string, struct)" {
const back_i = try v.regToValue(alloc, &table, r_i, .i64);
try std.testing.expectEqual(@as(i64, 42), back_i.int);
// string (materialized into flat memory, read back + deep-copied out)
// string (materialized into comptime memory, read back + deep-copied out)
const r_s = try v.valueToReg(&table, .{ .string = "hi" }, .string);
const back_s = try v.regToValue(alloc, &table, r_s, .string);
defer alloc.free(back_s.string);

View File

@@ -1,19 +1,19 @@
//! Flat-memory comptime machine — Phase 1 of `current/PLAN-COMPILER-VM.md`.
//! Byte-addressable comptime machine — Phase 1 of `current/PLAN-COMPILER-VM.md`.
//!
//! The comptime evaluator is being rebuilt around a flat, byte-addressable memory
//! The comptime evaluator is being rebuilt around a byte-addressable memory
//! so comptime values are NATIVE BYTES (like runtime), instead of the tagged
//! `Value` union the legacy interpreter (`interp.zig`) uses. This module is the
//! machine substrate: byte-addressable memory backed by an ARENA of stable host
//! allocations (each `allocBytes` never moves; freed wholesale on `deinit`), plus
//! a per-call `Frame` holding a register file. `Addr` is the allocation's real
//! host pointer, so a flat-memory pointer and an FFI-returned host pointer are the
//! host pointer, so a comptime pointer and an FFI-returned host pointer are the
//! same kind of value.
//!
//! Value model (grows over later sub-steps): a register (`Reg`) is a raw 64-bit
//! word that is EITHER an immediate scalar (its bits) OR an `Addr` into flat
//! word that is EITHER an immediate scalar (its bits) OR an `Addr` into comptime
//! memory (for aggregates) — interpreted by the IR result type, exactly like a
//! real machine / LLVM. Scalars up to 64 bits (sx's widest is `i64`/`u64`/`f64`)
//! fit a register directly; structs/arrays/slices live in flat memory and a
//! fit a register directly; structs/arrays/slices live in comptime memory and a
//! register holds their address.
//!
//! Target-awareness lives in the EXECUTOR, not here: this module only moves raw
@@ -23,7 +23,7 @@
//! `Machine` (arena-backed memory + scalar word read/write + byte views) holds the
//! comptime stack + heap; `Frame` is the per-call register file. A `Frame` does NOT
//! reclaim the machine's memory on exit — a callee can return an aggregate whose
//! register holds an `Addr` into flat memory, and reclaiming would dangle it. The
//! register holds an `Addr` into comptime memory, and reclaiming would dangle it. The
//! legacy interpreter remains the live evaluator until the VM reaches parity.
const std = @import("std");
@@ -55,7 +55,7 @@ const Span = inst_mod.Span;
/// machine allocates each object from an arena that never moves it. `null_addr` (0)
/// is the null sentinel (no allocation is ever at address 0), so a zeroed register
/// reads as null — mirroring how the legacy `Value` model distinguishes `null_val`.
/// Because addresses are absolute host pointers, a flat-memory pointer and an
/// Because addresses are absolute host pointers, a comptime pointer and an
/// FFI-returned host pointer are the SAME kind of value: the FFI bridge hands them
/// to / from real libc with no translation (Phase 4D).
pub const Addr = u64;
@@ -70,7 +70,7 @@ pub const Reg = u64;
/// NEVER moves and is freed wholesale on `deinit` (no per-object free — comptime is
/// short-lived). There is NO fixed buffer and NO size cap: the arena grows through
/// its backing allocator on demand. `Addr` is the allocation's REAL host pointer,
/// so a flat-memory pointer and an FFI-returned host pointer are interchangeable —
/// so a comptime pointer and an FFI-returned host pointer are interchangeable —
/// the FFI bridge passes them to / from libc untouched (Phase 4D).
pub const Machine = struct {
arena: std.heap.ArenaAllocator,
@@ -134,7 +134,7 @@ pub const Machine = struct {
/// One call frame: a register file indexed by IR `Ref` index. It does NOT reclaim
/// the machine stack on exit — a callee can return an aggregate whose value is an
/// `Addr` into flat memory, and reclaiming the callee's region would dangle it.
/// `Addr` into comptime memory, and reclaiming the callee's region would dangle it.
/// Comptime evaluation is bounded, so all allocations live until `Vm.deinit`;
/// `Machine.mark`/`reset` remain for explicit scoped use. The register file IS
/// per-call (each `run` gets a fresh one sized to its callee's Ref space).
@@ -182,10 +182,10 @@ pub const Frame = struct {
pub var last_bail_reason: ?[]const u8 = null;
/// Wiring entry point: try to evaluate comptime function `func_id` entirely on the
/// flat-memory VM and return its result as a legacy `Value`, or `null` if the VM
/// comptime VM and return its result as a legacy `Value`, or `null` if the VM
/// can't handle it (unsupported op, no body, or any bail) — the caller then falls
/// back to the legacy interpreter. The result is deep-copied into `gpa`, so it
/// outlives the VM's flat memory (freed here on return).
/// outlives the VM's comptime memory (freed here on return).
///
/// Safe for ARBITRARY host comptime functions: the `Machine` accessors are
/// hardened to return `error.OutOfBounds` (not a debug panic) on a null/out-of-
@@ -207,7 +207,7 @@ pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.
// `runEntry` materializes the implicit `*Context` (a comptime const-init /
// `#run` wrapper is nullary in user args, so the implicit ctx is its sole
// param) as a zeroed Context in flat memory and runs. The common const body
// param) as a zeroed Context in comptime memory and runs. The common const body
// never reads the ctx; one that uses the allocator hits unported
// `call_indirect` → bails → legacy. Gate-ON corpus parity validates this.
const reg = vm.runEntry(func_id) catch |err| {
@@ -257,7 +257,7 @@ pub fn runBuildCallback(gpa: std.mem.Allocator, module: *const Module, func_id:
// ── Executor ────────────────────────────────────────────────────────────────
//
// Walks the SAME SSA IR the legacy interpreter (`interp.zig`) walks, but over
// flat-memory frames: each SSA result is a `Reg` word (immediate scalar bits, or
// comptime frames: each SSA result is a `Reg` word (immediate scalar bits, or
// an `Addr`). Scalar semantics MIRROR the legacy interp so the two evaluators
// agree byte-for-byte (the parity goal): integer math is 64-bit wrapping/signed
// (`+%`, `@divTrunc`, signed compares — the legacy's `.int` is i64 regardless of
@@ -284,7 +284,7 @@ fn nominalIdentOf(info: types.TypeInfo) ?struct { name: types.StringId, nominal_
};
}
/// A `{ name: string, ty: Type }` member decoded from flat memory — the shared
/// A `{ name: string, ty: Type }` member decoded from comptime memory — the shared
/// shape of a compiler-API `Member`, a metatype `EnumVariant { name, payload }`,
/// and a `StructField { name, type }` (all 2-field `{ string, Type }` structs).
const NamedMember = struct { name: types.StringId, ty: TypeId };
@@ -304,6 +304,41 @@ fn signExtendWord(raw: Reg, sz: usize) Reg {
return @bitCast((@as(i64, @bitCast(raw)) << shift) >> shift);
}
// ── BuildOptions target predicates (Phase 5.5) ───────────────────────────────
// Computed from the `--target` triple, mirroring `compiler_hooks`'s legacy hooks
// (which mirror `TargetConfig.is{MacOS,IOS,IOSDevice,IOSSimulator}()`).
fn tripleHas(triple: ?[]const u8, needle: []const u8) bool {
const t = triple orelse return false;
return std.mem.indexOf(u8, t, needle) != null;
}
fn predIsIOS(triple: ?[]const u8) bool {
return tripleHas(triple, "apple-ios");
}
fn predIsMacOS(triple: ?[]const u8) bool {
if (predIsIOS(triple)) return false;
return tripleHas(triple, "apple-macosx") or tripleHas(triple, "apple-macos") or tripleHas(triple, "apple-darwin");
}
fn predIsIOSDevice(triple: ?[]const u8) bool {
return predIsIOS(triple) and !tripleHas(triple, "simulator");
}
fn predIsIOSSimulator(triple: ?[]const u8) bool {
return predIsIOS(triple) and tripleHas(triple, "simulator");
}
fn predIsAndroid(triple: ?[]const u8) bool {
return tripleHas(triple, "android");
}
/// Map a BuildOptions predicate name (`is_macos`/…) to its triple-test, or null.
fn boolPredicate(name: []const u8) ?*const fn (?[]const u8) bool {
if (std.mem.eql(u8, name, "is_macos")) return predIsMacOS;
if (std.mem.eql(u8, name, "is_ios")) return predIsIOS;
if (std.mem.eql(u8, name, "is_ios_device")) return predIsIOSDevice;
if (std.mem.eql(u8, name, "is_ios_simulator")) return predIsIOSSimulator;
if (std.mem.eql(u8, name, "is_android")) return predIsAndroid;
return null;
}
pub const Vm = struct {
machine: Machine,
gpa: std.mem.Allocator,
@@ -381,16 +416,16 @@ pub const Vm = struct {
return self.run(func, argbuf.items);
}
/// Materialize the default `Context` in flat memory and return its address —
/// Materialize the default `Context` in comptime memory and return its address —
/// the VM analogue of the static `__sx_default_context` global / the legacy
/// `defaultContextValue`. The implicit-ctx param is an opaque `*void`, so the
/// real Context type AND its initializer (the nested `{ {null, alloc_fn,
/// dealloc_fn}, null }` constant carrying the CAllocator thunk func-refs) come
/// from the `__sx_default_context` global. Laying that constant into flat memory
/// from the `__sx_default_context` global. Laying that constant into comptime memory
/// gives a context whose `alloc_fn`/`dealloc_fn` are real func-refs, so a
/// comptime body that allocates via `context.allocator` dispatches through
/// `call_indirect` to the thunk to `CAllocator.alloc_bytes` to `libc_malloc` to
/// the VM's native `malloc` (flat memory) — all on the VM, no host heap. If no
/// the VM's native `malloc` (comptime memory) — all on the VM, no host heap. If no
/// `__sx_default_context` global exists, bail (legacy fallback).
fn materializeDefaultContext(self: *Vm, module: *const Module) Error!Addr {
const table = self.table orelse return self.failMsg("comptime VM: default context needs a type table");
@@ -431,7 +466,7 @@ pub const Vm = struct {
return null;
}
/// Lay a static `ConstantValue` of type `ty` into flat memory at `addr` (the
/// Lay a static `ConstantValue` of type `ty` into comptime memory at `addr` (the
/// destination is pre-zeroed). Scalars/func-refs write a word; a null/zero/undef
/// leaf stays zeroed; an aggregate recurses per field at the type's natural
/// offsets. Builds the default context from its global constant.
@@ -899,7 +934,7 @@ pub const Vm = struct {
return .{ .value = null_addr };
},
// Unpack a comptime frame `(func_id << 32 | span.start)` and build a
// `Frame { file, line, col, func, line_text }` aggregate in flat memory —
// `Frame { file, line, col, func, line_text }` aggregate in comptime memory —
// the VM-native mirror of the legacy interp's `.trace_resolve`. `ins.ty`
// is the `Frame` struct, so each field's type/offset comes from the table.
.trace_resolve => |u| {
@@ -940,7 +975,7 @@ pub const Vm = struct {
},
// `error_tag_name(e)` — the runtime tag id (a word) → its name string via
// the always-linked tag-name table. Pure: builds a `{ptr,len}` string in
// flat memory. Mirrors the legacy interp's `error_tag_name_get`.
// comptime memory. Mirrors the legacy interp's `error_tag_name_get`.
.error_tag_name_get => |u| {
const table = try self.requireTable();
const id: u32 = @intCast(frame.get(u.operand.index()));
@@ -969,7 +1004,7 @@ pub const Vm = struct {
.global_get => |gid| return .{ .value = try self.evalGlobal(gid) },
// `&global` — only `&__sx_default_context` is materialised at comptime
// (its address sees runtime use via the implicit-ctx plumbing). Return
// the context's flat-memory address — an aggregate value IS its address,
// the context's comptime address — an aggregate value IS its address,
// so a later `load`/field read sees the materialised Context. Mirrors the
// legacy interp's `global_addr` (the sole supported global); any other
// global bails to legacy fallback.
@@ -1019,7 +1054,7 @@ pub const Vm = struct {
// layout). The tag is the source TypeId index (matches the legacy comptime
// interp; runtime `anyTag` additionally normalizes arbitrary-width ints —
// an existing legacy/runtime split). The value slot holds a word source's
// scalar bytes, or an aggregate source's flat-memory ADDR (the runtime
// scalar bytes, or an aggregate source's comptime ADDR (the runtime
// "pointer in the value slot" shape — see emit_llvm.coerceToI64's struct path).
.box_any => |ba| {
const table = try self.requireTable();
@@ -1174,19 +1209,19 @@ pub const Vm = struct {
/// shared by `call` (static callee) and `call_indirect` (func-ref callee). An
/// extern/bodyless callee routes to the native libc memory builtins (else
/// bails); a normal callee runs on the VM. Aggregate args pass as their Addr
/// over the shared flat memory (no copy).
/// over the shared comptime memory (no copy).
fn invoke(self: *Vm, fid: inst_mod.FuncId, args: []const Ref, frame: *Frame, ref_types: []const TypeId, result_ty: TypeId) Error!Reg {
const module = self.module orelse return self.failMsg("comptime VM: call needs a module (not provided)");
if (fid.index() >= module.functions.items.len) return self.failMsg("comptime VM: call to an out-of-range function id");
const callee = module.getFunction(fid);
if (callee.is_extern or callee.blocks.items.len == 0) {
const name = module.types.getString(callee.name);
// A curated set of libc MEMORY builtins is modeled natively on flat
// A curated set of libc MEMORY builtins is modeled natively on comptime
// memory (sandboxed, target-aware) — comptime malloc/free/memcpy/…
// never reach the host heap or dlsym.
if (try self.callMemBuiltin(name, args, frame)) |r| return r;
// A welded `compiler`-library function (`abi(.zig) extern compiler`):
// the comptime compiler-API, serviced natively on flat memory (Phase 3
// the comptime compiler-API, serviced natively on comptime memory (Phase 3
// seed). The `compiler_welded` flag is the safety boundary.
if (callee.compiler_welded) {
if (try self.callCompilerFn(name, args, frame, ref_types, result_ty)) |r| return r;
@@ -1295,13 +1330,13 @@ pub const Vm = struct {
return std.math.cast(usize, w) orelse self.failMsg("comptime mem builtin: negative/oversized size arg");
}
/// Model a curated set of libc MEMORY builtins directly on flat memory, so a
/// Model a curated set of libc MEMORY builtins directly on comptime memory, so a
/// comptime `malloc`/`free`/`memcpy`/… stays sandboxed (no host heap, no
/// dlsym) and target-aware. Returns the result word, or `null` if `name` is
/// not one of them (the caller then bails to the legacy interpreter). libc
/// `malloc` returns 16-byte-aligned storage; we mirror that. The COMPUTED
/// result is byte-identical to the legacy path (which calls real libc) — only
/// the backing memory differs (flat vs host heap), which the result can't see.
/// the backing memory differs (comptime arena vs host heap), which the result can't see.
fn callMemBuiltin(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame) Error!?Reg {
// Error return-trace runtime (sx_trace.c, linked into the compiler). A
// comptime failable that raises emits `sx_trace_push(trace_frame())` as it
@@ -1357,10 +1392,10 @@ pub const Vm = struct {
return null; // not a modeled builtin → caller bails to legacy
}
/// Service a welded `compiler`-library function natively on flat memory — the
/// Service a welded `compiler`-library function natively on comptime memory — the
/// comptime compiler-API (Phase 3 of `PLAN-COMPILER-VM.md`). Returns the result
/// word, or `null` for an unknown name (caller bails → legacy). Mirrors the
/// legacy `compiler_lib` handlers, but reads/writes flat memory directly instead
/// legacy `compiler_lib` handlers, but reads/writes comptime memory directly instead
/// of marshaling `Value`s. The seed pair is the string-pool round-trip:
/// `intern(s: string) -> StringId` and `text_of(id: StringId) -> string`.
/// Read compiler-call arg `i` as a u32 handle (a `StringId` / `TypeId` word),
@@ -1497,7 +1532,7 @@ pub const Vm = struct {
// ── build-pipeline metadata queries (Phase 5.2) ─────────────────────
// Read-only: the compiler answers them from the `BuildConfig` `main.zig`
// forwards before the post-link callback runs. Each builds a fresh
// `List(string)` in flat memory (the result type drives its layout) — no
// `List(string)` in comptime memory (the result type drives its layout) — no
// driver action, so they're pure data even in the sx-driven end state.
if (std.mem.eql(u8, name, "c_object_paths")) {
if (args.len != 0) return self.failMsg("comptime c_object_paths: expected no args");
@@ -1551,7 +1586,7 @@ pub const Vm = struct {
// genuine ACTION: dispatch to the host-installed linker (the VM can't link
// itself). Void return (the build callback isn't fallible — Phase 5
// decision); a link failure bails loudly → hard build error. `ref_types`
// gives each List(string) arg its concrete type for the flat-memory reader.
// gives each List(string) arg its concrete type for the comptime reader.
if (std.mem.eql(u8, name, "link")) {
if (args.len != 6) return self.failMsg("comptime link: expected (objects, output, libraries, frameworks, flags, target)");
const bc = self.build_config orelse
@@ -1568,12 +1603,132 @@ pub const Vm = struct {
return self.failMsg("comptime link: linking failed");
return @as(Reg, null_addr); // void
}
// ── BuildOptions accessors (Phase 5.5) ──────────────────────────────
// Migrated off `struct #compiler` hooks onto VM-native arms. `self` (the
// opaque BuildOptions handle) is args[0] and ignored; the real state lives
// on the threaded `BuildConfig`. SETTERS dupe the string arg into the
// PERSISTENT `self.gpa` (the Compilation allocator — NOT the per-eval VM
// arena, whose bytes die at `Vm.deinit`) so it survives to post-link.
if (try self.callBuildOptionFn(name, args, frame)) |r| return r;
return null; // not a known compiler function → caller bails to legacy
}
/// Read string arg `idx` (a `{ptr,len}` fat pointer) and DUPE it into the
/// persistent `self.gpa`. The VM-arena view dies at `Vm.deinit`, so a
/// BuildConfig string set at `#run` must own a persistent copy.
fn dupeArgStr(self: *Vm, args: []const Ref, frame: *Frame, idx: usize) Error![]const u8 {
const table = try self.requireTable();
const view = try self.readStringArg(table, frame.get(args[idx].index()));
return self.gpa.dupe(u8, view) catch return self.failMsg("comptime BuildOptions setter: out of memory");
}
/// VM-native `BuildOptions` accessors (Phase 5.5). Returns null when `name` is
/// not a BuildOptions accessor (the caller then yields null → "unknown").
fn callBuildOptionFn(self: *Vm, name: []const u8, args: []const Ref, frame: *Frame) Error!?Reg {
const table = try self.requireTable();
// A getter/setter on a string field: `name` → the `?[]const u8` field. A
// setter (one extra arg) writes a persistent dupe; a getter returns the
// value (or "" when unset). Both ignore the `self` handle at args[0].
const StrField = struct { set: []const u8, get: []const u8, field: *?[]const u8 };
// A BuildOptions accessor is only ever reached from a `#run` / post-link
// eval, which always threads a `BuildConfig`. A null `bc` here means this
// isn't a BuildOptions call at all (e.g. a lowering-time type-fn) — yield
// null so the caller treats it as unknown (it then bails loudly).
const bc = self.build_config orelse return null;
const str_fields = [_]StrField{
.{ .set = "set_output_path", .get = "", .field = &bc.output_path },
.{ .set = "set_wasm_shell", .get = "", .field = &bc.wasm_shell_path },
.{ .set = "set_post_link_module", .get = "", .field = &bc.post_link_module },
.{ .set = "set_bundle_path", .get = "bundle_path", .field = &bc.bundle_path },
.{ .set = "set_bundle_id", .get = "bundle_id", .field = &bc.bundle_id },
.{ .set = "set_codesign_identity", .get = "codesign_identity", .field = &bc.codesign_identity },
.{ .set = "set_provisioning_profile", .get = "provisioning_profile", .field = &bc.provisioning_profile },
.{ .set = "set_manifest_path", .get = "manifest_path", .field = &bc.manifest_path },
.{ .set = "set_keystore_path", .get = "keystore_path", .field = &bc.keystore_path },
.{ .set = "_", .get = "binary_path", .field = &bc.binary_path },
.{ .set = "_", .get = "target_triple", .field = &bc.target_triple },
};
for (str_fields) |sf| {
if (sf.set.len > 1 and std.mem.eql(u8, name, sf.set)) {
if (args.len != 2) return self.failMsg("comptime BuildOptions setter: expected (self, value)");
sf.field.* = try self.dupeArgStr(args, frame, 1);
return @as(Reg, null_addr);
}
if (sf.get.len > 0 and std.mem.eql(u8, name, sf.get)) {
if (args.len != 1) return self.failMsg("comptime BuildOptions getter: expected (self)");
return try self.makeStringValue(table, sf.field.* orelse "");
}
}
// List-appending setters (dupe + append into the persistent gpa).
if (std.mem.eql(u8, name, "add_link_flag")) {
if (args.len != 2) return self.failMsg("comptime add_link_flag: expected (self, flag)");
bc.link_flags.append(self.gpa, try self.dupeArgStr(args, frame, 1)) catch
return self.failMsg("comptime add_link_flag: out of memory");
return @as(Reg, null_addr);
}
if (std.mem.eql(u8, name, "add_framework")) {
if (args.len != 2) return self.failMsg("comptime add_framework: expected (self, name)");
bc.frameworks.append(self.gpa, try self.dupeArgStr(args, frame, 1)) catch
return self.failMsg("comptime add_framework: out of memory");
return @as(Reg, null_addr);
}
if (std.mem.eql(u8, name, "add_asset_dir")) {
if (args.len != 3) return self.failMsg("comptime add_asset_dir: expected (self, src, dest)");
const src = try self.dupeArgStr(args, frame, 1);
const dest = try self.dupeArgStr(args, frame, 2);
bc.asset_dirs.append(self.gpa, .{ .src = src, .dest = dest }) catch
return self.failMsg("comptime add_asset_dir: out of memory");
return @as(Reg, null_addr);
}
// Count getters (i64).
if (std.mem.eql(u8, name, "asset_dir_count"))
return @as(Reg, @bitCast(@as(i64, @intCast(bc.asset_dirs.items.len))));
if (std.mem.eql(u8, name, "framework_count"))
return @as(Reg, @bitCast(@as(i64, @intCast(bc.target_frameworks.len))));
if (std.mem.eql(u8, name, "framework_path_count"))
return @as(Reg, @bitCast(@as(i64, @intCast(bc.target_framework_paths.len))));
if (std.mem.eql(u8, name, "jni_main_count"))
return @as(Reg, @bitCast(@as(i64, @intCast(bc.jni_main_runtime_paths.len))));
// Indexed string getters (out-of-range → "", mirroring the legacy hooks).
// Asset dirs are `{src,dest}` structs, so read the field directly.
if (std.mem.eql(u8, name, "asset_dir_src_at") or std.mem.eql(u8, name, "asset_dir_dest_at")) {
if (args.len != 2) return self.failMsg("comptime asset_dir getter: expected (self, i)");
const idx: i64 = @bitCast(frame.get(args[1].index()));
if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len)
return try self.makeStringValue(table, "");
const ad = bc.asset_dirs.items[@intCast(idx)];
return try self.makeStringValue(table, if (name[10] == 's') ad.src else ad.dest);
}
if (std.mem.eql(u8, name, "framework_at"))
return try self.indexedStr(args, frame, bc.target_frameworks);
if (std.mem.eql(u8, name, "framework_path_at"))
return try self.indexedStr(args, frame, bc.target_framework_paths);
if (std.mem.eql(u8, name, "jni_main_runtime_path_at"))
return try self.indexedStr(args, frame, bc.jni_main_runtime_paths);
if (std.mem.eql(u8, name, "jni_main_java_source_at"))
return try self.indexedStr(args, frame, bc.jni_main_java_sources);
// Target predicates (computed from the triple — mirror the legacy hooks).
if (boolPredicate(name)) |pred| {
if (args.len != 1) return self.failMsg("comptime BuildOptions predicate: expected (self)");
return @as(Reg, if (pred(bc.target_triple)) 1 else 0);
}
return null; // not a BuildOptions accessor
}
/// Read index arg 1, bounds-check against `items`, and return the element
/// string (or "" when out of range — mirrors the legacy hook behavior).
fn indexedStr(self: *Vm, args: []const Ref, frame: *Frame, items: []const []const u8) Error!Reg {
const table = try self.requireTable();
if (args.len != 2) return self.failMsg("comptime BuildOptions indexed getter: expected (self, i)");
const idx: i64 = @bitCast(frame.get(args[1].index()));
if (idx < 0 or @as(usize, @intCast(idx)) >= items.len)
return try self.makeStringValue(table, "");
return try self.makeStringValue(table, items[@intCast(idx)]);
}
/// VM-native `register_type(handle: Type, kind: i64, members: []Member) -> Type`
/// — fill a `declare_type`'d forward slot, branching on `kind` in the compiler
/// (mirrors `compiler_lib.handleRegisterType`, but reads `[]Member` from flat
/// (mirrors `compiler_lib.handleRegisterType`, but reads `[]Member` from comptime
/// memory instead of decoding a `Value`). `Member` is `{ name: string, ty: Type }`.
fn registerTypeVm(self: *Vm, args: []const Ref, frame: *Frame, ref_types: []const TypeId) Error!?Reg {
const table = try self.requireTable();
@@ -1642,7 +1797,7 @@ pub const Vm = struct {
return tbl.internNominal(.{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .i64 } }, 0);
}
/// Decode a `[]{ name: string, ty: Type }` slice from flat memory into interned
/// Decode a `[]{ name: string, ty: Type }` slice from comptime memory into interned
/// `(StringId, TypeId)` pairs — the shared shape of a compiler-API `Member`, a
/// metatype `EnumVariant { name, payload }`, and a `StructField { name, type }`.
/// `slice_ty` (the slice's IR type) gives the element layout (field offsets +
@@ -1671,7 +1826,7 @@ pub const Vm = struct {
}
/// Decode a `[]Type` slice (a metatype `TupleInfo.elements` — POSITIONAL, bare
/// `Type` elements with no name) from flat memory into `TypeId`s.
/// `Type` elements with no name) from comptime memory into `TypeId`s.
fn decodeTypeSlice(self: *Vm, table: *const types.TypeTable, slice_word: Reg, slice_ty: TypeId, out: *std.ArrayList(TypeId)) Error!void {
if (slice_ty.isBuiltin() or table.get(slice_ty) != .slice)
return self.failMsg("comptime define(): tuple elements arg is not a slice");
@@ -1715,7 +1870,7 @@ pub const Vm = struct {
}
/// Service a comptime metatype `#builtin` (`meta.sx`'s `declare`/`define`)
/// natively on flat memory, the VM-native mirror of the legacy
/// natively on comptime memory, the VM-native mirror of the legacy
/// `interp.execBuiltinInner` arms. Returns the result word, or `null` for a
/// builtin the VM doesn't model yet (caller bails → legacy fallback, so dual-path
/// parity holds). Keeps BOTH paths alive during the VM-default transition.
@@ -1777,7 +1932,7 @@ pub const Vm = struct {
},
// type_info($T) → reflect a type INTO a TypeInfo VALUE (the inverse of
// define's decode). The arg folded to a `const_type` (a `.type_value`
// word = the source TypeId); build the value in flat memory.
// word = the source TypeId); build the value in comptime memory.
.type_info => {
const table = try self.requireTable();
if (bi.args.len != 1) return self.failMsg("comptime type_info: expected (Type)");
@@ -1867,7 +2022,7 @@ pub const Vm = struct {
return @as(Reg, handle.index());
}
/// Reflect type `tid` INTO a `TypeInfo` VALUE built in flat memory — the inverse
/// Reflect type `tid` INTO a `TypeInfo` VALUE built in comptime memory — the inverse
/// of `defineFromInfo` and the VM-native mirror of legacy `reflectTypeInfo`. The
/// element/struct layouts come from the `result_ty` (= the metatype `TypeInfo`
/// tagged union): variant tag `t` → payload struct `EnumInfo`/`StructInfo`/
@@ -1969,7 +2124,7 @@ pub const Vm = struct {
// shapes bail loudly (added as wiring surfaces them).
/// Convert a legacy `Value` of type `ty` into a VM `Reg`, materializing
/// aggregates into flat memory (returning their `Addr`).
/// aggregates into comptime memory (returning their `Addr`).
pub fn valueToReg(self: *Vm, table: *const types.TypeTable, value: Value, ty: TypeId) Error!Reg {
switch (kindOf(table, ty)) {
.word => return switch (value) {
@@ -2010,8 +2165,8 @@ pub const Vm = struct {
}
}
/// Convert a VM `Reg` (+ flat memory) of type `ty` back into a legacy `Value`.
/// Strings/aggregates are deep-copied into `alloc` (they must outlive flat memory).
/// Convert a VM `Reg` (+ comptime memory) of type `ty` back into a legacy `Value`.
/// Strings/aggregates are deep-copied into `alloc` (they must outlive comptime memory).
pub fn regToValue(self: *Vm, alloc: std.mem.Allocator, table: *const types.TypeTable, reg: Reg, ty: TypeId) Error!Value {
switch (kindOf(table, ty)) {
.word => {
@@ -2061,7 +2216,7 @@ pub const Vm = struct {
}
/// How a value of type `ty` is held: a register word (scalar/pointer, ≤8
/// bytes) or by-address in flat memory (struct). Anything else is not ported
/// bytes) or by-address in comptime memory (struct). Anything else is not ported
/// yet (slice/string/any/optional/enum/union/array/tuple/vector — sub-step 4+).
const Kind = enum { word, aggregate, unsupported };
@@ -2143,7 +2298,7 @@ pub const Vm = struct {
return (try self.machine.readWord(v + table.typeSizeBytes(child), 1)) != 0;
}
/// Read a value of type `ty` from flat address `addr`: a scalar reads its
/// Read a value of type `ty` from comptime address `addr`: a scalar reads its
/// bytes; an aggregate value IS its address (it lives inline at `addr`).
/// `f32` is special: float REGISTERS hold f64 bits (like the legacy interp's
/// `.float`), but memory holds the 4-byte IEEE-754 single — so read 4 bytes as
@@ -2165,13 +2320,13 @@ pub const Vm = struct {
},
.aggregate => addr,
.unsupported => {
self.detail = "comptime VM: value type not yet supported on flat memory (slice/optional/enum/array/etc.)";
self.detail = "comptime VM: value type not yet supported on comptime memory (slice/optional/enum/array/etc.)";
return error.Unsupported;
},
};
}
/// Write register word `val` (of type `ty`) to flat address `addr`: a scalar
/// Write register word `val` (of type `ty`) to comptime address `addr`: a scalar
/// writes its bytes; an aggregate copies `sizeof(ty)` bytes from `val` (its
/// source address) into `addr`. A `null_addr` aggregate source is the
/// null/none sentinel (a non-pointer `?T` set to `null`, an empty slice/string,
@@ -2197,7 +2352,7 @@ pub const Vm = struct {
}
},
.unsupported => {
self.detail = "comptime VM: value type not yet supported on flat memory (slice/optional/enum/array/etc.)";
self.detail = "comptime VM: value type not yet supported on comptime memory (slice/optional/enum/array/etc.)";
return error.Unsupported;
},
}
@@ -2287,7 +2442,7 @@ pub const Vm = struct {
return data +% idx *% @as(u64, @intCast(elem_size));
}
/// Materialize `text` into flat memory as a `string` VALUE — NUL-terminated
/// Materialize `text` into comptime memory as a `string` VALUE — NUL-terminated
/// bytes + a `{ptr, len}` fat pointer (len excludes the NUL). Shared by
/// `text_of` and `type_info`'s variant/field-name construction.
fn makeStringValue(self: *Vm, table: *const types.TypeTable, text: []const u8) Error!Reg {
@@ -2296,7 +2451,7 @@ pub const Vm = struct {
return try self.makeSlice(table, data, text.len);
}
/// Build a `{ptr, len}` fat pointer (slice/string value) in flat memory and
/// Build a `{ptr, len}` fat pointer (slice/string value) in comptime memory and
/// return its address. `ptr` is `pointer_size` bytes at offset 0; `len` is an
/// i64 at offset 8 (the layout `typeSizeBytes` uses for slice/string: 16B).
fn makeSlice(self: *Vm, table: *const types.TypeTable, data: Addr, len: u64) Error!Addr {
@@ -2316,7 +2471,7 @@ pub const Vm = struct {
return self.machine.readWord(base, table.pointer_size);
}
/// Build a `List(string)` aggregate in flat memory from host strings and
/// Build a `List(string)` aggregate in comptime memory from host strings and
/// return its Addr (the VM's aggregate value IS its address). `list_ty` is
/// the result type of the calling primitive (`List(string)`); its field
/// offsets/types drive the layout (target-aware via the table), so this works
@@ -2348,7 +2503,7 @@ pub const Vm = struct {
}
/// Read a `string` argument (a `{ptr, len}` fat pointer at `val`) as a host
/// `[]const u8`. The bytes are a VIEW into flat memory (Addr is a real host
/// `[]const u8`. The bytes are a VIEW into comptime memory (Addr is a real host
/// pointer over a stable arena), valid for the duration of the call.
fn readStringArg(self: *Vm, table: *const types.TypeTable, val: Reg) Error![]const u8 {
const len: usize = @intCast(try self.sliceLen(val));
@@ -2357,7 +2512,7 @@ pub const Vm = struct {
}
/// Read a `List(string)` aggregate (at `addr`) into a host `[][]const u8` —
/// the inverse of `makeStringList`. Element string bytes are VIEWS into flat
/// the inverse of `makeStringList`. Element string bytes are VIEWS into comptime
/// memory (stable arena); the outer array is gpa-allocated (freed at
/// `Vm.deinit`). Used by the `link` primitive to read its List args.
fn readStringList(self: *Vm, table: *const types.TypeTable, list_ty: TypeId, addr: Addr) Error![]const []const u8 {

View File

@@ -116,7 +116,7 @@ pub const LLVMEmitter = struct {
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`)
// comptime const-init folds try the comptime 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,
@@ -855,6 +855,30 @@ pub const LLVMEmitter = struct {
std.debug.print("help: handle it at the `#run` site — `#run <expr> catch (e) {{ ... }}` or `#run <expr> or <default>`\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.
@@ -880,7 +904,12 @@ pub const LLVMEmitter = struct {
// 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)
// 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;
@@ -891,9 +920,9 @@ pub const LLVMEmitter = struct {
std.debug.print("[comptime-vm] fallback run '{s}': {s}\n", .{ fname, comptime_vm.last_bail_reason orelse "<unknown>" });
}
const result = vm_result orelse fallback: {
// Strict mode: NO fallback — a VM bail is a build-gating error naming
// the reason (the interp-retirement enumeration gate).
if (self.comptime_flat_strict) {
// 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 "<unknown>" });
self.comptime_failed = true;
break :fallback Value.void_val;
@@ -987,7 +1016,11 @@ pub const LLVMEmitter = struct {
// 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)
// 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;
@@ -1002,9 +1035,9 @@ pub const LLVMEmitter = struct {
}
}
const result = vm_result orelse fallback: {
// Strict mode: NO fallback — a VM bail is a build-gating error
// (the interp-retirement enumeration gate).
if (self.comptime_flat_strict) {
// 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 "<unknown>" });
self.comptime_failed = true;

View File

@@ -495,7 +495,7 @@ pub fn runComptimeTypeFunc(self: *Lowering, func_id: FuncId, span: ast.Span) ?Ty
// `getOrCreateThunks` is idempotent (cached in `protocol_thunk_map`), so the
// later Pass-1c call reuses these. Guarded exactly like `emitDefaultContextGlobal`
// (skip when the std allocator types aren't registered). This lets the
// flat-memory VM materialize a REAL lowering-time context (the func-refs are
// comptime VM materialize a REAL lowering-time context (the func-refs are
// dispatchable on the VM via `call_indirect` → thunk → native malloc).
{
const tbl = &self.module.types;
@@ -640,7 +640,7 @@ pub fn evalComptimeString(self: *Lowering, expr: *const Node) ?[:0]const u8 {
// dealloc thunks at the bottom of the dispatch.
const ct_func_id = self.createComptimeFunction("__insert", expr, .string);
// NOTE: the flat-memory VM is intentionally NOT wired at this LOWERING-time
// NOTE: the comptime VM is intentionally NOT wired at this LOWERING-time
// site. Unlike the emit-time const-init / `#run` folds (which run on fully
// lowered IR), lowering-time IR can be malformed (e.g. a `ret Ref.none` left by
// an unresolved name — see `0737`), and routing that through the VM is out of

View File

@@ -39,7 +39,7 @@ pub const TypeId = enum(u32) {
/// (`reflect`/`const_type`/the comptime compiler-API), not a boxed Any. It used
/// to share `.any`'s slot, but `.any` is a 16-byte `{tag,value}` box (variadic
/// any), so a `Type` stored in an aggregate was sized 16B while the value is 8B
/// — which blocked the flat-memory comptime VM. Its own slot fixes the size and
/// — which blocked the comptime VM. Its own slot fixes the size and
/// keeps every downstream `== .any`/`switch` check from conflating the two.
type_value = 19,
_, // user-defined types start at `first_user` (slots 2099 reserved for future builtins)
@@ -512,7 +512,7 @@ pub const TypeTable = struct {
/// member count (a scalar, pointer, the `unresolved` sentinel, …) — so a
/// caller bails loudly rather than reading a silent 0. The comptime
/// compiler-API reflection reader `type_field_count` rides on this (both the
/// legacy `compiler_lib` handler and the flat-memory VM call it, so the two
/// legacy `compiler_lib` handler and the comptime VM call it, so the two
/// paths can never drift). Out-of-range ids return null, not a panic.
pub fn memberCount(self: *const TypeTable, id: TypeId) ?i64 {
if (id.index() >= self.infos.items.len) return null;