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 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; } /// 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, // 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 // :" 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; 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, }; } 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.global_values.deinit(); self.hooks.deinit(); } /// Write `val` to the raw host address `addr`. Used when the /// protocol-dispatch chain bottoms out at a foreign-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) InterpError!void { _ = self; const dst: [*]u8 = @ptrFromInt(@as(usize, @bitCast(addr))); switch (val) { .int => |v| { const bytes = std.mem.toBytes(v); @memcpy(dst[0..bytes.len], &bytes); }, .float => |v| { const bytes = std.mem.toBytes(v); @memcpy(dst[0..bytes.len], &bytes); }, .boolean => |v| { dst[0] = if (v) 1 else 0; }, .null_val => { const zero: u64 = 0; const bytes = std.mem.toBytes(zero); @memcpy(dst[0..bytes.len], &bytes); }, else => return error.CannotEvalComptime, } } // ── 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"); const dealloc_thunk_name = tbl.internString("__thunk_CAllocator_Allocator_dealloc"); 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] = &.{}; } } 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 marshalForeignArg(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 foreign 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 foreign 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 error.TypeError, } } return error.TypeError; }, else => error.TypeError, }; } fn callForeign(self: *Interpreter, func: *const inst_mod.Function, args: []const Value) InterpError!Value { const name = self.module.types.getString(func.name); const symbol = (host_ffi.lookupSymbol(self.alloc, name) catch return error.CannotEvalComptime) orelse { return error.CannotEvalComptime; }; var packed_args: [8]usize = undefined; if (args.len > packed_args.len) return error.CannotEvalComptime; 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.marshalForeignArg(a, &tmp) catch return error.TypeError; } const argv = packed_args[0..args.len]; // Variadic foreign 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 == .s8 or ret == .s16 or ret == .s32 or ret == .s64 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 foreign symbols like // `puts`, `getenv`, `posix_spawn`, etc. return self.callForeign(func, args); } // 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 }, // ── 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 error.TypeError, } }; }, // ── 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); 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 }, else => return error.CannotEvalComptime, } }, .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 foreign call, e.g. libc_malloc). // 8-byte stride assumed — covers the s64/pointer/f64 cases // sx hits via comptime protocol erasure. Aggregate stores // unpack and recurse. .int => |p| { try storeAtRawPtr(self, p, val); }, // 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 error.CannotEvalComptime, } 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; }, else => return error.TypeError, } }, // ── 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 error.TypeError; return .{ .value = fields[0] }; }, else => return error.TypeError, } }, .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 error.TypeError, } }, // ── 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 error.CannotEvalComptime, // Same story for JNI — no JVM at compile time. .jni_msg_send => return error.CannotEvalComptime, // ── 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, // ── Heap operations ───────────────────────────────── .heap_alloc => |u| { const size_val = frame.getRef(u.operand); const size: usize = @intCast(size_val.asInt() orelse return error.TypeError); const hp = self.heapAlloc(size); return .{ .value = .{ .heap_ptr = hp } }; }, .heap_free => |u| { const ptr = frame.getRef(u.operand); switch (ptr) { .heap_ptr => |hp| self.heapFree(hp), else => {}, } return .{ .value = .void_val }; }, // ── 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 error.CannotEvalComptime; return .{ .value = result }; } return .{ .value = .void_val }; } 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 } }; }, else => return error.CannotEvalComptime, } }, // ── 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] }; }, else => return error.CannotEvalComptime, } }, .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 error.CannotEvalComptime, } }, .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 error.CannotEvalComptime, } }, .subslice => |sub| { const base = frame.getRef(sub.base); const lo_val = frame.getRef(sub.lo); const hi_val = frame.getRef(sub.hi); const lo: usize = @intCast(lo_val.asInt() orelse return error.TypeError); const hi: usize = @intCast(hi_val.asInt() orelse return error.TypeError); if (base.asString(self)) |s| { if (hi > s.len) return error.OutOfBounds; return .{ .value = .{ .string = s[lo..hi] } }; } return error.CannotEvalComptime; }, // ── 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) }, else => return .{ .value = val }, } }, // ── 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; }, else => return .{ .value = val }, } }, // ── Reflection ───────────────────────────────────── .field_name_get => |fr| { const idx_val = frame.getRef(fr.index); const idx: usize = @intCast(switch (idx_val) { .int => |i| i, else => return error.CannotEvalComptime, }); 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 error.CannotEvalComptime, }; if (idx >= fields.len) return error.OutOfBounds; const name = self.module.types.getString(fields[idx].name); return .{ .value = .{ .string = name } }; }, .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 error.CannotEvalComptime, }); 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 error.CannotEvalComptime, }; 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 error.CannotEvalComptime, } }, // ── 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 error.CannotEvalComptime; }, .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 foreign 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 error.CannotEvalComptime; }, .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 — same byte-addressed offset // semantics as the aggregate{int_ptr, ...} branch. .int => |p| { const offset = idx.asInt() orelse return error.TypeError; return .{ .value = .{ .int = p + offset } }; }, else => return error.CannotEvalComptime, } }, // ── 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 error.CannotEvalComptime, } }, else => return error.CannotEvalComptime, } }, // ── 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 error.CannotEvalComptime, } }, // 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, .closure_create, .union_get, .union_gep, .vec_splat, .vec_extract, .vec_insert => { return error.CannotEvalComptime; }, } } // ── 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 error.TypeError; } // ── 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, }; } } return error.TypeError; } // ── 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; } // ── 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, .type_of, .alloc, .dealloc => { return error.CannotEvalComptime; }, } } }; // ── 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; } } };