This commit is contained in:
agra
2026-02-26 14:46:21 +02:00
parent dd14f1206b
commit 2552882ce6
14 changed files with 6433 additions and 159 deletions

View File

@@ -32,12 +32,19 @@ pub const Value = union(enum) {
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,
@@ -63,6 +70,29 @@ pub const Value = union(enum) {
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 ───────────────────────────────────────────────────────────────
@@ -85,16 +115,93 @@ pub const Interpreter = struct {
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),
pub fn init(module: *const Module, alloc: Allocator) Interpreter {
return .{
.module = module,
.alloc = alloc,
.output = std.ArrayList(u8).empty,
.heap = std.ArrayList([]u8).empty,
.global_values = std.AutoHashMap(u32, Value).init(alloc),
};
}
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();
}
// ── 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 {
@@ -103,15 +210,22 @@ pub const Interpreter = struct {
defer self.call_depth -= 1;
const func = self.module.getFunction(func_id);
if (func.is_extern) return error.CannotEvalComptime;
if (func.blocks.items.len == 0) return error.CannotEvalComptime;
if (func.is_extern or func.blocks.items.len == 0) {
return error.CannotEvalComptime;
}
var frame = Frame.init(self.alloc);
// 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
for (args) |arg| {
frame.pushRef(self.alloc, arg);
// 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)
@@ -119,18 +233,40 @@ pub const Interpreter = struct {
var block_args: []const Value = &.{};
while (true) {
const block = &func.blocks.items[current_block.index()];
const block_idx = current_block.index();
const block = &func.blocks.items[block_idx];
var ref_counter: u32 = block.first_ref;
// Bind block params
for (block_args) |arg| {
frame.pushRef(self.alloc, arg);
// 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| {
const result = try self.execInst(instruction, &frame, &current_block, &block_args);
// 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, &current_block, &block_args) catch |err| {
return err;
};
switch (result) {
.value => |val| frame.pushRef(self.alloc, val),
.branch => break, // current_block and block_args updated by execInst
.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,
}
@@ -229,7 +365,14 @@ pub const Interpreter = struct {
.load => |u| {
const ptr = frame.getRef(u.operand);
switch (ptr) {
.slot_ptr => |slot| return .{ .value = frame.loadSlot(slot) },
.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,
}
},
@@ -237,7 +380,20 @@ pub const Interpreter = struct {
const ptr = frame.getRef(s.ptr);
const val = frame.getRef(s.val);
switch (ptr) {
.slot_ptr => |slot| frame.storeSlot(slot, val),
.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 };
@@ -252,12 +408,32 @@ pub const Interpreter = struct {
return .{ .value = .{ .aggregate = fields } };
},
.struct_get => |fa| {
const base = frame.getRef(fa.base);
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,
}
},
@@ -392,8 +568,360 @@ pub const Interpreter = struct {
.ret_void => return .ret_nothing,
.@"unreachable" => return error.Unreachable,
// ── Not evaluable at comptime ───────────────────────
.heap_alloc, .heap_free, .call_indirect, .call_closure, .call_builtin, .protocol_call_dynamic, .protocol_erase, .closure_create, .context_load, .context_store, .context_save, .context_restore, .global_get, .global_set, .box_any, .unbox_any, .struct_gep, .union_get, .union_gep, .index_get, .index_gep, .length, .data_ptr, .subslice, .array_to_slice, .tuple_init, .tuple_get, .addr_of, .deref, .vec_splat, .vec_extract, .vec_insert, .bit_and, .bit_or, .bit_xor, .bit_not, .shl, .shr, .placeholder => {
// ── 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);
},
// ── 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,
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,
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_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;
},
}
@@ -487,38 +1015,241 @@ pub const Interpreter = struct {
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 };
},
};
}
// ── 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) } };
},
.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: std.ArrayList(Value),
refs: []Value,
ref_alloc: Allocator,
slots: std.ArrayList(Value),
fn init(alloc: Allocator) Frame {
_ = alloc;
/// 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 = std.ArrayList(Value).empty,
.refs = refs,
.ref_alloc = alloc,
.slots = std.ArrayList(Value).empty,
};
}
fn deinit(self: *Frame) void {
// We use the interpreter's allocator for everything — it's an arena-like pattern.
// Actual cleanup handled by the test allocator.
_ = self;
self.ref_alloc.free(self.refs);
}
fn pushRef(self: *Frame, alloc: Allocator, val: Value) void {
self.refs.append(alloc, val) catch unreachable;
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.items.len) return .undef;
return self.refs.items[idx];
if (idx >= self.refs.len) return .undef;
return self.refs[idx];
}
fn allocSlot(self: *Frame, alloc: Allocator) u32 {