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 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, 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, 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; // ── 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, 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(); } // ── 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; } 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) { return error.CannotEvalComptime; } // 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(); // Bind parameters as initial refs (indices 0..N-1) for (args, 0..) |arg, i| { frame.setRef(@intCast(i), 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| { 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 }; }, 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); }, 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| { args[i] = frame.getRef(ref); } const result = try self.call(c.callee, args); return .{ .value = result }; }, // ── 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 => { // Address-of-global not meaningful in interpreter 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)), } } }; }, 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), } } }; }, 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] = frame.getRef(ref); } const result = try self.call(fid, args); return .{ .value = result }; }, else => return error.CannotEvalComptime, } }, // ── Not yet evaluable at comptime ────────────────── .call_closure, .protocol_call_dynamic, .protocol_erase, .closure_create, .context_load, .context_store, .context_save, .context_restore, .union_get, .union_gep, .vec_splat, .vec_extract, .vec_insert, .placeholder => { 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 ──────────────────────────────────── /// 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 }; }, }; } // ── 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; 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; 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) { // Clone the aggregate and update the field const new_fields = self.alloc.alloc(Value, parent_fields.len) catch return false; @memcpy(new_fields, parent_fields); 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 { switch (bi.builtin) { .malloc => { const size_val = frame.getRef(bi.args[0]); const size: usize = @intCast(size_val.asInt() orelse return error.TypeError); const hp = self.heapAlloc(size); return .{ .value = .{ .heap_ptr = hp } }; }, .free => { const ptr = frame.getRef(bi.args[0]); switch (ptr) { .heap_ptr => |hp| self.heapFree(hp), else => {}, } return .{ .value = .void_val }; }, .memcpy => { const dst = frame.getRef(bi.args[0]); const src = frame.getRef(bi.args[1]); const len_val = frame.getRef(bi.args[2]); const len: usize = @intCast(len_val.asInt() orelse return error.TypeError); const dst_hp = switch (dst) { .heap_ptr => |hp| hp, else => return error.CannotEvalComptime, }; // Get source bytes const src_bytes: []const u8 = switch (src) { .heap_ptr => |hp| self.heapSlice(hp) orelse return error.CannotEvalComptime, .string => |s| s, else => return error.CannotEvalComptime, }; self.heapMemcpy(dst_hp, src_bytes, len); return .{ .value = .{ .heap_ptr = dst_hp } }; }, .memset => { const dst = frame.getRef(bi.args[0]); const val = frame.getRef(bi.args[1]); const len_val = frame.getRef(bi.args[2]); const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF); const len: usize = @intCast(len_val.asInt() orelse return error.TypeError); switch (dst) { .heap_ptr => |hp| self.heapMemset(hp, byte, len), else => {}, } return .{ .value = .void_val }; }, .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 } }; }, .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; } } };