New `.jni_msg_send` IR opcode carrying `{env, target, name, sig,
args[], is_static}`. `lowerFfiIntrinsicCall` now dispatches on
`fic.kind`: `.objc_call` keeps the existing path; `.jni_call` and
`.jni_static_call` route through `lowerJniCall`, which emits the new
opcode.
emit_llvm.zig expands `.jni_msg_send` into the JNI vtable
indirection:
%ifs = load ptr, %env ; vtable
%get_obj_class = load ptr, gep(%ifs, i32 31)
%cls = call ptr %get_obj_class(%env, %target)
%get_method_id = load ptr, gep(%ifs, i32 33)
%mid = call ptr %get_method_id(%env, %cls, %name, %sig)
%call_void_method = load ptr, gep(%ifs, i32 61)
call void %call_void_method(%env, %target, %mid, args...)
Per step 1.15's scope: only `.jni_call` (instance) + `void` return
are wired through the switch. `.jni_static_call` (1.23) and the
non-void returns (1.18–1.22) drop to a placeholder `LLVMGetUndef` so
the build doesn't fault — the next-step commits flip those arms one
shape at a time. Method-ID caching is step 1.17.
Two small helpers landed alongside:
- `loadJniFn(ifs, offset, name)` — GEP into the vtable + load.
- `extractSlicePtr(val)` — string literals lower as `{ptr, i64}`
slices in sx IR; JNI's `GetMethodID` expects raw C strings, so
this extracts field 0 when the source is a slice.
Android cross-compile now passes for `examples/ffi-jni-call-02-void.sx`
(2/2 cross targets green). Host run_examples still passes 112/112.
Chess iOS-sim + Android both compile clean.
1358 lines
60 KiB
Zig
1358 lines
60 KiB
Zig
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
const types = @import("types.zig");
|
|
const inst_mod = @import("inst.zig");
|
|
const mod_mod = @import("module.zig");
|
|
|
|
const 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 };
|
|
},
|
|
|
|
// 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 => {
|
|
// 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;
|
|
}
|
|
}
|
|
};
|
|
|