This commit is contained in:
agra
2026-02-26 02:25:02 +02:00
parent 7209e8e69d
commit dd14f1206b
23 changed files with 5433 additions and 9 deletions

541
src/ir/interp.zig Normal file
View 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, &current_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;
}
}
};