TypeInfo gains a `tuple(TupleInfo) variant (TupleInfo{elements: []Type},
positional/unnamed) — completing the reflect/construct triad with enum
and struct.
- meta.sx: TupleInfo + `tuple TypeInfo variant.
- interp: reflectTypeInfo builds .tuple (tag 2) as bare type_tag elements
(no name pairs); defineType dispatches tag 2 -> defineTuple, which
decodes []Type and completes the declare slot as a structural .tuple
via replaceKeyedInfo (kind change). Tuples are structural so the
declared name is vestigial, but the slot is still completed in place so
define returns the handle (consistent with enum/struct).
- call.zig: the lower-time type_info guard now admits .tuple.
define(declare("P"), .tuple(.{elements=.[i64,f64]})) builds a tuple, and
define(declare("T"), type_info((i64,bool,f64))) round-trips one. Suite
green (683).
2372 lines
122 KiB
Zig
2372 lines
122 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const types = @import("types.zig");
|
|
const inst_mod = @import("inst.zig");
|
|
const mod_mod = @import("module.zig");
|
|
const errors = @import("../errors.zig");
|
|
|
|
const TypeId = types.TypeId;
|
|
const TypeTable = types.TypeTable;
|
|
const StringId = types.StringId;
|
|
const Ref = inst_mod.Ref;
|
|
const BlockId = inst_mod.BlockId;
|
|
const FuncId = inst_mod.FuncId;
|
|
const Inst = inst_mod.Inst;
|
|
const Op = inst_mod.Op;
|
|
const Function = inst_mod.Function;
|
|
const Block = inst_mod.Block;
|
|
const Module = mod_mod.Module;
|
|
const Builder = mod_mod.Builder;
|
|
|
|
// ── Value ───────────────────────────────────────────────────────────────
|
|
|
|
pub const Value = union(enum) {
|
|
int: i64,
|
|
float: f64,
|
|
boolean: bool,
|
|
string: []const u8,
|
|
null_val,
|
|
void_val,
|
|
undef,
|
|
aggregate: []const Value,
|
|
slot_ptr: u32, // index into the frame's local slots
|
|
func_ref: FuncId,
|
|
closure: ClosureVal,
|
|
type_tag: TypeId,
|
|
heap_ptr: HeapPtr, // pointer into heap-allocated memory
|
|
/// Byte-granular raw pointer. Produced by `index_gep` on a string /
|
|
/// `[*]u8` aggregate whose data field is itself a raw integer pointer
|
|
/// (e.g. from libc_malloc). Store/load through this variant operate
|
|
/// on a single byte — matching the heap_ptr semantics for the same
|
|
/// op shape.
|
|
byte_ptr: usize,
|
|
|
|
pub const ClosureVal = struct {
|
|
func: FuncId,
|
|
env: ?[]const Value,
|
|
};
|
|
|
|
/// A pointer to heap-allocated memory, with an optional byte offset.
|
|
pub const HeapPtr = struct {
|
|
id: u32, // index into Interpreter.heap
|
|
offset: u32 = 0,
|
|
};
|
|
|
|
pub fn asInt(self: Value) ?i64 {
|
|
return switch (self) {
|
|
.int => |v| v,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
pub fn asFloat(self: Value) ?f64 {
|
|
return switch (self) {
|
|
.float => |v| v,
|
|
.int => |v| @floatFromInt(v), // implicit int→float for convenience
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
pub fn asBool(self: Value) ?bool {
|
|
return switch (self) {
|
|
.boolean => |v| v,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
pub fn isNull(self: Value) bool {
|
|
return self == .null_val;
|
|
}
|
|
|
|
/// Extract the TypeId from a first-class Type value. Returns null
|
|
/// for anything else — including `.int(N)` where N happens to be
|
|
/// a valid TypeId enum value. The kinds are distinct: a Type IS
|
|
/// NOT an int. Use this helper instead of `asInt` when reading a
|
|
/// TypeId out of a Value to keep the kind-distinction honest.
|
|
pub fn asTypeId(self: Value) ?TypeId {
|
|
return switch (self) {
|
|
.type_tag => |id| id,
|
|
else => null,
|
|
};
|
|
}
|
|
|
|
/// Resolve the `TypeId` a dynamic `type_name` / `type_is_unsigned` must
|
|
/// operate on, honoring the rule that a reflection builtin reads an
|
|
/// `Any`'s runtime TYPE-TAG, never its raw payload:
|
|
/// - a native `.type_tag(tid)` Value → `tid` (a first-class Type value).
|
|
/// - an `Any` aggregate `{ tag, value }`: when the tag is `.any`, the
|
|
/// box carries a *Type value* (the `box_any(.., .any)` / `const_type`
|
|
/// shape) → the TypeId is the payload; otherwise the box carries a
|
|
/// *runtime value* whose type IS the tag → the tag is the TypeId. This
|
|
/// makes `type_name(av)` for `av : Any = 6` report `i64` (the held
|
|
/// value's type) while `type_name(type_of(x))` still names the type.
|
|
/// Returns null when `self` is neither shape (the caller bails loudly).
|
|
pub fn reflectTypeId(self: Value) ?TypeId {
|
|
if (self.asTypeId()) |t| return t;
|
|
if (self == .aggregate) {
|
|
const fields = self.aggregate;
|
|
if (fields.len >= 2) {
|
|
const tag = fields[0].asInt() orelse return null;
|
|
if (tag == @as(i64, @intCast(TypeId.any.index()))) {
|
|
if (fields[1].asTypeId()) |t| return t;
|
|
if (fields[1].asInt()) |iv| return TypeId.fromIndex(@intCast(iv));
|
|
return null;
|
|
}
|
|
return TypeId.fromIndex(@intCast(tag));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Get the string content, whether from a literal or a heap-backed string aggregate.
|
|
pub fn asString(self: Value, interp: *const Interpreter) ?[]const u8 {
|
|
return switch (self) {
|
|
.string => |s| s,
|
|
.aggregate => |fields| {
|
|
// String fat pointer: { heap_ptr/string/raw_int_ptr, int(len) }
|
|
if (fields.len == 2) {
|
|
const len: usize = @intCast(fields[1].asInt() orelse return null);
|
|
switch (fields[0]) {
|
|
.heap_ptr => |hp| {
|
|
const mem = interp.heapSlice(hp) orelse return null;
|
|
return if (len <= mem.len) mem[0..len] else null;
|
|
},
|
|
.string => |s| return if (len <= s.len) s[0..len] else s,
|
|
// Raw host pointer (e.g. from CAllocator.alloc →
|
|
// libc_malloc). Read `len` bytes back from real
|
|
// memory.
|
|
.int => |addr| {
|
|
const p: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(addr)));
|
|
return p[0..len];
|
|
},
|
|
else => return null,
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
else => null,
|
|
};
|
|
}
|
|
};
|
|
|
|
// ── Error ───────────────────────────────────────────────────────────────
|
|
|
|
pub const InterpError = error{
|
|
CannotEvalComptime,
|
|
TypeError,
|
|
OutOfBounds,
|
|
DivisionByZero,
|
|
StackOverflow,
|
|
Unreachable,
|
|
};
|
|
|
|
const compiler_hooks = @import("compiler_hooks.zig");
|
|
pub const BuildConfig = compiler_hooks.BuildConfig;
|
|
const host_ffi = @import("host_ffi.zig");
|
|
|
|
// ── Interpreter ─────────────────────────────────────────────────────────
|
|
|
|
pub const Interpreter = struct {
|
|
module: *const Module,
|
|
alloc: Allocator,
|
|
output: std.ArrayList(u8),
|
|
call_depth: u32 = 0,
|
|
max_call_depth: u32 = 256,
|
|
/// Active sx call-frame chain (oldest→newest), maintained across `call` for
|
|
/// `trace.print_interpreter_frames()` (ERR E4.1). Only sx-bodied frames are
|
|
/// tracked — extern calls return before the frame is pushed.
|
|
call_chain: std.ArrayList(FuncId) = .empty,
|
|
|
|
/// File → source text (the diagnostics' import_sources). Set by the host
|
|
/// where available so `.trace_resolve` can turn a `(func_id, span.start)`
|
|
/// frame into `file:line:col` at comptime (ERR E3.0 slice 3b). Null → the
|
|
/// resolver degrades to line/col 1:1.
|
|
source_map: ?*const std.StringHashMap([:0]const u8) = null,
|
|
|
|
/// Comptime type-MINT target — the SAME `TypeTable` the host (`Lowering`)
|
|
/// owns (aliases `self.module.types`; the const view here and the host's
|
|
/// mutable view point at one table). Set by the host before a comptime-eval
|
|
/// that may run `declare`/`define`. Null elsewhere (unit tests, emit-time
|
|
/// `#run`) → those builtins bail loudly.
|
|
mint: ?*types.TypeTable = null,
|
|
|
|
// Heap: dynamically allocated memory blocks
|
|
heap: std.ArrayList([]u8),
|
|
|
|
// Global values: evaluated comptime globals, indexed by GlobalId
|
|
global_values: std.AutoHashMap(u32, Value),
|
|
|
|
// Mutable build configuration — set by LLVMEmitter, written by #run blocks
|
|
build_config: ?*BuildConfig = null,
|
|
|
|
// Compiler hook registry for #compiler methods
|
|
hooks: compiler_hooks.Registry,
|
|
|
|
// First op tag that bailed with InterpError, captured the first
|
|
// time the interpreter unwinds so callers can surface "op=foo at
|
|
// <file>:<offset>" alongside the bare error name. Static so it
|
|
// survives Interpreter teardown (lifetime: program global).
|
|
pub var last_bail_op: ?[]const u8 = null;
|
|
pub var last_bail_file: ?[]const u8 = null;
|
|
pub var last_bail_offset: u32 = 0;
|
|
pub var last_bail_builtin: ?[]const u8 = null;
|
|
/// Free-text explanation of WHY the bail happened — set at sites
|
|
/// that currently can't handle a specific Value/op combination
|
|
/// (raw-pointer loads, struct_gep through `*void`, etc.). The host
|
|
/// diagnostic renderer surfaces this so users see "load through
|
|
/// raw pointer not supported" instead of a bare `CannotEvalComptime`.
|
|
pub var last_bail_detail: ?[]const u8 = null;
|
|
|
|
/// Set `last_bail_detail` to a static message and return the error.
|
|
/// Use at sites where a specific raw-pointer Value tag isn't handled
|
|
/// so users get a clear explanation instead of guessing.
|
|
fn bailDetail(comptime msg: []const u8) InterpError {
|
|
if (last_bail_detail == null) last_bail_detail = msg;
|
|
return error.CannotEvalComptime;
|
|
}
|
|
|
|
/// Like `bailDetail` but returns a `TypeError` — for extern-arg
|
|
/// marshalling sites that previously erased the reason.
|
|
fn typeErrorDetail(comptime msg: []const u8) InterpError {
|
|
if (last_bail_detail == null) last_bail_detail = msg;
|
|
return error.TypeError;
|
|
}
|
|
|
|
pub fn init(module: *const Module, alloc: Allocator) Interpreter {
|
|
var hooks = compiler_hooks.Registry.init(alloc);
|
|
hooks.registerDefaults();
|
|
return .{
|
|
.module = module,
|
|
.alloc = alloc,
|
|
.output = std.ArrayList(u8).empty,
|
|
.heap = std.ArrayList([]u8).empty,
|
|
.global_values = std.AutoHashMap(u32, Value).init(alloc),
|
|
.hooks = hooks,
|
|
};
|
|
}
|
|
|
|
/// Provide the file→source map so `.trace_resolve` can compute file:line:col
|
|
/// for comptime trace frames. Optional — absent in unit tests.
|
|
pub fn setSourceMap(self: *Interpreter, sm: *const std.StringHashMap([:0]const u8)) void {
|
|
self.source_map = sm;
|
|
}
|
|
|
|
/// Enable the comptime type-construction builtins (`declare`/`define`) by
|
|
/// handing the interp the host's mutable `TypeTable`. Called by `Lowering`
|
|
/// before a comptime-eval that may mint types.
|
|
pub fn setMintTable(self: *Interpreter, tbl: *types.TypeTable) void {
|
|
self.mint = tbl;
|
|
}
|
|
|
|
pub fn deinit(self: *Interpreter) void {
|
|
// Free all heap allocations
|
|
for (self.heap.items) |block| {
|
|
self.alloc.free(block);
|
|
}
|
|
self.heap.deinit(self.alloc);
|
|
self.output.deinit(self.alloc);
|
|
self.call_chain.deinit(self.alloc);
|
|
self.global_values.deinit();
|
|
self.hooks.deinit();
|
|
}
|
|
|
|
/// Write `val` to the raw host address `addr` using exactly the
|
|
/// number of bytes declared by `val_ty`. Used when the
|
|
/// protocol-dispatch chain bottoms out at a extern-libc-malloc
|
|
/// pointer and sx code stores through it. Comptime safety is the
|
|
/// caller's responsibility — wild writes will fault.
|
|
fn storeAtRawPtr(self: *Interpreter, addr: i64, val: Value, val_ty: @import("types.zig").TypeId) InterpError!void {
|
|
const dst: [*]u8 = @ptrFromInt(@as(usize, @bitCast(addr)));
|
|
const width = self.module.types.typeSizeBytes(val_ty);
|
|
switch (val) {
|
|
.int => |v| {
|
|
// Width is whatever the declared IR type says (i8..i64,
|
|
// u8..u64, usize, pointer-as-int, bool-after-extension).
|
|
// The Value tag itself is .int regardless.
|
|
if (width == 0 or width > 8) return bailDetail("comptime store of int through raw pointer: unexpected declared width (expected 1..8 bytes)");
|
|
const bytes = std.mem.toBytes(v);
|
|
@memcpy(dst[0..width], bytes[0..width]);
|
|
},
|
|
.float => |v| {
|
|
switch (width) {
|
|
8 => {
|
|
const bytes = std.mem.toBytes(v);
|
|
@memcpy(dst[0..8], &bytes);
|
|
},
|
|
4 => {
|
|
const f32_v: f32 = @floatCast(v);
|
|
const bytes = std.mem.toBytes(f32_v);
|
|
@memcpy(dst[0..4], &bytes);
|
|
},
|
|
else => return bailDetail("comptime store of float through raw pointer: unexpected declared width (expected 4 or 8 bytes)"),
|
|
}
|
|
},
|
|
.boolean => |v| {
|
|
if (width == 0) return bailDetail("comptime store of bool through raw pointer: zero-width destination");
|
|
@memset(dst[0..width], 0);
|
|
dst[0] = if (v) 1 else 0;
|
|
},
|
|
.null_val => {
|
|
if (width == 0 or width > 8) return bailDetail("comptime store of null through raw pointer: unexpected declared width");
|
|
@memset(dst[0..width], 0);
|
|
},
|
|
.aggregate => return bailDetail("comptime store of aggregate through raw pointer not supported (struct field layout not threaded into Store IR op)"),
|
|
.heap_ptr => return bailDetail("comptime store of interp-heap pointer through raw pointer not supported"),
|
|
.byte_ptr => return bailDetail("comptime store of byte pointer through raw pointer not supported"),
|
|
.slot_ptr => return bailDetail("comptime store of slot pointer through raw pointer not supported (frame-local slot indices aren't meaningful as memory contents)"),
|
|
.func_ref => return bailDetail("comptime store of func_ref through raw pointer not supported"),
|
|
.closure => return bailDetail("comptime store of closure value through raw pointer not supported"),
|
|
.string, .type_tag, .void_val, .undef => return bailDetail("comptime store: unsupported Value kind at raw destination"),
|
|
}
|
|
}
|
|
|
|
// ── Implicit Context ──────────────────────────────────────────
|
|
|
|
/// Build the default Context aggregate for top-level interp calls.
|
|
/// Mirrors the static `__sx_default_context` LLVM global: a Context
|
|
/// whose `allocator` field is the stateless CAllocator inline-protocol
|
|
/// value (alloc/dealloc thunks bottom out at libc malloc/free).
|
|
fn defaultContextValue(self: *Interpreter) Value {
|
|
const tbl_ptr: *const @import("types.zig").TypeTable = &self.module.types;
|
|
const tbl = @constCast(tbl_ptr);
|
|
const alloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_alloc_bytes");
|
|
const dealloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_dealloc_bytes");
|
|
|
|
var alloc_fn: Value = .null_val;
|
|
var dealloc_fn: Value = .null_val;
|
|
for (self.module.functions.items, 0..) |func, i| {
|
|
if (func.name == alloc_thunk_name) alloc_fn = .{ .func_ref = FuncId.fromIndex(@intCast(i)) };
|
|
if (func.name == dealloc_thunk_name) dealloc_fn = .{ .func_ref = FuncId.fromIndex(@intCast(i)) };
|
|
}
|
|
|
|
const allocator_fields = self.alloc.alloc(Value, 3) catch unreachable;
|
|
allocator_fields[0] = .null_val; // CAllocator receiver — stateless
|
|
allocator_fields[1] = alloc_fn;
|
|
allocator_fields[2] = dealloc_fn;
|
|
const allocator_val: Value = .{ .aggregate = allocator_fields };
|
|
|
|
const ctx_fields = self.alloc.alloc(Value, 2) catch unreachable;
|
|
ctx_fields[0] = allocator_val;
|
|
ctx_fields[1] = .null_val;
|
|
return .{ .aggregate = ctx_fields };
|
|
}
|
|
|
|
// ── Heap operations ────────────────────────────────────────────
|
|
|
|
fn heapAlloc(self: *Interpreter, size: usize) Value.HeapPtr {
|
|
const mem = self.alloc.alloc(u8, size) catch unreachable;
|
|
@memset(mem, 0);
|
|
const id: u32 = @intCast(self.heap.items.len);
|
|
self.heap.append(self.alloc, mem) catch unreachable;
|
|
return .{ .id = id };
|
|
}
|
|
|
|
fn heapFree(self: *Interpreter, hp: Value.HeapPtr) void {
|
|
if (hp.id < self.heap.items.len) {
|
|
self.alloc.free(self.heap.items[hp.id]);
|
|
self.heap.items[hp.id] = &.{};
|
|
}
|
|
}
|
|
|
|
pub fn heapSlice(self: *const Interpreter, hp: Value.HeapPtr) ?[]u8 {
|
|
if (hp.id >= self.heap.items.len) return null;
|
|
const mem = self.heap.items[hp.id];
|
|
if (hp.offset >= mem.len) return null;
|
|
return mem[hp.offset..];
|
|
}
|
|
|
|
fn heapMemcpy(self: *Interpreter, dst: Value.HeapPtr, src_bytes: []const u8, len: usize) void {
|
|
const dst_mem = self.heapSlice(dst) orelse return;
|
|
const copy_len = @min(len, @min(dst_mem.len, src_bytes.len));
|
|
@memcpy(dst_mem[0..copy_len], src_bytes[0..copy_len]);
|
|
}
|
|
|
|
fn heapMemset(self: *Interpreter, dst: Value.HeapPtr, val: u8, len: usize) void {
|
|
const dst_mem = self.heapSlice(dst) orelse return;
|
|
const set_len = @min(len, dst_mem.len);
|
|
@memset(dst_mem[0..set_len], val);
|
|
}
|
|
|
|
fn heapStoreByte(self: *Interpreter, dst: Value.HeapPtr, val: u8) void {
|
|
const mem = self.heapSlice(dst) orelse return;
|
|
if (mem.len > 0) mem[0] = val;
|
|
}
|
|
|
|
/// Look up a global value, lazy-evaluating its comptime_func if needed.
|
|
fn getGlobal(self: *Interpreter, gid: inst_mod.GlobalId) InterpError!Value {
|
|
const idx = gid.index();
|
|
// Check cache first
|
|
if (self.global_values.get(idx)) |v| return v;
|
|
|
|
// Not cached — evaluate from global definition
|
|
const global = &self.module.globals.items[idx];
|
|
if (global.comptime_func) |func_id| {
|
|
const result = try self.call(func_id, &.{});
|
|
self.global_values.put(idx, result) catch {};
|
|
return result;
|
|
}
|
|
// Static init value
|
|
if (global.init_val) |iv| {
|
|
const val: Value = self.constToValue(iv);
|
|
self.global_values.put(idx, val) catch {};
|
|
return val;
|
|
}
|
|
return .undef;
|
|
}
|
|
|
|
/// Marshal a single sx Value into a `usize` slot for a cdecl host call.
|
|
/// Strings are made null-terminated; pointer-like values pass their
|
|
/// underlying address. The returned `usize` is only valid for the
|
|
/// duration of this call — caller-allocated buffers are tracked in
|
|
/// `tmp` so they get freed once the call returns.
|
|
fn marshalExternArg(self: *Interpreter, v: Value, tmp: *std.ArrayList([]u8)) !usize {
|
|
return switch (v) {
|
|
.int => |i| @bitCast(i),
|
|
.boolean => |b| @intFromBool(b),
|
|
.null_val => 0,
|
|
.byte_ptr => |addr| addr,
|
|
.heap_ptr => |hp| blk: {
|
|
// `heapSlice` returns the slice already advanced by `hp.offset`,
|
|
// so its `.ptr` IS the offset address. Adding `hp.offset` again
|
|
// double-counts and lands the extern call past the buffer end.
|
|
_ = self.heapSlice(hp) orelse return error.TypeError;
|
|
break :blk @intFromPtr(self.heap.items[hp.id].ptr) + hp.offset;
|
|
},
|
|
.string => |s| blk: {
|
|
const buf = try self.alloc.alloc(u8, s.len + 1);
|
|
@memcpy(buf[0..s.len], s);
|
|
buf[s.len] = 0;
|
|
tmp.append(self.alloc, buf) catch return error.TypeError;
|
|
break :blk @intFromPtr(buf.ptr);
|
|
},
|
|
.aggregate => |fields| blk: {
|
|
// Fat string pointer: { ptr, len }. Pass the raw bytes
|
|
// null-terminated so libc string APIs work.
|
|
if (fields.len == 2) {
|
|
const len: usize = @intCast(fields[1].asInt() orelse return error.TypeError);
|
|
switch (fields[0]) {
|
|
.heap_ptr => |hp| {
|
|
const mem = self.heapSlice(hp) orelse return error.TypeError;
|
|
const start = hp.offset;
|
|
const slice = mem[start .. start + len];
|
|
const buf = try self.alloc.alloc(u8, len + 1);
|
|
@memcpy(buf[0..len], slice);
|
|
buf[len] = 0;
|
|
tmp.append(self.alloc, buf) catch return error.TypeError;
|
|
break :blk @intFromPtr(buf.ptr);
|
|
},
|
|
.string => |s| {
|
|
const slice = if (len <= s.len) s[0..len] else s;
|
|
const buf = try self.alloc.alloc(u8, slice.len + 1);
|
|
@memcpy(buf[0..slice.len], slice);
|
|
buf[slice.len] = 0;
|
|
tmp.append(self.alloc, buf) catch return error.TypeError;
|
|
break :blk @intFromPtr(buf.ptr);
|
|
},
|
|
// Raw host pointer (from libc_malloc-backed
|
|
// cstring). Read bytes from real memory and copy
|
|
// into a null-terminated buffer the extern call
|
|
// can consume.
|
|
.int => |addr| {
|
|
const src: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(addr)));
|
|
const buf = try self.alloc.alloc(u8, len + 1);
|
|
@memcpy(buf[0..len], src[0..len]);
|
|
buf[len] = 0;
|
|
tmp.append(self.alloc, buf) catch return error.TypeError;
|
|
break :blk @intFromPtr(buf.ptr);
|
|
},
|
|
else => return typeErrorDetail("comptime extern call: unsupported aggregate data-field kind (expected heap_ptr/string/int)"),
|
|
}
|
|
}
|
|
return typeErrorDetail("comptime extern call: aggregate arg must be a {ptr, len} fat-pointer pair");
|
|
},
|
|
else => return typeErrorDetail("comptime extern call: unsupported arg Value kind"),
|
|
};
|
|
}
|
|
|
|
/// Append the current sx call-frame chain to the interp output, most-recent
|
|
/// last (ERR E4.1). The topmost frame is `print_interpreter_frames` itself
|
|
/// (the dump site), so it's skipped. Frame source locations await IR-offset
|
|
/// resolution (the comptime analog of DWARF), so only function names print.
|
|
fn printInterpFrames(self: *Interpreter) void {
|
|
const n = self.call_chain.items.len;
|
|
if (n <= 1) return;
|
|
self.output.appendSlice(self.alloc, "comptime call frames (most recent call last):\n") catch {};
|
|
var i: usize = 0;
|
|
while (i < n - 1) : (i += 1) {
|
|
const fid = self.call_chain.items[i];
|
|
const fname = self.module.types.getString(self.module.getFunction(fid).name);
|
|
const line = std.fmt.allocPrint(self.alloc, " at {s}\n", .{fname}) catch continue;
|
|
defer self.alloc.free(line);
|
|
self.output.appendSlice(self.alloc, line) catch {};
|
|
}
|
|
}
|
|
|
|
fn callExtern(self: *Interpreter, func: *const inst_mod.Function, args: []const Value) InterpError!Value {
|
|
const name = self.module.types.getString(func.name);
|
|
|
|
// A extern call may not return (e.g. `process.exit` → `_exit`), which
|
|
// would discard the interpreter's buffered `print` output (otherwise
|
|
// flushed only after `#run` completes). Flush it first so comptime
|
|
// diagnostics emitted just before a terminating call survive.
|
|
if (self.output.items.len > 0) {
|
|
_ = std.c.write(1, self.output.items.ptr, self.output.items.len);
|
|
self.output.clearRetainingCapacity();
|
|
}
|
|
const symbol = (host_ffi.lookupSymbol(self.alloc, name) catch return bailDetail("comptime extern call: dlsym error looking up symbol")) orelse {
|
|
if (last_bail_detail == null) last_bail_detail = "comptime extern call: symbol not found via dlsym (target-specific binding called at compile time?)";
|
|
return error.CannotEvalComptime;
|
|
};
|
|
|
|
var packed_args: [8]usize = undefined;
|
|
if (args.len > packed_args.len) return bailDetail("comptime extern call: more than 8 args (host_ffi trampolines max out at 8)");
|
|
|
|
var tmp = std.ArrayList([]u8).empty;
|
|
defer {
|
|
for (tmp.items) |buf| self.alloc.free(buf);
|
|
tmp.deinit(self.alloc);
|
|
}
|
|
for (args, 0..) |a, i| {
|
|
packed_args[i] = self.marshalExternArg(a, &tmp) catch return error.TypeError;
|
|
}
|
|
const argv = packed_args[0..args.len];
|
|
|
|
// Variadic extern functions (declared `args: ..T`) must be
|
|
// dispatched through C-variadic trampolines so the trailing
|
|
// args land in the right place per the target's variadic
|
|
// ABI. The fixed-arity trampolines would put them in arg
|
|
// registers, and the callee would read garbage from the
|
|
// stack.
|
|
const fixed = func.params.len;
|
|
const variadic = func.is_variadic and args.len > fixed;
|
|
|
|
const ret = func.ret;
|
|
if (ret == .void) {
|
|
if (variadic) {
|
|
host_ffi.callVoidRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime;
|
|
} else {
|
|
host_ffi.callVoidRet(symbol, argv) catch return error.CannotEvalComptime;
|
|
}
|
|
return .void_val;
|
|
}
|
|
if (ret == .i8 or ret == .i16 or ret == .i32 or ret == .i64 or
|
|
ret == .u8 or ret == .u16 or ret == .u32 or ret == .u64 or
|
|
ret == .usize or ret == .isize)
|
|
{
|
|
const r = if (variadic)
|
|
host_ffi.callIntRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime
|
|
else
|
|
host_ffi.callIntRet(symbol, argv) catch return error.CannotEvalComptime;
|
|
return Value{ .int = r };
|
|
}
|
|
if (ret == .bool) {
|
|
const r = if (variadic)
|
|
host_ffi.callIntRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime
|
|
else
|
|
host_ffi.callIntRet(symbol, argv) catch return error.CannotEvalComptime;
|
|
return Value{ .boolean = r != 0 };
|
|
}
|
|
const r = if (variadic)
|
|
host_ffi.callPtrRetVar(symbol, fixed, argv) catch return error.CannotEvalComptime
|
|
else
|
|
host_ffi.callPtrRet(symbol, argv) catch return error.CannotEvalComptime;
|
|
return Value{ .int = @bitCast(@as(u64, r)) };
|
|
}
|
|
|
|
pub fn call(self: *Interpreter, func_id: FuncId, args: []const Value) InterpError!Value {
|
|
if (self.call_depth >= self.max_call_depth) return error.StackOverflow;
|
|
self.call_depth += 1;
|
|
defer self.call_depth -= 1;
|
|
|
|
const func = self.module.getFunction(func_id);
|
|
if (func.is_extern or func.blocks.items.len == 0) {
|
|
// Dispatch to host libc via dlsym. Lets `#run` (and the
|
|
// post-link bundler) call ordinary extern symbols like
|
|
// `puts`, `getenv`, `posix_spawn`, etc.
|
|
return self.callExtern(func, args);
|
|
}
|
|
|
|
// Track the sx call chain for `trace.print_interpreter_frames()`.
|
|
self.call_chain.append(self.alloc, func_id) catch {};
|
|
defer _ = self.call_chain.pop();
|
|
|
|
// Compute total refs: params + all instructions across all blocks
|
|
var total_refs: u32 = @intCast(func.params.len);
|
|
for (func.blocks.items) |blk| {
|
|
total_refs += @intCast(blk.insts.items.len);
|
|
}
|
|
|
|
var frame = Frame.initSized(self.alloc, total_refs);
|
|
defer frame.deinit();
|
|
|
|
// Implicit-context bootstrap: when an entry point with implicit
|
|
// ctx is called without an explicit ctx arg, materialise the
|
|
// default context in a fresh slot and bind slot_ptr(0) to ref 0.
|
|
// This is the interp-side equivalent of FFI-inbound wrappers
|
|
// installing `&__sx_default_context` at function entry.
|
|
var skip_first: u32 = 0;
|
|
if (func.has_implicit_ctx and args.len + 1 == func.params.len) {
|
|
const ctx_val = self.defaultContextValue();
|
|
const slot = frame.allocSlot(self.alloc);
|
|
frame.storeSlot(slot, ctx_val);
|
|
frame.setRef(0, .{ .slot_ptr = slot });
|
|
skip_first = 1;
|
|
}
|
|
|
|
// Bind parameters as initial refs (indices skip_first..N-1)
|
|
for (args, 0..) |arg, i| {
|
|
frame.setRef(@intCast(i + skip_first), arg);
|
|
}
|
|
|
|
// Start at the entry block (index 0)
|
|
var current_block: BlockId = BlockId.fromIndex(0);
|
|
var block_args: []const Value = &.{};
|
|
|
|
while (true) {
|
|
const block_idx = current_block.index();
|
|
const block = &func.blocks.items[block_idx];
|
|
var ref_counter: u32 = block.first_ref;
|
|
|
|
// Bind block params (block_param instructions handle this, but we
|
|
// also need to pre-set the values for them)
|
|
for (block_args) |_| {
|
|
// block_param instructions will read from frame refs when executed
|
|
// The block_param instruction itself produces the value
|
|
}
|
|
|
|
for (block.insts.items) |*instruction| {
|
|
// Special handling for block_param: bind the arg value
|
|
if (instruction.op == .block_param) {
|
|
const bp = instruction.op.block_param;
|
|
if (bp.param_index < block_args.len) {
|
|
frame.setRef(ref_counter, block_args[bp.param_index]);
|
|
}
|
|
ref_counter += 1;
|
|
continue;
|
|
}
|
|
|
|
const result = self.execInst(instruction, &frame, ¤t_block, &block_args) catch |err| {
|
|
if (last_bail_op == null) {
|
|
last_bail_op = @tagName(instruction.op);
|
|
last_bail_file = func.source_file;
|
|
last_bail_offset = instruction.span.start;
|
|
}
|
|
return err;
|
|
};
|
|
switch (result) {
|
|
.value => |val| {
|
|
frame.setRef(ref_counter, val);
|
|
ref_counter += 1;
|
|
},
|
|
.branch => {
|
|
ref_counter += 1; // terminator consumes a ref slot
|
|
break;
|
|
},
|
|
.ret_val => |val| return val,
|
|
.ret_nothing => return .void_val,
|
|
}
|
|
} else {
|
|
// Fell through the block with no terminator — treat as implicit return void
|
|
return .void_val;
|
|
}
|
|
}
|
|
}
|
|
|
|
const ExecResult = union(enum) {
|
|
value: Value,
|
|
branch,
|
|
ret_val: Value,
|
|
ret_nothing,
|
|
};
|
|
|
|
fn execInst(self: *Interpreter, instruction: *const Inst, frame: *Frame, current_block: *BlockId, block_args: *[]const Value) InterpError!ExecResult {
|
|
const op = instruction.op;
|
|
|
|
switch (op) {
|
|
// ── Constants ───────────────────────────────────────
|
|
.const_int => |v| return .{ .value = .{ .int = v } },
|
|
.const_float => |v| return .{ .value = .{ .float = v } },
|
|
.const_bool => |v| return .{ .value = .{ .boolean = v } },
|
|
.const_string => |sid| return .{ .value = .{ .string = self.module.types.getString(sid) } },
|
|
.const_null => return .{ .value = .null_val },
|
|
.const_undef => return .{ .value = .undef },
|
|
.is_comptime => return .{ .value = .{ .boolean = true } },
|
|
.interp_print_frames => {
|
|
self.printInterpFrames();
|
|
return .{ .value = .void_val };
|
|
},
|
|
.trace_frame => {
|
|
// Comptime frame: pack (func_id, span.start) so the slice-3b
|
|
// resolver can recover file:line:col via the IR/source tables.
|
|
// The interp never produces a `Frame*` — only the compiled
|
|
// backend does — so this stays a packed id, never a pointer.
|
|
const fid: u64 = if (self.call_chain.items.len > 0)
|
|
self.call_chain.items[self.call_chain.items.len - 1].index()
|
|
else
|
|
0;
|
|
const packed_frame: u64 = (fid << 32) | @as(u64, instruction.span.start);
|
|
return .{ .value = .{ .int = @bitCast(packed_frame) } };
|
|
},
|
|
.trace_resolve => |u| {
|
|
// Unpack the comptime frame `(func_id << 32 | span.start)` and
|
|
// resolve it to a `Frame { file, line, col, func }` aggregate.
|
|
const raw: u64 = @bitCast(frame.getRef(u.operand).asInt() orelse 0);
|
|
const fid: u32 = @intCast(raw >> 32);
|
|
const offset: u32 = @truncate(raw);
|
|
const func = self.module.getFunction(FuncId.fromIndex(fid));
|
|
const func_name = self.module.types.getString(func.name);
|
|
const file_full = func.source_file orelse "";
|
|
const file = std.fs.path.basename(file_full);
|
|
var line: i64 = 1;
|
|
var col: i64 = 1;
|
|
var line_text: []const u8 = "";
|
|
if (self.source_map) |sm| {
|
|
if (sm.get(file_full)) |src| {
|
|
const loc = errors.SourceLoc.compute(src, offset);
|
|
line = @intCast(loc.line);
|
|
col = @intCast(loc.col);
|
|
line_text = errors.lineAt(src, offset);
|
|
}
|
|
}
|
|
const fields = self.alloc.alloc(Value, 5) catch return .{ .value = .undef };
|
|
fields[0] = .{ .string = file };
|
|
fields[1] = .{ .int = line };
|
|
fields[2] = .{ .int = col };
|
|
fields[3] = .{ .string = func_name };
|
|
fields[4] = .{ .string = line_text };
|
|
return .{ .value = .{ .aggregate = fields } };
|
|
},
|
|
.const_type => |tid| return .{ .value = .{ .type_tag = tid } },
|
|
|
|
// ── Arithmetic ──────────────────────────────────────
|
|
.add => |b| return .{ .value = try self.evalArith(frame, b, .add) },
|
|
.sub => |b| return .{ .value = try self.evalArith(frame, b, .sub) },
|
|
.mul => |b| return .{ .value = try self.evalArith(frame, b, .mul) },
|
|
.div => |b| return .{ .value = try self.evalArith(frame, b, .div) },
|
|
.mod => |b| return .{ .value = try self.evalArith(frame, b, .mod) },
|
|
.neg => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
return .{ .value = switch (val) {
|
|
.int => |v| .{ .int = -v },
|
|
.float => |v| .{ .float = -v },
|
|
else => return typeErrorDetail("comptime unary `-`: operand is neither int nor float"),
|
|
} };
|
|
},
|
|
|
|
// ── Comparison ──────────────────────────────────────
|
|
.cmp_eq => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .eq) } },
|
|
.cmp_ne => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .ne) } },
|
|
.cmp_lt => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .lt) } },
|
|
.cmp_le => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .le) } },
|
|
.cmp_gt => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .gt) } },
|
|
.cmp_ge => |b| return .{ .value = .{ .boolean = try self.evalCmp(frame, b, .ge) } },
|
|
.str_eq => |b| {
|
|
const lhs = frame.getRef(b.lhs);
|
|
const rhs = frame.getRef(b.rhs);
|
|
const ls = if (lhs == .string) lhs.string else "";
|
|
const rs = if (rhs == .string) rhs.string else "";
|
|
return .{ .value = .{ .boolean = std.mem.eql(u8, ls, rs) } };
|
|
},
|
|
.str_ne => |b| {
|
|
const lhs = frame.getRef(b.lhs);
|
|
const rhs = frame.getRef(b.rhs);
|
|
const ls = if (lhs == .string) lhs.string else "";
|
|
const rs = if (rhs == .string) rhs.string else "";
|
|
return .{ .value = .{ .boolean = !std.mem.eql(u8, ls, rs) } };
|
|
},
|
|
|
|
// ── Logical ─────────────────────────────────────────
|
|
.bool_and => |b| {
|
|
const lhs = frame.getRef(b.lhs).asBool() orelse return error.TypeError;
|
|
if (!lhs) return .{ .value = .{ .boolean = false } };
|
|
const rhs = frame.getRef(b.rhs).asBool() orelse return error.TypeError;
|
|
return .{ .value = .{ .boolean = rhs } };
|
|
},
|
|
.bool_or => |b| {
|
|
const lhs = frame.getRef(b.lhs).asBool() orelse return error.TypeError;
|
|
if (lhs) return .{ .value = .{ .boolean = true } };
|
|
const rhs = frame.getRef(b.rhs).asBool() orelse return error.TypeError;
|
|
return .{ .value = .{ .boolean = rhs } };
|
|
},
|
|
.bool_not => |u| {
|
|
const val = frame.getRef(u.operand).asBool() orelse return error.TypeError;
|
|
return .{ .value = .{ .boolean = !val } };
|
|
},
|
|
|
|
// ── Conversions ─────────────────────────────────────
|
|
.widen, .narrow => |c| {
|
|
const val = frame.getRef(c.operand);
|
|
return .{ .value = val }; // comptime values don't truncate
|
|
},
|
|
.bitcast => |c| {
|
|
const val = frame.getRef(c.operand);
|
|
// Loud-fail on `.type_tag → <runtime kind>` casts. A Type
|
|
// value can flow through bitcast only to .any (Any-boxing)
|
|
// or to itself; any other destination means the lowering
|
|
// emitted a coercion that silently pretends the TypeId is
|
|
// some other shape (e.g. an int, or a string). The most
|
|
// likely site that would trip this: the `case type:` arm
|
|
// of `any_to_string` in stdlib doing `xx val to string` —
|
|
// which expects the value field to already be a string,
|
|
// a leftover from the pre-`type_tag` era when Type values
|
|
// were string-shaped.
|
|
if (val == .type_tag) {
|
|
const allowed = c.to == .any or c.to == c.from;
|
|
if (!allowed) {
|
|
return bailDetail("comptime bitcast: Type value cast to a non-Type runtime kind — most likely a stale `xx val to string` from the pre-type_tag era; use `type_name(val)` instead");
|
|
}
|
|
}
|
|
return .{ .value = val };
|
|
},
|
|
.int_to_float => |c| {
|
|
const val = frame.getRef(c.operand);
|
|
const i = val.asInt() orelse return error.TypeError;
|
|
return .{ .value = .{ .float = @floatFromInt(i) } };
|
|
},
|
|
.float_to_int => |c| {
|
|
const val = frame.getRef(c.operand);
|
|
const f = val.asFloat() orelse return error.TypeError;
|
|
return .{ .value = .{ .int = @intFromFloat(f) } };
|
|
},
|
|
|
|
// ── Memory (stack simulation) ───────────────────────
|
|
.alloca => {
|
|
const slot = frame.allocSlot(self.alloc);
|
|
return .{ .value = .{ .slot_ptr = slot } };
|
|
},
|
|
.load => |u| {
|
|
const ptr = frame.getRef(u.operand);
|
|
switch (ptr) {
|
|
.slot_ptr => |slot| {
|
|
const slot_val = frame.loadSlot(slot);
|
|
// Check if this is a field pointer (from struct_gep)
|
|
if (self.resolveFieldLoad(frame, slot_val)) |field_val| {
|
|
return .{ .value = field_val };
|
|
}
|
|
return .{ .value = slot_val };
|
|
},
|
|
// The implicit __sx_ctx arrives as an aggregate after
|
|
// materializeCtxArg dereferences the caller's slot_ptr.
|
|
// `load(ref_0)` then naturally yields the Context value.
|
|
.aggregate => return .{ .value = ptr },
|
|
// Comptime load through a raw host pointer needs the
|
|
// target IR type to know byte width — currently not
|
|
// threaded into the .load op. Add it when a comptime
|
|
// path hits this.
|
|
.int => return bailDetail("comptime load through raw host pointer not supported (IR type width not threaded)"),
|
|
.byte_ptr => return bailDetail("comptime load through raw byte pointer not supported"),
|
|
.heap_ptr => return bailDetail("comptime load through interp heap pointer not supported"),
|
|
else => return bailDetail("comptime load: unsupported pointer kind"),
|
|
}
|
|
},
|
|
.store => |s| {
|
|
const ptr = frame.getRef(s.ptr);
|
|
const val = frame.getRef(s.val);
|
|
switch (ptr) {
|
|
.slot_ptr => |slot| {
|
|
const slot_val = frame.loadSlot(slot);
|
|
// Check if this is a field pointer (from struct_gep)
|
|
if (self.resolveFieldStore(frame, slot_val, val)) {
|
|
// Field store handled
|
|
} else {
|
|
frame.storeSlot(slot, val);
|
|
}
|
|
},
|
|
.heap_ptr => |hp| {
|
|
// Store a byte into heap memory (from index_gep on string)
|
|
const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF);
|
|
self.heapStoreByte(hp, byte);
|
|
},
|
|
// Raw host pointer (from extern call, e.g. libc_malloc).
|
|
// `val_ty` carries the declared destination width so we
|
|
// write exactly that many bytes — no neighbor clobber.
|
|
.int => |p| {
|
|
try storeAtRawPtr(self, p, val, s.val_ty);
|
|
},
|
|
// Byte-granular pointer (from index_gep on a string).
|
|
// Always a 1-byte store — matches the heap_ptr arm.
|
|
.byte_ptr => |addr| {
|
|
const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF);
|
|
const dst: [*]u8 = @ptrFromInt(addr);
|
|
dst[0] = byte;
|
|
},
|
|
else => return bailDetail("comptime store: unsupported pointer kind"),
|
|
}
|
|
return .{ .value = .void_val };
|
|
},
|
|
|
|
// ── Struct ops ──────────────────────────────────────
|
|
.struct_init => |agg| {
|
|
const fields = self.alloc.alloc(Value, agg.fields.len) catch return error.CannotEvalComptime;
|
|
for (agg.fields, 0..) |ref, i| {
|
|
fields[i] = frame.getRef(ref);
|
|
}
|
|
return .{ .value = .{ .aggregate = fields } };
|
|
},
|
|
.struct_get => |fa| {
|
|
var base = frame.getRef(fa.base);
|
|
// Auto-deref slot_ptr → load the value
|
|
if (base == .slot_ptr) {
|
|
const loaded = frame.loadSlot(base.slot_ptr);
|
|
if (self.resolveFieldLoad(frame, loaded)) |resolved| {
|
|
base = resolved;
|
|
} else {
|
|
base = loaded;
|
|
}
|
|
}
|
|
switch (base) {
|
|
.aggregate => |fields| {
|
|
if (fa.field_index >= fields.len) return error.OutOfBounds;
|
|
return .{ .value = fields[fa.field_index] };
|
|
},
|
|
.string => |s| {
|
|
// String as fat pointer: field 0 = ptr (string), field 1 = len
|
|
if (fa.field_index == 0) return .{ .value = .{ .string = s } };
|
|
if (fa.field_index == 1) return .{ .value = .{ .int = @intCast(s.len) } };
|
|
return error.OutOfBounds;
|
|
},
|
|
.int => |v| {
|
|
// Scalar boxed as "struct" — field 0 is the value itself
|
|
if (fa.field_index == 0) return .{ .value = .{ .int = v } };
|
|
return error.OutOfBounds;
|
|
},
|
|
.type_tag => |tid| {
|
|
// A first-class Type value is the comptime form of the
|
|
// runtime Any-Type aggregate `{ tag=.any, value=tid }`
|
|
// (see `const_type` lowering in buildPackSliceValue).
|
|
// `type_of(any_holding_a_Type)` lowers to struct_get
|
|
// field 0, expecting that runtime layout — mirror it so
|
|
// field 0 reads the `.any` tag and field 1 the type id.
|
|
if (fa.field_index == 0) return .{ .value = .{ .int = @intCast(TypeId.any.index()) } };
|
|
if (fa.field_index == 1) return .{ .value = .{ .type_tag = tid } };
|
|
return error.OutOfBounds;
|
|
},
|
|
else => return typeErrorDetail("comptime struct_get: base has no fields (not an aggregate/string/int)"),
|
|
}
|
|
},
|
|
|
|
// ── Enum ops ────────────────────────────────────────
|
|
.enum_init => |ei| {
|
|
if (ei.payload.isNone()) {
|
|
return .{ .value = .{ .int = @intCast(ei.tag) } };
|
|
} else {
|
|
const payload = frame.getRef(ei.payload);
|
|
const fields = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
|
fields[0] = .{ .int = @intCast(ei.tag) };
|
|
fields[1] = payload;
|
|
return .{ .value = .{ .aggregate = fields } };
|
|
}
|
|
},
|
|
.enum_tag => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
switch (val) {
|
|
.int => return .{ .value = val },
|
|
.aggregate => |fields| {
|
|
if (fields.len == 0) return typeErrorDetail("comptime enum_tag: aggregate operand has zero fields");
|
|
return .{ .value = fields[0] };
|
|
},
|
|
else => return typeErrorDetail("comptime enum_tag: operand is neither an int (untagged enum) nor an aggregate (tagged union)"),
|
|
}
|
|
},
|
|
.enum_payload => |fa| {
|
|
const base = frame.getRef(fa.base);
|
|
switch (base) {
|
|
.aggregate => |fields| {
|
|
if (fa.field_index + 1 >= fields.len) return error.OutOfBounds;
|
|
return .{ .value = fields[fa.field_index + 1] };
|
|
},
|
|
else => return typeErrorDetail("comptime enum_payload: base is not a tagged-union aggregate"),
|
|
}
|
|
},
|
|
|
|
// ── Optional ops ────────────────────────────────────
|
|
.optional_wrap => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
return .{ .value = val }; // wrapped value is just the value
|
|
},
|
|
.optional_unwrap => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
if (val.isNull()) return error.TypeError; // unwrapping null
|
|
return .{ .value = val };
|
|
},
|
|
.optional_has_value => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
return .{ .value = .{ .boolean = !val.isNull() } };
|
|
},
|
|
.optional_coalesce => |b| {
|
|
const lhs = frame.getRef(b.lhs);
|
|
if (!lhs.isNull()) return .{ .value = lhs };
|
|
return .{ .value = frame.getRef(b.rhs) };
|
|
},
|
|
|
|
// ── Calls ───────────────────────────────────────────
|
|
.call => |c| {
|
|
const args = self.alloc.alloc(Value, c.args.len) catch return error.CannotEvalComptime;
|
|
defer self.alloc.free(args);
|
|
for (c.args, 0..) |ref, i| {
|
|
// Inline any slot_ptr field-refs in the caller's frame before
|
|
// the value crosses the call boundary. slot_ptr indices are
|
|
// frame-local; if a slice/aggregate carrying one is passed to
|
|
// the callee, the callee would later resolve the index against
|
|
// its own slot table and read garbage.
|
|
args[i] = self.materializeForCall(frame, frame.getRef(ref));
|
|
}
|
|
// The implicit __sx_ctx is logically a `*Context` but the
|
|
// interp can't dereference cross-frame slot_ptrs. Materialise
|
|
// args[0] to the loaded Context aggregate so the callee can
|
|
// treat its slot 0 as the value directly.
|
|
const callee_func = self.module.getFunction(c.callee);
|
|
if (callee_func.has_implicit_ctx and args.len >= 1) {
|
|
args[0] = self.materializeCtxArg(frame, args[0]);
|
|
}
|
|
const result = try self.call(c.callee, args);
|
|
return .{ .value = result };
|
|
},
|
|
|
|
// The Obj-C runtime isn't available at comptime; any
|
|
// `#objc_call` reached during `#run` execution can't
|
|
// resolve. Fail fast so callers see a useful diagnostic.
|
|
.objc_msg_send => return bailDetail("#objc_call not available at comptime (no Obj-C runtime)"),
|
|
// Same story for JNI — no JVM at compile time.
|
|
.jni_msg_send => return bailDetail("#jni_call not available at comptime (no JVM)"),
|
|
// Inline asm executes target machine code — never comptime-evaluable.
|
|
.inline_asm => return bailDetail("inline assembly requires native execution; not available at comptime"),
|
|
|
|
// ── Block params ────────────────────────────────────
|
|
.block_param => {
|
|
// Block params are pushed at the start of block execution.
|
|
// This instruction is a no-op; the value was already pushed
|
|
// during block arg binding.
|
|
return .{ .value = .void_val };
|
|
},
|
|
|
|
// ── Terminators ─────────────────────────────────────
|
|
.br => |b| {
|
|
const args = self.alloc.alloc(Value, b.args.len) catch return error.CannotEvalComptime;
|
|
for (b.args, 0..) |ref, i| {
|
|
args[i] = frame.getRef(ref);
|
|
}
|
|
current_block.* = b.target;
|
|
block_args.* = args;
|
|
return .branch;
|
|
},
|
|
.cond_br => |cb| {
|
|
const cond = frame.getRef(cb.cond).asBool() orelse return error.TypeError;
|
|
if (cond) {
|
|
const args = self.alloc.alloc(Value, cb.then_args.len) catch return error.CannotEvalComptime;
|
|
for (cb.then_args, 0..) |ref, i| {
|
|
args[i] = frame.getRef(ref);
|
|
}
|
|
current_block.* = cb.then_target;
|
|
block_args.* = args;
|
|
} else {
|
|
const args = self.alloc.alloc(Value, cb.else_args.len) catch return error.CannotEvalComptime;
|
|
for (cb.else_args, 0..) |ref, i| {
|
|
args[i] = frame.getRef(ref);
|
|
}
|
|
current_block.* = cb.else_target;
|
|
block_args.* = args;
|
|
}
|
|
return .branch;
|
|
},
|
|
.switch_br => |sb| {
|
|
const operand = frame.getRef(sb.operand).asInt() orelse return error.TypeError;
|
|
for (sb.cases) |case| {
|
|
if (operand == case.value) {
|
|
const args = self.alloc.alloc(Value, case.args.len) catch return error.CannotEvalComptime;
|
|
for (case.args, 0..) |ref, i| {
|
|
args[i] = frame.getRef(ref);
|
|
}
|
|
current_block.* = case.target;
|
|
block_args.* = args;
|
|
return .branch;
|
|
}
|
|
}
|
|
// Default
|
|
const args = self.alloc.alloc(Value, sb.default_args.len) catch return error.CannotEvalComptime;
|
|
for (sb.default_args, 0..) |ref, i| {
|
|
args[i] = frame.getRef(ref);
|
|
}
|
|
current_block.* = sb.default;
|
|
block_args.* = args;
|
|
return .branch;
|
|
},
|
|
.ret => |u| {
|
|
return .{ .ret_val = frame.getRef(u.operand) };
|
|
},
|
|
.ret_void => return .ret_nothing,
|
|
.@"unreachable" => return error.Unreachable,
|
|
|
|
// ── Builtin calls ──────────────────────────────────
|
|
.call_builtin => |bi| {
|
|
return self.execBuiltin(bi, frame, instruction.ty);
|
|
},
|
|
|
|
// ── Compiler hook calls (#compiler methods) ────────
|
|
.compiler_call => |cc| {
|
|
const name = self.module.types.getString(@enumFromInt(cc.name));
|
|
if (self.hooks.get(name)) |hook| {
|
|
// Resolve args from Ref to Value
|
|
var resolved_args = std.ArrayList(Value).empty;
|
|
defer resolved_args.deinit(self.alloc);
|
|
for (cc.args) |arg| {
|
|
resolved_args.append(self.alloc, frame.getRef(arg)) catch return error.CannotEvalComptime;
|
|
}
|
|
if (self.build_config) |bc| {
|
|
const result = hook(self, resolved_args.items, bc, self.alloc) catch return bailDetail("#compiler hook returned an error (see hook impl)");
|
|
return .{ .value = result };
|
|
}
|
|
return .{ .value = .void_val };
|
|
}
|
|
if (last_bail_detail == null) {
|
|
// Capture which hook name failed so the host diag
|
|
// surfaces "compiler_call: unknown hook 'X'" instead
|
|
// of a bare CannotEvalComptime.
|
|
last_bail_detail = "#compiler hook not registered (likely a target-specific BuildOptions setter)";
|
|
}
|
|
return error.CannotEvalComptime;
|
|
},
|
|
|
|
// ── Struct GEP (field pointer) ─────────────────────
|
|
.struct_gep => |fa| {
|
|
const base = frame.getRef(fa.base);
|
|
switch (base) {
|
|
.slot_ptr => |slot| {
|
|
// Create a field-pointer: we encode as a slot_ptr with field info
|
|
// When loading, we extract the field; when storing, we modify the field
|
|
const field_slot = frame.allocSlot(self.alloc);
|
|
// Store a field reference: { parent_slot, field_index }
|
|
const field_ref = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
|
field_ref[0] = .{ .int = @intCast(slot) };
|
|
field_ref[1] = .{ .int = @intCast(fa.field_index) };
|
|
frame.storeSlot(field_slot, .{ .aggregate = field_ref });
|
|
return .{ .value = .{ .slot_ptr = field_slot } };
|
|
},
|
|
// struct_gep through a raw host pointer requires the
|
|
// struct's field-offset table — feasible via
|
|
// `fa.base_type` but not currently wired. Add when a
|
|
// comptime path hits this.
|
|
.int => return bailDetail("comptime struct_gep through raw host pointer not supported"),
|
|
.byte_ptr => return bailDetail("comptime struct_gep through raw byte pointer not supported"),
|
|
.heap_ptr => return bailDetail("comptime struct_gep through interp heap pointer not supported"),
|
|
else => return bailDetail("comptime struct_gep: unsupported pointer kind"),
|
|
}
|
|
},
|
|
|
|
// ── String/slice operations ────────────────────────
|
|
.index_get => |idx| {
|
|
const base = frame.getRef(idx.lhs);
|
|
const index_val = frame.getRef(idx.rhs);
|
|
const i: usize = @intCast(index_val.asInt() orelse return error.TypeError);
|
|
// Try as string value
|
|
if (base.asString(self)) |s| {
|
|
if (i >= s.len) return error.OutOfBounds;
|
|
return .{ .value = .{ .int = s[i] } };
|
|
}
|
|
// Try as aggregate array or slice
|
|
switch (base) {
|
|
.aggregate => |fields| {
|
|
// Check for slice-like: {data_ptr, len} where data_ptr is slot_ptr
|
|
if (fields.len == 2 and fields[1] == .int) {
|
|
const data = fields[0];
|
|
if (data == .slot_ptr) {
|
|
// The data field is a ptr — resolve through slots to get the array
|
|
const arr = self.resolveSlotChain(frame, data);
|
|
switch (arr) {
|
|
.aggregate => |arr_fields| {
|
|
if (i < arr_fields.len) return .{ .value = arr_fields[i] };
|
|
return error.OutOfBounds;
|
|
},
|
|
else => {},
|
|
}
|
|
} else if (data == .aggregate) {
|
|
// Inline array data
|
|
const arr_fields = data.aggregate;
|
|
if (i < arr_fields.len) return .{ .value = arr_fields[i] };
|
|
return error.OutOfBounds;
|
|
}
|
|
}
|
|
// Plain aggregate indexing
|
|
if (i >= fields.len) return error.OutOfBounds;
|
|
return .{ .value = fields[i] };
|
|
},
|
|
// Raw host pointer base — `buf[i]` reads one byte at
|
|
// offset i. Matches the byte-addressed `index_gep`
|
|
// semantics for the same shape. Used by comptime sx
|
|
// code that walks libc-malloc'd buffers.
|
|
.int => |p| {
|
|
const src: [*]const u8 = @ptrFromInt(@as(usize, @bitCast(p)));
|
|
return .{ .value = .{ .int = src[i] } };
|
|
},
|
|
.byte_ptr => |addr| {
|
|
const src: [*]const u8 = @ptrFromInt(addr);
|
|
return .{ .value = .{ .int = src[i] } };
|
|
},
|
|
else => return bailDetail("comptime index_get: unsupported base kind"),
|
|
}
|
|
},
|
|
.length => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
if (val.asString(self)) |s| {
|
|
return .{ .value = .{ .int = @intCast(s.len) } };
|
|
}
|
|
switch (val) {
|
|
.aggregate => |fields| {
|
|
// For fat pointers {ptr, len}, len is field[1]
|
|
if (fields.len == 2) {
|
|
return .{ .value = fields[1] };
|
|
}
|
|
return .{ .value = .{ .int = @intCast(fields.len) } };
|
|
},
|
|
else => return bailDetail("comptime .len: operand is neither a string nor an aggregate"),
|
|
}
|
|
},
|
|
.data_ptr => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
switch (val) {
|
|
.aggregate => |fields| {
|
|
if (fields.len >= 1) return .{ .value = fields[0] };
|
|
return error.OutOfBounds;
|
|
},
|
|
.string => return .{ .value = val },
|
|
else => return bailDetail("comptime .ptr: operand has no data field (not a string or slice aggregate)"),
|
|
}
|
|
},
|
|
.subslice => |sub| {
|
|
const base = frame.getRef(sub.base);
|
|
const lo: usize = @intCast(frame.getRef(sub.lo).asInt() orelse return bailDetail("comptime subslice: lo index is not an integer"));
|
|
const hi: usize = @intCast(frame.getRef(sub.hi).asInt() orelse return bailDetail("comptime subslice: hi index is not an integer"));
|
|
if (hi < lo) return error.OutOfBounds;
|
|
if (base.asString(self)) |s| {
|
|
if (hi > s.len) return error.OutOfBounds;
|
|
return .{ .value = .{ .string = s[lo..hi] } };
|
|
}
|
|
// Non-string aggregate (array or `{data,len}` slice). The
|
|
// underlying element list comes from the aggregate directly (an
|
|
// array) or its data field (a slice) — `sub.base_ty` picks which,
|
|
// since a 2-element array and a `{ptr,len}` pair are
|
|
// indistinguishable by Value shape alone.
|
|
const elems = self.subsliceElements(frame, base, sub.base_ty) orelse
|
|
return bailDetail("comptime subslice: base is not a sliceable array/slice value");
|
|
if (hi > elems.len) return error.OutOfBounds;
|
|
const sub_elems = elems[lo..hi];
|
|
// Return a proper slice VALUE `{data, len}`: data is the element
|
|
// aggregate, len the (int) count. The int len is what lets
|
|
// downstream `.length` / `index_get` / `decodeVariantElements`
|
|
// read this as a slice and not a bare array.
|
|
const pair = self.alloc.dupe(Value, &.{ .{ .aggregate = sub_elems }, .{ .int = @intCast(sub_elems.len) } }) catch return error.CannotEvalComptime;
|
|
return .{ .value = .{ .aggregate = pair } };
|
|
},
|
|
|
|
// ── Addr/deref ─────────────────────────────────────
|
|
.addr_of => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
return .{ .value = val }; // pass through pointer-like values
|
|
},
|
|
.deref => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
switch (val) {
|
|
.slot_ptr => |slot| return .{ .value = frame.loadSlot(slot) },
|
|
// Real raw-memory deref needs val's IR type for byte
|
|
// width — not yet threaded. Erroring is safer than
|
|
// returning the pointer-as-int unchanged, which
|
|
// silently looks like a successful deref.
|
|
.int => return bailDetail("comptime deref through raw host pointer not supported (IR type width not threaded)"),
|
|
.byte_ptr => return bailDetail("comptime deref through raw byte pointer not supported"),
|
|
.heap_ptr => return bailDetail("comptime deref through interp heap pointer not supported"),
|
|
// Pre-dereferenced values that flow through deref as a
|
|
// no-op: an aggregate/string already IS the loaded
|
|
// value (lowering sometimes emits `deref(struct_val)`
|
|
// where the struct was previously materialized in
|
|
// place rather than via a slot).
|
|
.aggregate, .string => return .{ .value = val },
|
|
// Null deref is UB at runtime; surface it at comptime
|
|
// instead of silently producing a null again.
|
|
.null_val => return bailDetail("comptime deref of null"),
|
|
// Scalars / handles / undef aren't pointer-shaped —
|
|
// dereffing them is a frontend bug. Bail rather than
|
|
// returning the bare value (which looked like a
|
|
// successful deref to callers).
|
|
.boolean, .float, .func_ref, .closure, .type_tag, .void_val, .undef => return bailDetail("comptime deref: operand is not a pointer"),
|
|
}
|
|
},
|
|
|
|
// ── Bitwise operations ─────────────────────────────
|
|
.bit_and => |b| {
|
|
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
|
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
|
return .{ .value = .{ .int = lhs & rhs } };
|
|
},
|
|
.bit_or => |b| {
|
|
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
|
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
|
return .{ .value = .{ .int = lhs | rhs } };
|
|
},
|
|
.bit_xor => |b| {
|
|
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
|
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
|
return .{ .value = .{ .int = lhs ^ rhs } };
|
|
},
|
|
.bit_not => |u| {
|
|
const val = frame.getRef(u.operand).asInt() orelse return error.TypeError;
|
|
return .{ .value = .{ .int = ~val } };
|
|
},
|
|
.shl => |b| {
|
|
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
|
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
|
const shift: u6 = @intCast(@min(rhs, 63));
|
|
return .{ .value = .{ .int = lhs << shift } };
|
|
},
|
|
.shr => |b| {
|
|
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
|
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
|
const shift: u6 = @intCast(@min(rhs, 63));
|
|
return .{ .value = .{ .int = lhs >> shift } };
|
|
},
|
|
|
|
// ── Tuple ops (same as struct) ─────────────────────
|
|
.tuple_init => |agg| {
|
|
const fields = self.alloc.alloc(Value, agg.fields.len) catch return error.CannotEvalComptime;
|
|
for (agg.fields, 0..) |ref, i| {
|
|
fields[i] = frame.getRef(ref);
|
|
}
|
|
return .{ .value = .{ .aggregate = fields } };
|
|
},
|
|
.tuple_get => |fa| {
|
|
const base = frame.getRef(fa.base);
|
|
switch (base) {
|
|
.aggregate => |fields| {
|
|
if (fa.field_index >= fields.len) return error.OutOfBounds;
|
|
return .{ .value = fields[fa.field_index] };
|
|
},
|
|
else => return error.TypeError,
|
|
}
|
|
},
|
|
|
|
// ── Box/unbox (Any type) ───────────────────────────
|
|
.box_any => |ba| {
|
|
const val = frame.getRef(ba.operand);
|
|
// Box as aggregate: { type_tag, value } — matches LLVM layout
|
|
const fields = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
|
fields[0] = .{ .int = @intFromEnum(ba.source_type) };
|
|
fields[1] = val;
|
|
return .{ .value = .{ .aggregate = fields } };
|
|
},
|
|
.unbox_any => |ua| {
|
|
const val = frame.getRef(ua.operand);
|
|
switch (val) {
|
|
.aggregate => |fields| {
|
|
// Value is at field 1 in { tag, value } layout
|
|
if (fields.len >= 2) return .{ .value = fields[1] };
|
|
if (fields.len >= 1) return .{ .value = fields[0] };
|
|
return error.OutOfBounds;
|
|
},
|
|
// Any-typed comptime values flow through box_any first,
|
|
// which always wraps as an aggregate. If we reach here
|
|
// with a scalar / undef / null, the IR shape upstream
|
|
// diverged from the box_any contract — bail loudly so
|
|
// the offending box_any site shows in the diagnostic.
|
|
.int, .float, .boolean, .string, .null_val, .undef => return bailDetail("comptime unbox_any: operand is a bare scalar (expected { tag, value } aggregate from box_any)"),
|
|
.void_val => return bailDetail("comptime unbox_any: operand is void_val"),
|
|
.slot_ptr, .heap_ptr, .byte_ptr, .func_ref, .closure, .type_tag => return bailDetail("comptime unbox_any: operand is a pointer/handle (expected { tag, value } aggregate)"),
|
|
}
|
|
},
|
|
|
|
// ── Reflection ─────────────────────────────────────
|
|
.field_name_get => |fr| {
|
|
const idx_val = frame.getRef(fr.index);
|
|
const idx: usize = @intCast(switch (idx_val) {
|
|
.int => |i| i,
|
|
else => return bailDetail("comptime field_name(T, i): index operand is not an int"),
|
|
});
|
|
const info = self.module.types.get(fr.struct_type);
|
|
const fields = switch (info) {
|
|
.@"struct" => |s| s.fields,
|
|
.@"union" => |u| u.fields,
|
|
.tagged_union => |u| u.fields,
|
|
else => return bailDetail("comptime field_name(T, i): T is not a struct/union/tagged_union"),
|
|
};
|
|
if (idx >= fields.len) return error.OutOfBounds;
|
|
const name = self.module.types.getString(fields[idx].name);
|
|
return .{ .value = .{ .string = name } };
|
|
},
|
|
.error_tag_name_get => |u| {
|
|
const tag_val = frame.getRef(u.operand);
|
|
const id: u32 = @intCast(switch (tag_val) {
|
|
.int => |i| i,
|
|
else => return bailDetail("comptime error_tag_name(e): operand is not an integer tag id"),
|
|
});
|
|
return .{ .value = .{ .string = self.module.types.tags.getName(id) } };
|
|
},
|
|
.field_value_get => |fr| {
|
|
const base_val = frame.getRef(fr.base);
|
|
const idx_val = frame.getRef(fr.index);
|
|
const idx: usize = @intCast(switch (idx_val) {
|
|
.int => |i| i,
|
|
else => return bailDetail("comptime field_value(s, i): index operand is not an int"),
|
|
});
|
|
switch (base_val) {
|
|
.aggregate => |agg| {
|
|
if (idx >= agg.len) return error.OutOfBounds;
|
|
// Box as Any: { value, type_tag }
|
|
const info = self.module.types.get(fr.struct_type);
|
|
const fields = switch (info) {
|
|
.@"struct" => |s| s.fields,
|
|
.@"union" => |u| u.fields,
|
|
.tagged_union => |u| u.fields,
|
|
else => return bailDetail("comptime field_value(s, i): s's type is not a struct/union/tagged_union"),
|
|
};
|
|
const field_ty_tag: i64 = if (idx < fields.len) @intFromEnum(fields[idx].ty) else 0;
|
|
const boxed = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
|
boxed[0] = agg[idx];
|
|
boxed[1] = .{ .int = field_ty_tag };
|
|
return .{ .value = .{ .aggregate = boxed } };
|
|
},
|
|
else => return bailDetail("comptime field_value(s, i): s is not an aggregate Value (struct values must be materialized as aggregates at comptime)"),
|
|
}
|
|
},
|
|
|
|
// ── Global access ──────────────────────────────────
|
|
.global_get => |gid| {
|
|
const val = try self.getGlobal(gid);
|
|
return .{ .value = val };
|
|
},
|
|
.global_addr => |gid| {
|
|
// The implicit-context default global is the only global
|
|
// whose address sees runtime use. Return the Context
|
|
// aggregate directly so `load(args[0])` yields it via the
|
|
// aggregate-passthrough branch of the `.load` handler.
|
|
if (gid.index() < self.module.globals.items.len) {
|
|
const global = &self.module.globals.items[gid.index()];
|
|
const name = self.module.types.getString(global.name);
|
|
if (std.mem.eql(u8, name, "__sx_default_context")) {
|
|
return .{ .value = self.defaultContextValue() };
|
|
}
|
|
}
|
|
return bailDetail("comptime global_addr: only `&__sx_default_context` is currently materialised at comptime");
|
|
},
|
|
.func_ref => |fid| {
|
|
return .{ .value = .{ .func_ref = fid } };
|
|
},
|
|
.global_set => |gs| {
|
|
const val = frame.getRef(gs.value);
|
|
self.global_values.put(gs.global.index(), val) catch {};
|
|
return .{ .value = .void_val };
|
|
},
|
|
|
|
// ── Index GEP (array element pointer) ─────────────
|
|
.index_gep => |b| {
|
|
const base = frame.getRef(b.lhs);
|
|
const idx = frame.getRef(b.rhs);
|
|
switch (base) {
|
|
.slot_ptr => |slot| {
|
|
// Create an indexed element pointer: { parent_slot, index, is_index_gep=1 }
|
|
const field_slot = frame.allocSlot(self.alloc);
|
|
const ref = self.alloc.alloc(Value, 3) catch return error.CannotEvalComptime;
|
|
ref[0] = .{ .int = @intCast(slot) };
|
|
ref[1] = idx;
|
|
ref[2] = .{ .int = 1 }; // marker: this is index_gep, not struct_gep
|
|
frame.storeSlot(field_slot, .{ .aggregate = ref });
|
|
return .{ .value = .{ .slot_ptr = field_slot } };
|
|
},
|
|
.aggregate => |fields| {
|
|
// String/slice aggregate {data_ptr, len} — compute data_ptr + index
|
|
if (fields.len >= 2) {
|
|
const data_ptr = fields[0];
|
|
const offset = idx.asInt() orelse return error.TypeError;
|
|
switch (data_ptr) {
|
|
.heap_ptr => |hp| {
|
|
return .{ .value = .{ .heap_ptr = .{
|
|
.id = hp.id,
|
|
.offset = hp.offset + @as(u32, @intCast(offset)),
|
|
} } };
|
|
},
|
|
// Raw host pointer (from extern call return,
|
|
// e.g. libc_malloc). Byte-addressed offset
|
|
// matches the heap_ptr branch above — both
|
|
// are u8-granular for sx's string/slice ops.
|
|
// Producing `.byte_ptr` makes store-through
|
|
// this address write a single byte.
|
|
.int => |p| {
|
|
return .{ .value = .{ .byte_ptr = @intCast(p + offset) } };
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return bailDetail("comptime index_gep: unsupported aggregate-base shape (expected {data_ptr, len} with heap_ptr or int data field)");
|
|
},
|
|
.string => |s| {
|
|
// String literal — copy to heap and return heap_ptr at offset
|
|
const offset: usize = @intCast(@as(u64, @bitCast(idx.asInt() orelse return error.TypeError)));
|
|
const hp = self.heapAlloc(s.len);
|
|
self.heapMemcpy(hp, s, s.len);
|
|
return .{ .value = .{ .heap_ptr = .{
|
|
.id = hp.id,
|
|
.offset = @intCast(offset),
|
|
} } };
|
|
},
|
|
// Raw host pointer base — byte-addressed offset.
|
|
// Element size > 1 would silently mis-index; document
|
|
// the assumption. Callers stride past byte granularity
|
|
// must wrap the pointer in an aggregate so the
|
|
// {data_ptr, len} branch fires (which is also
|
|
// byte-addressed today — fix here when needed).
|
|
.int => |p| {
|
|
const offset = idx.asInt() orelse return error.TypeError;
|
|
return .{ .value = .{ .int = p + offset } };
|
|
},
|
|
else => return bailDetail("comptime index_gep: unsupported base kind"),
|
|
}
|
|
},
|
|
|
|
// ── Array to slice ────────────────────────────────
|
|
.array_to_slice => |u| {
|
|
const val = frame.getRef(u.operand);
|
|
switch (val) {
|
|
.aggregate => |fields| {
|
|
// Convert array aggregate to slice: { aggregate_ref, len }
|
|
const slice = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
|
slice[0] = val; // the array data
|
|
slice[1] = .{ .int = @intCast(fields.len) };
|
|
return .{ .value = .{ .aggregate = slice } };
|
|
},
|
|
.slot_ptr => |slot| {
|
|
const arr = frame.loadSlot(slot);
|
|
switch (arr) {
|
|
.aggregate => |fields| {
|
|
const slice = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
|
slice[0] = arr;
|
|
slice[1] = .{ .int = @intCast(fields.len) };
|
|
return .{ .value = .{ .aggregate = slice } };
|
|
},
|
|
else => return bailDetail("comptime array_to_slice: slot-backed value is not an aggregate"),
|
|
}
|
|
},
|
|
else => return bailDetail("comptime array_to_slice: operand is neither an aggregate nor a slot pointer"),
|
|
}
|
|
},
|
|
|
|
// ── Call indirect (function pointer) ──────────────
|
|
.call_indirect => |ci| {
|
|
const callee = frame.getRef(ci.callee);
|
|
switch (callee) {
|
|
.func_ref => |fid| {
|
|
const args = self.alloc.alloc(Value, ci.args.len) catch return error.CannotEvalComptime;
|
|
defer self.alloc.free(args);
|
|
for (ci.args, 0..) |ref, i| {
|
|
args[i] = self.materializeForCall(frame, frame.getRef(ref));
|
|
}
|
|
const target = self.module.getFunction(fid);
|
|
if (target.has_implicit_ctx and args.len >= 1) {
|
|
args[0] = self.materializeCtxArg(frame, args[0]);
|
|
}
|
|
const result = try self.call(fid, args);
|
|
return .{ .value = result };
|
|
},
|
|
else => return bailDetail("comptime call_indirect: callee is not a func_ref Value (raw fn-pointers from extern calls aren't dispatchable in interp)"),
|
|
}
|
|
},
|
|
|
|
// Type-as-value sentinel emitted for the type arg of
|
|
// `cast(T) val`. Result is never read (the cast lowering
|
|
// consumes the type from the AST, not the IR Ref), so an
|
|
// undef value is sufficient — matches the LLVM emitter.
|
|
.placeholder => return .{ .value = .undef },
|
|
|
|
// ── Not yet evaluable at comptime ──────────────────
|
|
.call_closure => return bailDetail("comptime call_closure not yet implemented (closure trampoline ABI threading required)"),
|
|
.closure_create => return bailDetail("comptime closure_create not yet implemented"),
|
|
.union_get => return bailDetail("comptime union_get not yet implemented"),
|
|
.union_gep => return bailDetail("comptime union_gep not yet implemented"),
|
|
.vec_splat => return bailDetail("comptime vec_splat not yet implemented"),
|
|
.vec_extract => return bailDetail("comptime vec_extract not yet implemented"),
|
|
.vec_insert => return bailDetail("comptime vec_insert not yet implemented"),
|
|
}
|
|
}
|
|
|
|
// ── Arithmetic helpers ──────────────────────────────────────────
|
|
|
|
const ArithOp = enum { add, sub, mul, div, mod };
|
|
|
|
fn evalArith(self: *Interpreter, frame: *Frame, b: inst_mod.BinOp, comptime aop: ArithOp) InterpError!Value {
|
|
_ = self;
|
|
const lhs = frame.getRef(b.lhs);
|
|
const rhs = frame.getRef(b.rhs);
|
|
|
|
// Both int
|
|
if (lhs.asInt()) |li| {
|
|
if (rhs.asInt()) |ri| {
|
|
return .{ .int = switch (aop) {
|
|
.add => li +% ri,
|
|
.sub => li -% ri,
|
|
.mul => li *% ri,
|
|
.div => if (ri == 0) return error.DivisionByZero else @divTrunc(li, ri),
|
|
.mod => if (ri == 0) return error.DivisionByZero else @mod(li, ri),
|
|
} };
|
|
}
|
|
}
|
|
|
|
// Both float (or int promoted to float)
|
|
if (lhs.asFloat()) |lf| {
|
|
if (rhs.asFloat()) |rf| {
|
|
return .{ .float = switch (aop) {
|
|
.add => lf + rf,
|
|
.sub => lf - rf,
|
|
.mul => lf * rf,
|
|
.div => if (rf == 0.0) return error.DivisionByZero else lf / rf,
|
|
.mod => @mod(lf, rf),
|
|
} };
|
|
}
|
|
}
|
|
|
|
return typeErrorDetail("comptime arithmetic: operand pair is neither both-int nor both-float-coercible");
|
|
}
|
|
|
|
// ── Comparison helpers ──────────────────────────────────────────
|
|
|
|
const CmpOp = enum { eq, ne, lt, le, gt, ge };
|
|
|
|
fn evalCmp(self: *Interpreter, frame: *Frame, b: inst_mod.BinOp, comptime cop: CmpOp) InterpError!bool {
|
|
_ = self;
|
|
const lhs = frame.getRef(b.lhs);
|
|
const rhs = frame.getRef(b.rhs);
|
|
|
|
// Both int
|
|
if (lhs.asInt()) |li| {
|
|
if (rhs.asInt()) |ri| {
|
|
return switch (cop) {
|
|
.eq => li == ri,
|
|
.ne => li != ri,
|
|
.lt => li < ri,
|
|
.le => li <= ri,
|
|
.gt => li > ri,
|
|
.ge => li >= ri,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Both float
|
|
if (lhs.asFloat()) |lf| {
|
|
if (rhs.asFloat()) |rf| {
|
|
return switch (cop) {
|
|
.eq => lf == rf,
|
|
.ne => lf != rf,
|
|
.lt => lf < rf,
|
|
.le => lf <= rf,
|
|
.gt => lf > rf,
|
|
.ge => lf >= rf,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Bool equality
|
|
if (lhs.asBool()) |lb| {
|
|
if (rhs.asBool()) |rb| {
|
|
return switch (cop) {
|
|
.eq => lb == rb,
|
|
.ne => lb != rb,
|
|
else => return error.TypeError,
|
|
};
|
|
}
|
|
}
|
|
|
|
// Type-as-value equality. Compares TypeIds structurally.
|
|
// `.type_tag` vs `.int(N)` deliberately does NOT compare —
|
|
// a Type is not an int even if the underlying enum value
|
|
// matches; falls through to the typeErrorDetail below.
|
|
if (lhs.asTypeId()) |la| {
|
|
if (rhs.asTypeId()) |ra| {
|
|
return switch (cop) {
|
|
.eq => la == ra,
|
|
.ne => la != ra,
|
|
else => return error.TypeError,
|
|
};
|
|
}
|
|
}
|
|
|
|
return typeErrorDetail("comptime comparison: operand pair has no shared comparable shape (int/float/bool/string/type)");
|
|
}
|
|
|
|
// ── Slot chain resolution ────────────────────────────────────
|
|
|
|
/// Walk an aggregate Value and rewrite any embedded `slot_ptr` that points
|
|
/// to a field-ref slot in `frame` (the marker shape `{parent_slot, idx, ..}`
|
|
/// emitted by `struct_gep` / `index_gep`) into the resolved parent value.
|
|
/// Slot indices are frame-local; a slice passed across a call would otherwise
|
|
/// read its data_ptr out of the callee's slot table.
|
|
/// Resolve the implicit __sx_ctx arg to its loaded Context value so
|
|
/// callees can treat their own slot 0 as the aggregate directly
|
|
/// (no cross-frame slot_ptr indirection).
|
|
fn materializeCtxArg(self: *Interpreter, frame: *Frame, val: Value) Value {
|
|
_ = self;
|
|
return switch (val) {
|
|
.slot_ptr => |slot| frame.loadSlot(slot),
|
|
else => val,
|
|
};
|
|
}
|
|
|
|
fn materializeForCall(self: *Interpreter, frame: *Frame, val: Value) Value {
|
|
switch (val) {
|
|
.aggregate => |fields| {
|
|
const new_fields = self.alloc.alloc(Value, fields.len) catch return val;
|
|
for (fields, 0..) |f, i| {
|
|
new_fields[i] = self.materializeForCall(frame, f);
|
|
}
|
|
return .{ .aggregate = new_fields };
|
|
},
|
|
.slot_ptr => |slot| {
|
|
const stored = frame.loadSlot(slot);
|
|
if (stored == .aggregate) {
|
|
const ref_fields = stored.aggregate;
|
|
if (ref_fields.len >= 2) {
|
|
const parent_slot_val = ref_fields[0].asInt() orelse return val;
|
|
if (ref_fields[1].asInt() == null) return val;
|
|
const parent_slot: u32 = @intCast(parent_slot_val);
|
|
const parent = frame.loadSlot(parent_slot);
|
|
return self.materializeForCall(frame, parent);
|
|
}
|
|
}
|
|
return val;
|
|
},
|
|
else => return val,
|
|
}
|
|
}
|
|
|
|
/// Follow a slot_ptr through field-pointer / index-gep chains
|
|
/// to get the underlying value. Handles nested dereferences.
|
|
fn resolveSlotChain(self: *Interpreter, frame: *Frame, val: Value) Value {
|
|
_ = self;
|
|
var current = val;
|
|
var depth: u32 = 0;
|
|
while (depth < 16) : (depth += 1) {
|
|
switch (current) {
|
|
.slot_ptr => |slot| {
|
|
const stored = frame.loadSlot(slot);
|
|
switch (stored) {
|
|
.aggregate => |ref_fields| {
|
|
if (ref_fields.len >= 2) {
|
|
// Field-pointer or index-gep reference: {parent_slot, index, [marker]}
|
|
const parent_slot_val = ref_fields[0].asInt() orelse return stored;
|
|
const parent_slot: u32 = @intCast(parent_slot_val);
|
|
const parent = frame.loadSlot(parent_slot);
|
|
return parent; // Return the parent array/struct
|
|
}
|
|
return stored;
|
|
},
|
|
.slot_ptr => {
|
|
current = stored;
|
|
continue;
|
|
},
|
|
else => return stored,
|
|
}
|
|
},
|
|
else => return current,
|
|
}
|
|
}
|
|
return current;
|
|
}
|
|
|
|
/// The element list backing a comptime array/slice VALUE, for `subslice`.
|
|
/// `base_ty` (threaded onto the op at lower time) disambiguates the two
|
|
/// shapes that look identical as Values: an ARRAY's aggregate holds its
|
|
/// elements directly, while a SLICE is a `{data, len}` fat pointer whose
|
|
/// `data` field holds them. Returns null for any other shape (caller bails).
|
|
fn subsliceElements(self: *Interpreter, frame: *Frame, base: Value, base_ty: TypeId) ?[]const Value {
|
|
var b = base;
|
|
if (b == .slot_ptr) b = self.resolveSlotChain(frame, b);
|
|
const fields = switch (b) {
|
|
.aggregate => |f| f,
|
|
else => return null,
|
|
};
|
|
const is_slice = !base_ty.isBuiltin() and self.module.types.get(base_ty) == .slice;
|
|
if (is_slice) {
|
|
if (fields.len != 2) return null;
|
|
const len: usize = @intCast(fields[1].asInt() orelse return null);
|
|
var data = fields[0];
|
|
if (data == .slot_ptr) data = self.resolveSlotChain(frame, data);
|
|
return switch (data) {
|
|
.aggregate => |arr| if (len <= arr.len) arr[0..len] else null,
|
|
else => null,
|
|
};
|
|
}
|
|
// Array (or unknown base_ty fallback): the fields ARE the elements.
|
|
return fields;
|
|
}
|
|
|
|
// ── Constant → Value conversion ─────────────────────────────
|
|
|
|
fn constToValue(self: *Interpreter, cv: inst_mod.ConstantValue) Value {
|
|
return switch (cv) {
|
|
.int => |v| .{ .int = v },
|
|
.float => |v| .{ .float = v },
|
|
.boolean => |v| .{ .boolean = v },
|
|
.string => |sid| .{ .string = self.module.types.getString(sid) },
|
|
.null_val => .null_val,
|
|
.undef, .zeroinit => .undef,
|
|
.aggregate => |items| {
|
|
const fields = self.alloc.alloc(Value, items.len) catch return .undef;
|
|
for (items, 0..) |item, i| {
|
|
fields[i] = self.constToValue(item);
|
|
}
|
|
return .{ .aggregate = fields };
|
|
},
|
|
.vtable => |func_ids| {
|
|
// Vtable is a struct of function refs — represent as aggregate of func_ref values
|
|
const fields = self.alloc.alloc(Value, func_ids.len) catch return .undef;
|
|
for (func_ids, 0..) |fid, i| {
|
|
fields[i] = .{ .func_ref = fid };
|
|
}
|
|
return .{ .aggregate = fields };
|
|
},
|
|
.func_ref => |fid| .{ .func_ref = fid },
|
|
};
|
|
}
|
|
|
|
// ── Field pointer helpers (for struct_gep load/store) ─────────
|
|
|
|
/// Check if a slot value is a field pointer { parent_slot, field_index [, is_index_gep] }.
|
|
/// If so, load the parent aggregate and return the field value.
|
|
fn resolveFieldLoad(self: *Interpreter, frame: *Frame, slot_val: Value) ?Value {
|
|
_ = self;
|
|
switch (slot_val) {
|
|
.aggregate => |fields| {
|
|
if (fields.len >= 2) {
|
|
const parent_slot_val = fields[0].asInt() orelse return null;
|
|
const field_idx_val = fields[1].asInt() orelse return null;
|
|
// A real field-pointer's parent_slot is a small frame
|
|
// index; a struct aggregate whose first field happens
|
|
// to be a wide integer (e.g. a stored pointer-as-int
|
|
// or a u64) would otherwise mis-trigger this branch.
|
|
if (parent_slot_val < 0 or parent_slot_val > std.math.maxInt(u32)) return null;
|
|
if (field_idx_val < 0 or field_idx_val > std.math.maxInt(u32)) return null;
|
|
const parent_slot: u32 = @intCast(parent_slot_val);
|
|
const field_idx: usize = @intCast(field_idx_val);
|
|
const parent = frame.loadSlot(parent_slot);
|
|
switch (parent) {
|
|
.aggregate => |parent_fields| {
|
|
if (field_idx < parent_fields.len) return parent_fields[field_idx];
|
|
},
|
|
.string => |s| {
|
|
// String fat pointer: field 0 = ptr (as string), field 1 = len
|
|
if (field_idx == 0) return .{ .string = s };
|
|
if (field_idx == 1) return .{ .int = @intCast(s.len) };
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Check if a slot value is a field pointer. If so, modify the field
|
|
/// in the parent aggregate. Returns true if handled.
|
|
fn resolveFieldStore(self: *Interpreter, frame: *Frame, slot_val: Value, new_val: Value) bool {
|
|
switch (slot_val) {
|
|
.aggregate => |fields| {
|
|
if (fields.len >= 2) {
|
|
const parent_slot_val = fields[0].asInt() orelse return false;
|
|
const field_idx_val = fields[1].asInt() orelse return false;
|
|
// Same field-pointer-vs-real-struct disambiguation as
|
|
// resolveFieldLoad — a wide integer in fields[0] is a
|
|
// stored pointer, not a frame index.
|
|
if (parent_slot_val < 0 or parent_slot_val > std.math.maxInt(u32)) return false;
|
|
if (field_idx_val < 0 or field_idx_val > std.math.maxInt(u32)) return false;
|
|
const parent_slot: u32 = @intCast(parent_slot_val);
|
|
const field_idx: usize = @intCast(field_idx_val);
|
|
const parent = frame.loadSlot(parent_slot);
|
|
switch (parent) {
|
|
.aggregate => |parent_fields| {
|
|
const new_len = @max(field_idx + 1, parent_fields.len);
|
|
const new_fields = self.alloc.alloc(Value, new_len) catch return false;
|
|
@memcpy(new_fields[0..parent_fields.len], parent_fields);
|
|
for (new_fields[parent_fields.len..]) |*f| f.* = .undef;
|
|
new_fields[field_idx] = new_val;
|
|
frame.storeSlot(parent_slot, .{ .aggregate = new_fields });
|
|
return true;
|
|
},
|
|
.undef => {
|
|
// Initialize a new aggregate from undef
|
|
const num_fields: usize = @max(field_idx + 1, 2); // at least 2 for strings
|
|
const new_fields = self.alloc.alloc(Value, num_fields) catch return false;
|
|
for (new_fields) |*f| f.* = .undef;
|
|
new_fields[field_idx] = new_val;
|
|
frame.storeSlot(parent_slot, .{ .aggregate = new_fields });
|
|
return true;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
},
|
|
else => {},
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ── Builtin call dispatch ──────────────────────────────────────
|
|
|
|
fn execBuiltin(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame, _: TypeId) InterpError!ExecResult {
|
|
const result = self.execBuiltinInner(bi, frame) catch |err| {
|
|
if (last_bail_builtin == null) last_bail_builtin = @tagName(bi.builtin);
|
|
return err;
|
|
};
|
|
return result;
|
|
}
|
|
|
|
fn execBuiltinInner(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame) InterpError!ExecResult {
|
|
switch (bi.builtin) {
|
|
.out => {
|
|
const str_val = frame.getRef(bi.args[0]);
|
|
if (str_val.asString(self)) |s| {
|
|
self.output.appendSlice(self.alloc, s) catch {};
|
|
}
|
|
return .{ .value = .void_val };
|
|
},
|
|
.size_of => {
|
|
// Return a default size (8 bytes for most types)
|
|
return .{ .value = .{ .int = 8 } };
|
|
},
|
|
.align_of => {
|
|
return .{ .value = .{ .int = 8 } };
|
|
},
|
|
.sqrt => {
|
|
const val = frame.getRef(bi.args[0]);
|
|
const f = val.asFloat() orelse return error.TypeError;
|
|
return .{ .value = .{ .float = @sqrt(f) } };
|
|
},
|
|
.sin => {
|
|
const val = frame.getRef(bi.args[0]);
|
|
const f = val.asFloat() orelse return error.TypeError;
|
|
return .{ .value = .{ .float = @sin(f) } };
|
|
},
|
|
.cos => {
|
|
const val = frame.getRef(bi.args[0]);
|
|
const f = val.asFloat() orelse return error.TypeError;
|
|
return .{ .value = .{ .float = @cos(f) } };
|
|
},
|
|
.floor => {
|
|
const val = frame.getRef(bi.args[0]);
|
|
const f = val.asFloat() orelse return error.TypeError;
|
|
return .{ .value = .{ .float = @floor(f) } };
|
|
},
|
|
.cast => return bailDetail("comptime #builtin cast: handled at lowering, not the interp (you reached this if a #builtin cast leaked into IR)"),
|
|
.type_of => return bailDetail("comptime #builtin type_of: handled at lowering, not the interp"),
|
|
.alloc => return bailDetail("comptime #builtin alloc unused (use context.allocator.alloc)"),
|
|
.dealloc => return bailDetail("comptime #builtin dealloc unused (use context.allocator.dealloc)"),
|
|
|
|
// ── Comptime reflection (Type-as-Value path) ─────────
|
|
// These are only reached when lower.zig emitted a real
|
|
// builtin_call — i.e. the type argument was NOT statically
|
|
// resolvable (e.g. inside a builder body where `args[i]` is
|
|
// a `.type_tag(TypeId)` Value bound at interp time). Static
|
|
// calls fold to `const_string` / `const_bool` at lower time
|
|
// and never hit this dispatch.
|
|
.type_name => {
|
|
if (bi.args.len < 1) return bailDetail("comptime type_name: missing argument");
|
|
const arg = frame.getRef(bi.args[0]);
|
|
// A bare `.type_tag` Value (the comptime-native form), an
|
|
// Any-boxed Type (`{ .any, tid }`), or an Any holding a
|
|
// runtime value (`{ tag, value }`, where the tag IS the
|
|
// value's type). `reflectTypeId` reads the runtime tag so
|
|
// `type_name(av)` for `av : Any = 6` names `i64`, not the
|
|
// type whose index equals the payload.
|
|
const tid = arg.reflectTypeId() orelse
|
|
return bailDetail("comptime type_name: argument is not a Type value or boxed value (expected `.type_tag` or Any aggregate)");
|
|
const name = self.module.types.typeName(tid);
|
|
// Copy the slice into the interp's allocator so it
|
|
// outlives any TypeTable churn during the rest of the
|
|
// interp execution. The TypeTable's strings are stable
|
|
// for now but copying is the safe pattern.
|
|
const owned = self.alloc.dupe(u8, name) catch return error.CannotEvalComptime;
|
|
return .{ .value = .{ .string = owned } };
|
|
},
|
|
.type_eq => {
|
|
if (bi.args.len < 2) return bailDetail("comptime type_eq: needs two Type arguments");
|
|
const a = frame.getRef(bi.args[0]).asTypeId() orelse return bailDetail("comptime type_eq: first argument is not a Type value");
|
|
const b = frame.getRef(bi.args[1]).asTypeId() orelse return bailDetail("comptime type_eq: second argument is not a Type value");
|
|
return .{ .value = .{ .boolean = a == b } };
|
|
},
|
|
.type_is_unsigned => {
|
|
if (bi.args.len < 1) return bailDetail("comptime type_is_unsigned: missing argument");
|
|
const arg = frame.getRef(bi.args[0]);
|
|
// A bare `.type_tag`, an Any-boxed Type (`{ .any, tid }`,
|
|
// the `type_of(x)` shape), or an Any holding a runtime value
|
|
// (`{ tag, value }`, where the tag IS the value's type).
|
|
// `reflectTypeId` reads the runtime tag so
|
|
// `type_is_unsigned(av)` for `av : Any = 6` answers about
|
|
// `i64`, not the type whose index equals the payload.
|
|
const tid = arg.reflectTypeId() orelse
|
|
return bailDetail("comptime type_is_unsigned: argument is not a Type value or boxed value (expected `.type_tag` or Any aggregate)");
|
|
return .{ .value = .{ .boolean = self.module.types.isUnsignedInt(tid) } };
|
|
},
|
|
.has_impl => {
|
|
// has_impl at interp time needs access to the host's
|
|
// protocol-registration maps (protocol_thunk_map +
|
|
// param_impl_map). These live on `Lowering`, not on
|
|
// the Interpreter. Plumbing a queryable snapshot is
|
|
// its own slice — until then, bail loudly so the user
|
|
// gets a clear "not yet wired" message instead of a
|
|
// silent false. Static-arg has_impl still works via
|
|
// `tryConstBoolCondition` in lower.zig.
|
|
return bailDetail("comptime has_impl: interp-time evaluation not yet wired (use static type args for now — they fold at lower time)");
|
|
},
|
|
|
|
// ── Comptime type CONSTRUCTION primitives ────────────
|
|
.declare => {
|
|
const tbl = self.mint orelse
|
|
return bailDetail("comptime declare(): no type-mint target (declare/define are comptime-only — reached at runtime/emit?)");
|
|
if (bi.args.len != 1) return bailDetail("comptime declare(name): needs the name argument");
|
|
const nm = frame.getRef(bi.args[0]).asString(self) orelse
|
|
return bailDetail("comptime declare(): name is not a string");
|
|
const name_id = tbl.internString(nm);
|
|
// Lowering already registered this named forward slot (so a
|
|
// `*Name` self-reference in the body resolved); return THAT slot
|
|
// so `define` completes the same one. Mint it if somehow absent.
|
|
if (tbl.findByName(name_id)) |existing| return .{ .value = .{ .type_tag = existing } };
|
|
const info: types.TypeInfo = .{ .tagged_union = .{
|
|
.name = name_id,
|
|
.fields = &.{},
|
|
.tag_type = .i64,
|
|
} };
|
|
const tid = tbl.internNominal(info, 0);
|
|
return .{ .value = .{ .type_tag = tid } };
|
|
},
|
|
.define => {
|
|
const tbl = self.mint orelse
|
|
return bailDetail("comptime define(): no type-mint target (declare/define are comptime-only — reached at runtime/emit?)");
|
|
if (bi.args.len != 2) return bailDetail("comptime define(handle, info): needs exactly two arguments");
|
|
const handle = frame.getRef(bi.args[0]).asTypeId() orelse
|
|
return bailDetail("comptime define(): first argument is not a Type handle (use a `declare()` result)");
|
|
const info_val = frame.getRef(bi.args[1]);
|
|
return self.defineType(tbl, handle, info_val);
|
|
},
|
|
.type_info => {
|
|
// Reflect a type INTO a `TypeInfo` value — the inverse of
|
|
// `define`'s decode. Lowering already validated the arg is an
|
|
// enum/tagged-union and passed it as a `const_type`.
|
|
if (bi.args.len != 1) return bailDetail("comptime type_info: missing type argument");
|
|
const tid = frame.getRef(bi.args[0]).asTypeId() orelse
|
|
return bailDetail("comptime type_info: argument is not a Type value");
|
|
return self.reflectTypeInfo(tid);
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Build the `.enum(EnumInfo{ variants })` `TypeInfo` value for `tid` — the
|
|
/// exact shape `defineEnum` decodes, so `define(declare(n), type_info(T))`
|
|
/// round-trips. A `tagged_union` reflects each field as
|
|
/// `EnumVariant{ name, payload = field.ty }` (tagless variants already carry
|
|
/// `void`); a payloadless `@"enum"` reflects every variant with `void`.
|
|
/// Value layout mirrors how the interp evaluates the hand-written literal:
|
|
/// variant = { string(name), type_tag(payload) }
|
|
/// variants = { aggregate(variant…), int(len) } (slice fat pointer)
|
|
/// EnumInfo = { variants }
|
|
/// TypeInfo = { int(0), EnumInfo } (`.enum` tag = 0)
|
|
fn reflectTypeInfo(self: *Interpreter, tid: TypeId) InterpError!ExecResult {
|
|
var elems = std.ArrayList(Value).empty;
|
|
const info = self.module.types.get(tid);
|
|
// The TypeInfo variant tag (declaration order in `meta.sx`: `enum`=0,
|
|
// `struct`=1). Each member reflects as `{ string(name), type_tag(ty) }`
|
|
// regardless of kind — payload type for an enum variant, field type for a
|
|
// struct field (a payloadless `@"enum"` variant carries `void`).
|
|
const tag: i64 = switch (info) {
|
|
.tagged_union => |u| blk: {
|
|
for (u.fields) |f| {
|
|
const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime;
|
|
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime;
|
|
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
|
|
}
|
|
break :blk 0;
|
|
},
|
|
.@"enum" => |e| blk: {
|
|
for (e.variants) |vname| {
|
|
const nm = self.alloc.dupe(u8, self.module.types.getString(vname)) catch return error.CannotEvalComptime;
|
|
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = .void } }) catch return error.CannotEvalComptime;
|
|
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
|
|
}
|
|
break :blk 0;
|
|
},
|
|
.@"struct" => |s| blk: {
|
|
for (s.fields) |f| {
|
|
const nm = self.alloc.dupe(u8, self.module.types.getString(f.name)) catch return error.CannotEvalComptime;
|
|
const pair = self.alloc.dupe(Value, &.{ .{ .string = nm }, .{ .type_tag = f.ty } }) catch return error.CannotEvalComptime;
|
|
elems.append(self.alloc, .{ .aggregate = pair }) catch return error.CannotEvalComptime;
|
|
}
|
|
break :blk 1;
|
|
},
|
|
.tuple => |t| blk: {
|
|
// Tuple elements are POSITIONAL — bare `type_tag` values, not
|
|
// `{ name, type }` pairs (TupleInfo carries no field names).
|
|
for (t.fields) |elem_ty| {
|
|
elems.append(self.alloc, .{ .type_tag = elem_ty }) catch return error.CannotEvalComptime;
|
|
}
|
|
break :blk 2;
|
|
},
|
|
else => return bailDetail("comptime type_info: only enum / tagged-union / struct / tuple types reflect today"),
|
|
};
|
|
if (elems.items.len == 0) return bailDetail("comptime type_info: type has no members");
|
|
|
|
// Wrap: members → `{ data, len }` slice → info struct `{ members }` →
|
|
// TypeInfo `{ int(tag), info }`. Identical shape for `.enum` / `.struct`.
|
|
const members_slice = self.alloc.dupe(Value, &.{
|
|
.{ .aggregate = elems.items },
|
|
.{ .int = @intCast(elems.items.len) },
|
|
}) catch return error.CannotEvalComptime;
|
|
const inner = self.alloc.dupe(Value, &.{.{ .aggregate = members_slice }}) catch return error.CannotEvalComptime;
|
|
const typeinfo = self.alloc.dupe(Value, &.{ .{ .int = tag }, .{ .aggregate = inner } }) catch return error.CannotEvalComptime;
|
|
return .{ .value = .{ .aggregate = typeinfo } };
|
|
}
|
|
|
|
/// Complete a `declare()`d slot from a `TypeInfo` VALUE, dispatching on the
|
|
/// TypeInfo tag (`{ tag, payload }`): `0` → `.enum(EnumInfo)` (tagged_union),
|
|
/// `1` → `.struct(StructInfo)`. The tag is the variant index in `meta.sx`'s
|
|
/// `TypeInfo` enum declaration order (`enum` then `struct`).
|
|
fn defineType(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult {
|
|
const ti_fields = switch (info_val) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): info did not evaluate to a TypeInfo value"),
|
|
};
|
|
if (ti_fields.len != 2) return bailDetail("comptime define(): malformed TypeInfo value (expected `{ tag, info }`)");
|
|
const tag = ti_fields[0].asInt() orelse return bailDetail("comptime define(): TypeInfo tag is not an integer");
|
|
return switch (tag) {
|
|
0 => self.defineEnum(tbl, handle, info_val),
|
|
1 => self.defineStruct(tbl, handle, info_val),
|
|
2 => self.defineTuple(tbl, handle, info_val),
|
|
else => bailDetail("comptime define(): unknown TypeInfo variant (only `.enum` / `.struct` / `.tuple` are supported)"),
|
|
};
|
|
}
|
|
|
|
/// Complete a `declare()`d slot from a `TypeInfo` VALUE. The value is the
|
|
/// `.enum(EnumInfo)` tagged-union (`{ tag, EnumInfo }`), EnumInfo is
|
|
/// `{ variants }`, and each variant is `{ name: string, payload: Type }`.
|
|
/// Decodes those into a `tagged_union` byte-identical to a source enum's
|
|
/// `buildEnumInfo` output (default `i64` tag, no backing) and fills the slot
|
|
/// via `updatePreservingKey` (the handle's name + nominal id are unchanged).
|
|
/// Every decode failure is a loud bail — never a silent default.
|
|
fn defineEnum(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult {
|
|
// Unwrap TypeInfo `.enum(EnumInfo)` → EnumInfo `{ variants }`.
|
|
const ti_fields = switch (info_val) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): info did not evaluate to a TypeInfo value"),
|
|
};
|
|
if (ti_fields.len != 2) return bailDetail("comptime define(): only the `.enum(...)` TypeInfo variant is supported");
|
|
const einfo = ti_fields[1];
|
|
const einfo_fields = switch (einfo) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): `.enum` payload is not an EnumInfo struct value"),
|
|
};
|
|
// EnumInfo = `{ variants: []EnumVariant }`. The name was given to
|
|
// `declare` (it's already the slot's name) — `define` only fills the body.
|
|
if (einfo_fields.len != 1) return bailDetail("comptime define(): EnumInfo must have a `variants` field");
|
|
const elems = decodeVariantElements(einfo_fields[0]) orelse
|
|
return bailDetail("comptime define(): `variants` is not a slice/array of EnumVariant");
|
|
if (elems.len == 0) return bailDetail("comptime define(): enum has no variants");
|
|
|
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
for (elems) |elem| {
|
|
const ev = switch (elem) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): EnumVariant did not evaluate to a struct value"),
|
|
};
|
|
if (ev.len != 2) return bailDetail("comptime define(): EnumVariant must have `name` and `payload`");
|
|
const vname = ev[0].asString(self) orelse return bailDetail("comptime define(): EnumVariant `name` is not a string");
|
|
const payload_tid = ev[1].asTypeId() orelse return bailDetail("comptime define(): EnumVariant `payload` is not a Type value");
|
|
const vname_id = tbl.internString(vname);
|
|
// Reject a duplicate variant name loudly — two same-named variants
|
|
// make construction (`.a`) and matching ambiguous and would silently
|
|
// pick one. The name is dynamic, so set the bail detail directly
|
|
// (bailDetail takes a comptime string); evalComptimeType renders it.
|
|
for (fields.items) |existing| {
|
|
if (existing.name == vname_id) {
|
|
last_bail_detail = std.fmt.allocPrint(self.alloc, "comptime define(): duplicate variant name '{s}'", .{vname}) catch "comptime define(): duplicate variant name";
|
|
return error.CannotEvalComptime;
|
|
}
|
|
}
|
|
fields.append(self.alloc, .{ .name = vname_id, .ty = payload_tid }) catch return error.CannotEvalComptime;
|
|
}
|
|
|
|
// Complete the declared slot IN PLACE: it already has its name + nominal
|
|
// id (from `declare`); fill the body. Name/id unchanged → the intern key
|
|
// is stable, so `updatePreservingKey`.
|
|
const cur = tbl.get(handle);
|
|
if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d enum slot");
|
|
const full: types.TypeInfo = .{ .tagged_union = .{
|
|
.name = cur.tagged_union.name,
|
|
.fields = fields.items,
|
|
.tag_type = .i64,
|
|
.backing_type = null,
|
|
.explicit_tag_values = null,
|
|
.nominal_id = cur.tagged_union.nominal_id,
|
|
} };
|
|
tbl.updatePreservingKey(handle, full);
|
|
// Return the handle so the one-shot form chains: `T :: define(declare("T"), info)`.
|
|
return .{ .value = .{ .type_tag = handle } };
|
|
}
|
|
|
|
/// Complete a `declare()`d slot from a `.struct(StructInfo)` `TypeInfo` VALUE.
|
|
/// Mirror of `defineEnum` for structs: StructInfo is `{ fields }`, each field
|
|
/// `{ name: string, type: Type }`. Fills the (tagged_union-shaped) declare
|
|
/// slot in place as a `.@"struct"`, preserving its name + nominal id.
|
|
fn defineStruct(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult {
|
|
// Unwrap TypeInfo `.struct(StructInfo)` → StructInfo `{ fields }`.
|
|
const ti_fields = info_val.aggregate; // defineType already checked the shape
|
|
const sinfo = ti_fields[1];
|
|
const sinfo_fields = switch (sinfo) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): `.struct` payload is not a StructInfo struct value"),
|
|
};
|
|
if (sinfo_fields.len != 1) return bailDetail("comptime define(): StructInfo must have a `fields` field");
|
|
const elems = decodeVariantElements(sinfo_fields[0]) orelse
|
|
return bailDetail("comptime define(): `fields` is not a slice/array of StructField");
|
|
if (elems.len == 0) return bailDetail("comptime define(): struct has no fields");
|
|
|
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
for (elems) |elem| {
|
|
const sf = switch (elem) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): StructField did not evaluate to a struct value"),
|
|
};
|
|
if (sf.len != 2) return bailDetail("comptime define(): StructField must have `name` and `type`");
|
|
const fname = sf[0].asString(self) orelse return bailDetail("comptime define(): StructField `name` is not a string");
|
|
const fty = sf[1].asTypeId() orelse return bailDetail("comptime define(): StructField `type` is not a Type value");
|
|
const fname_id = tbl.internString(fname);
|
|
// Reject duplicate field names (a struct can't have two same-named
|
|
// fields). Dynamic name → set the detail directly (bailDetail is
|
|
// comptime-only); evalComptimeType renders it.
|
|
for (fields.items) |existing| {
|
|
if (existing.name == fname_id) {
|
|
last_bail_detail = std.fmt.allocPrint(self.alloc, "comptime define(): duplicate field name '{s}'", .{fname}) catch "comptime define(): duplicate field name";
|
|
return error.CannotEvalComptime;
|
|
}
|
|
}
|
|
fields.append(self.alloc, .{ .name = fname_id, .ty = fty }) catch return error.CannotEvalComptime;
|
|
}
|
|
|
|
// Complete the declare slot as a struct. It was minted as an (empty)
|
|
// tagged_union by `declare`; we keep its TypeId + name + nominal id but
|
|
// SWAP THE KIND to struct. A kind change moves the intern key, so use
|
|
// `replaceKeyedInfo` (re-keys) rather than `updatePreservingKey` (which
|
|
// asserts the key is unchanged — true for the enum path, false here).
|
|
const cur = tbl.get(handle);
|
|
if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d slot");
|
|
const full: types.TypeInfo = .{ .@"struct" = .{
|
|
.name = cur.tagged_union.name,
|
|
.fields = fields.items,
|
|
.nominal_id = cur.tagged_union.nominal_id,
|
|
} };
|
|
tbl.replaceKeyedInfo(handle, full);
|
|
return .{ .value = .{ .type_tag = handle } };
|
|
}
|
|
|
|
/// Complete a `declare()`d slot from a `.tuple(TupleInfo)` `TypeInfo` VALUE.
|
|
/// TupleInfo is `{ elements }`, each element a bare `Type` (positional, no
|
|
/// name). Tuples are structural, so the declared NAME is vestigial — we still
|
|
/// complete the slot in place (so `define` returns the handle, like the
|
|
/// enum/struct paths) via `replaceKeyedInfo` (kind change: tagged_union slot →
|
|
/// tuple, re-keyed structurally by element types).
|
|
fn defineTuple(self: *Interpreter, tbl: *types.TypeTable, handle: TypeId, info_val: Value) InterpError!ExecResult {
|
|
const ti_fields = info_val.aggregate; // defineType already checked the shape
|
|
const tinfo = ti_fields[1];
|
|
const tinfo_fields = switch (tinfo) {
|
|
.aggregate => |f| f,
|
|
else => return bailDetail("comptime define(): `.tuple` payload is not a TupleInfo struct value"),
|
|
};
|
|
if (tinfo_fields.len != 1) return bailDetail("comptime define(): TupleInfo must have an `elements` field");
|
|
const elems = decodeVariantElements(tinfo_fields[0]) orelse
|
|
return bailDetail("comptime define(): `elements` is not a slice/array of Type");
|
|
if (elems.len == 0) return bailDetail("comptime define(): tuple has no elements");
|
|
|
|
var field_tys = std.ArrayList(TypeId).empty;
|
|
for (elems) |elem| {
|
|
const ety = elem.asTypeId() orelse return bailDetail("comptime define(): tuple element is not a Type value");
|
|
field_tys.append(self.alloc, ety) catch return error.CannotEvalComptime;
|
|
}
|
|
|
|
const cur = tbl.get(handle);
|
|
if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d slot");
|
|
const full: types.TypeInfo = .{ .tuple = .{ .fields = field_tys.items, .names = null } };
|
|
tbl.replaceKeyedInfo(handle, full);
|
|
return .{ .value = .{ .type_tag = handle } };
|
|
}
|
|
};
|
|
|
|
/// Normalize an interpreter value into the list of EnumVariant element values.
|
|
/// A `[]EnumVariant` slice evaluates to a `{ data, len }` aggregate (`len` an
|
|
/// int); a `[N]EnumVariant` array literal evaluates to the element aggregate
|
|
/// directly. Returns null for any other shape (the caller bails loudly).
|
|
fn decodeVariantElements(result: Value) ?[]const Value {
|
|
const fields = switch (result) {
|
|
.aggregate => |f| f,
|
|
else => return null,
|
|
};
|
|
// Slice fat pointer `{ data, len }`: a 2-field aggregate whose 2nd field is
|
|
// an integer length. (A 2-VARIANT array can't collide — its 2nd field is an
|
|
// EnumVariant aggregate, so `asInt` is null.)
|
|
if (fields.len == 2) {
|
|
if (fields[1].asInt()) |len_i| {
|
|
const len: usize = @intCast(len_i);
|
|
switch (fields[0]) {
|
|
.aggregate => |arr| return if (len <= arr.len) arr[0..len] else null,
|
|
else => return null,
|
|
}
|
|
}
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
// ── Frame ───────────────────────────────────────────────────────────────
|
|
// Holds SSA values (by Ref index) and local mutable slots (for alloca).
|
|
|
|
const Frame = struct {
|
|
refs: []Value,
|
|
ref_alloc: Allocator,
|
|
slots: std.ArrayList(Value),
|
|
|
|
/// Create a frame pre-allocated with `num_refs` slots (all undef).
|
|
fn initSized(alloc: Allocator, num_refs: u32) Frame {
|
|
const refs = alloc.alloc(Value, num_refs) catch unreachable;
|
|
@memset(refs, .undef);
|
|
return .{
|
|
.refs = refs,
|
|
.ref_alloc = alloc,
|
|
.slots = std.ArrayList(Value).empty,
|
|
};
|
|
}
|
|
|
|
fn deinit(self: *Frame) void {
|
|
self.ref_alloc.free(self.refs);
|
|
}
|
|
|
|
fn setRef(self: *Frame, idx: u32, val: Value) void {
|
|
if (idx < self.refs.len) {
|
|
self.refs[idx] = val;
|
|
}
|
|
}
|
|
|
|
fn getRef(self: *const Frame, ref: Ref) Value {
|
|
if (ref.isNone()) return .void_val;
|
|
const idx = ref.index();
|
|
if (idx >= self.refs.len) return .undef;
|
|
return self.refs[idx];
|
|
}
|
|
|
|
fn allocSlot(self: *Frame, alloc: Allocator) u32 {
|
|
const idx: u32 = @intCast(self.slots.items.len);
|
|
self.slots.append(alloc, .undef) catch unreachable;
|
|
return idx;
|
|
}
|
|
|
|
fn loadSlot(self: *const Frame, slot: u32) Value {
|
|
if (slot >= self.slots.items.len) return .undef;
|
|
return self.slots.items[slot];
|
|
}
|
|
|
|
fn storeSlot(self: *Frame, slot: u32, val: Value) void {
|
|
if (slot < self.slots.items.len) {
|
|
self.slots.items[slot] = val;
|
|
}
|
|
}
|
|
};
|
|
|