|
|
|
|
@@ -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 {
|
|
|
|
|
|