ir
This commit is contained in:
541
src/ir/interp.zig
Normal file
541
src/ir/interp.zig
Normal file
@@ -0,0 +1,541 @@
|
||||
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,
|
||||
|
||||
pub const ClosureVal = struct {
|
||||
func: FuncId,
|
||||
env: ?[]const Value,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Error ───────────────────────────────────────────────────────────────
|
||||
|
||||
pub const InterpError = error{
|
||||
CannotEvalComptime,
|
||||
TypeError,
|
||||
OutOfBounds,
|
||||
DivisionByZero,
|
||||
StackOverflow,
|
||||
Unreachable,
|
||||
};
|
||||
|
||||
// ── Interpreter ─────────────────────────────────────────────────────────
|
||||
|
||||
pub const Interpreter = struct {
|
||||
module: *const Module,
|
||||
alloc: Allocator,
|
||||
output: std.ArrayList(u8),
|
||||
call_depth: u32 = 0,
|
||||
max_call_depth: u32 = 256,
|
||||
|
||||
pub fn init(module: *const Module, alloc: Allocator) Interpreter {
|
||||
return .{
|
||||
.module = module,
|
||||
.alloc = alloc,
|
||||
.output = std.ArrayList(u8).empty,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Interpreter) void {
|
||||
self.output.deinit(self.alloc);
|
||||
}
|
||||
|
||||
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) return error.CannotEvalComptime;
|
||||
if (func.blocks.items.len == 0) return error.CannotEvalComptime;
|
||||
|
||||
var frame = Frame.init(self.alloc);
|
||||
defer frame.deinit();
|
||||
|
||||
// Bind parameters as initial refs
|
||||
for (args) |arg| {
|
||||
frame.pushRef(self.alloc, arg);
|
||||
}
|
||||
|
||||
// Start at the entry block (index 0)
|
||||
var current_block: BlockId = BlockId.fromIndex(0);
|
||||
var block_args: []const Value = &.{};
|
||||
|
||||
while (true) {
|
||||
const block = &func.blocks.items[current_block.index()];
|
||||
|
||||
// Bind block params
|
||||
for (block_args) |arg| {
|
||||
frame.pushRef(self.alloc, arg);
|
||||
}
|
||||
|
||||
for (block.insts.items) |*instruction| {
|
||||
const result = try self.execInst(instruction, &frame, ¤t_block, &block_args);
|
||||
switch (result) {
|
||||
.value => |val| frame.pushRef(self.alloc, val),
|
||||
.branch => break, // current_block and block_args updated by execInst
|
||||
.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) } },
|
||||
|
||||
// ── 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| return .{ .value = frame.loadSlot(slot) },
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
.store => |s| {
|
||||
const ptr = frame.getRef(s.ptr);
|
||||
const val = frame.getRef(s.val);
|
||||
switch (ptr) {
|
||||
.slot_ptr => |slot| frame.storeSlot(slot, val),
|
||||
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| {
|
||||
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,
|
||||
}
|
||||
},
|
||||
|
||||
// ── 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,
|
||||
|
||||
// ── 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 => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// ── Frame ───────────────────────────────────────────────────────────────
|
||||
// Holds SSA values (by Ref index) and local mutable slots (for alloca).
|
||||
|
||||
const Frame = struct {
|
||||
refs: std.ArrayList(Value),
|
||||
slots: std.ArrayList(Value),
|
||||
|
||||
fn init(alloc: Allocator) Frame {
|
||||
_ = alloc;
|
||||
return .{
|
||||
.refs = std.ArrayList(Value).empty,
|
||||
.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;
|
||||
}
|
||||
|
||||
fn pushRef(self: *Frame, alloc: Allocator, val: Value) void {
|
||||
self.refs.append(alloc, val) catch unreachable;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user