comptime VM: flat-memory machine + executor + Reg<->Value bridge + tryEval
Phase 1 of the flat-memory comptime VM (current/PLAN-COMPILER-VM.md),
built standalone + unit-tested with the legacy interpreter still live and
the corpus untouched (688 green).
src/ir/comptime_vm.zig:
- Machine: one linear byte memory (comptime stack+heap) with a bump/stack
allocator (mark/reset), scalar readWord/writeWord (1/2/4/8 LE) + byte
views; addr 0 reserved as null_addr. Frame: a Ref-indexed register file
(Reg = raw u64: immediate scalar bits OR an Addr). Target-aware layout
comes from the type table, so cross-compilation stays correct.
- Vm executor over the SAME SSA IR, mirroring the legacy interp's scalar
semantics (i64 wrapping/signed, f64). Ported: constants, arithmetic,
comparison, logical, conversions, control flow (br/cond_br/ret + block
params); structs (alloca/load/store/struct_init/get/gep at target
offsets); tuples; arrays (index_get/gep, length); slices+strings as
{ptr,len} fat pointers (const_string, data_ptr, subslice,
array_to_slice, str_eq/ne, index-through-slice); optionals (pointer and
{T,i1} shapes); payloadless enums; deref/addr_of; direct + recursive
call over the shared flat memory (depth-guarded). The value model: a
word for scalars/pointers, by-address for aggregates (a struct's value
IS its Addr). Any unported op bails loudly (error.Unsupported + detail).
- Reg<->Value boundary bridge (valueToReg / regToValue) + tryEval, the
hybrid-wiring entry point: run a comptime fn on the VM, return a legacy
Value or null to fall back. Transitional, for the legacy interop edge.
Registered in the ir.zig barrel.
This commit is contained in:
817
src/ir/comptime_vm.test.zig
Normal file
817
src/ir/comptime_vm.test.zig
Normal file
@@ -0,0 +1,817 @@
|
||||
// Tests for the flat-memory comptime machine (Phase 1 of PLAN-COMPILER-VM.md).
|
||||
|
||||
const std = @import("std");
|
||||
const vm = @import("comptime_vm.zig");
|
||||
const inst_mod = @import("inst.zig");
|
||||
const types = @import("types.zig");
|
||||
const Inst = inst_mod.Inst;
|
||||
const Op = inst_mod.Op;
|
||||
const Ref = inst_mod.Ref;
|
||||
const BlockId = inst_mod.BlockId;
|
||||
const FuncId = inst_mod.FuncId;
|
||||
const Function = inst_mod.Function;
|
||||
const Block = inst_mod.Block;
|
||||
const Module = @import("module.zig").Module;
|
||||
const Value = @import("interp.zig").Value;
|
||||
const TypeId = types.TypeId;
|
||||
|
||||
const dummy: types.StringId = @enumFromInt(0);
|
||||
|
||||
fn ref(i: u32) Ref {
|
||||
return Ref.fromIndex(i);
|
||||
}
|
||||
fn param(ty: TypeId) Function.Param {
|
||||
return .{ .name = dummy, .ty = ty };
|
||||
}
|
||||
fn inst(op: Op, ty: TypeId) Inst {
|
||||
return .{ .op = op, .ty = ty };
|
||||
}
|
||||
fn fromI64(v: i64) vm.Reg {
|
||||
return @bitCast(v);
|
||||
}
|
||||
fn toI64(w: vm.Reg) i64 {
|
||||
return @bitCast(w);
|
||||
}
|
||||
fn fromF64(v: f64) vm.Reg {
|
||||
return @bitCast(v);
|
||||
}
|
||||
fn toF64(w: vm.Reg) f64 {
|
||||
return @bitCast(w);
|
||||
}
|
||||
|
||||
/// Minimal hand-builder for tiny IR functions. Blocks MUST be fully populated in
|
||||
/// order (a block's `first_ref` is fixed at creation from the running ref count),
|
||||
/// and branch targets reference block indices (0,1,2,…) which are sequential.
|
||||
const Fb = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
func: Function,
|
||||
next_ref: u32,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, params: []const Function.Param, ret: TypeId) Fb {
|
||||
return .{ .alloc = alloc, .func = Function.init(dummy, params, ret), .next_ref = @intCast(params.len) };
|
||||
}
|
||||
fn deinit(self: *Fb) void {
|
||||
self.func.deinit(self.alloc);
|
||||
}
|
||||
/// Create a block (with `bparams` block-parameter types); returns its index.
|
||||
fn block(self: *Fb, bparams: []const TypeId) u32 {
|
||||
var blk = Block.init(dummy, bparams);
|
||||
blk.first_ref = self.next_ref;
|
||||
self.func.blocks.append(self.alloc, blk) catch @panic("OOM");
|
||||
return @intCast(self.func.blocks.items.len - 1);
|
||||
}
|
||||
/// Append an instruction to block `b`; returns the Ref index of its result.
|
||||
fn add(self: *Fb, b: u32, i: Inst) u32 {
|
||||
self.func.blocks.items[b].insts.append(self.alloc, i) catch @panic("OOM");
|
||||
const r = self.next_ref;
|
||||
self.next_ref += 1;
|
||||
return r;
|
||||
}
|
||||
};
|
||||
|
||||
test "comptime_vm exec: integer add of two params" {
|
||||
const params = [_]Function.Param{ param(.i64), param(.i64) };
|
||||
var fb = Fb.init(std.testing.allocator, ¶ms, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const sum = fb.add(b0, inst(.{ .add = .{ .lhs = ref(0), .rhs = ref(1) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(sum) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(std.testing.allocator);
|
||||
defer v.deinit();
|
||||
const out = try v.run(&fb.func, &.{ fromI64(3), fromI64(40) });
|
||||
try std.testing.expectEqual(@as(i64, 43), toI64(out));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: f64 arithmetic (a*2.0 + 1.0)" {
|
||||
const params = [_]Function.Param{param(.f64)};
|
||||
var fb = Fb.init(std.testing.allocator, ¶ms, .f64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const two = fb.add(b0, inst(.{ .const_float = 2.0 }, .f64));
|
||||
const prod = fb.add(b0, inst(.{ .mul = .{ .lhs = ref(0), .rhs = ref(two) } }, .f64));
|
||||
const one = fb.add(b0, inst(.{ .const_float = 1.0 }, .f64));
|
||||
const res = fb.add(b0, inst(.{ .add = .{ .lhs = ref(prod), .rhs = ref(one) } }, .f64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(res) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(std.testing.allocator);
|
||||
defer v.deinit();
|
||||
const out = try v.run(&fb.func, &.{fromF64(3.0)});
|
||||
try std.testing.expectEqual(@as(f64, 7.0), toF64(out));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: comparison + cond_br selects a branch" {
|
||||
// f(a) = if a < 10 then 100 else 200
|
||||
const params = [_]Function.Param{param(.i64)};
|
||||
var fb = Fb.init(std.testing.allocator, ¶ms, .i64);
|
||||
defer fb.deinit();
|
||||
|
||||
const b0 = fb.block(&.{});
|
||||
const ten = fb.add(b0, inst(.{ .const_int = 10 }, .i64));
|
||||
const c = fb.add(b0, inst(.{ .cmp_lt = .{ .lhs = ref(0), .rhs = ref(ten) } }, .bool));
|
||||
_ = fb.add(b0, inst(.{ .cond_br = .{ .cond = ref(c), .then_target = BlockId.fromIndex(1), .then_args = &.{}, .else_target = BlockId.fromIndex(2), .else_args = &.{} } }, .void));
|
||||
|
||||
const b1 = fb.block(&.{});
|
||||
const x = fb.add(b1, inst(.{ .const_int = 100 }, .i64));
|
||||
_ = fb.add(b1, inst(.{ .ret = .{ .operand = ref(x) } }, .void));
|
||||
|
||||
const b2 = fb.block(&.{});
|
||||
const y = fb.add(b2, inst(.{ .const_int = 200 }, .i64));
|
||||
_ = fb.add(b2, inst(.{ .ret = .{ .operand = ref(y) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(std.testing.allocator);
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 100), toI64(try v.run(&fb.func, &.{fromI64(5)})));
|
||||
try std.testing.expectEqual(@as(i64, 200), toI64(try v.run(&fb.func, &.{fromI64(15)})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: loop with block params sums i..1" {
|
||||
// sum=0; i=n; while i>0 { sum+=i; i-=1 } return sum → n*(n+1)/2
|
||||
const params = [_]Function.Param{param(.i64)};
|
||||
var fb = Fb.init(std.testing.allocator, ¶ms, .i64);
|
||||
defer fb.deinit();
|
||||
const loop_p = [_]TypeId{ .i64, .i64 }; // (sum, i)
|
||||
const exit_p = [_]TypeId{.i64}; // (sum)
|
||||
|
||||
// b0 entry: br b1(0, n)
|
||||
const b0 = fb.block(&.{});
|
||||
const zero = fb.add(b0, inst(.{ .const_int = 0 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .br = .{ .target = BlockId.fromIndex(1), .args = &.{ ref(zero), ref(0) } } }, .void));
|
||||
|
||||
// b1 header(sum, i): if i>0 -> b2(sum,i) else b3(sum)
|
||||
const b1 = fb.block(&loop_p);
|
||||
const sum_h = fb.add(b1, inst(.{ .block_param = .{ .block = BlockId.fromIndex(1), .param_index = 0 } }, .i64));
|
||||
const i_h = fb.add(b1, inst(.{ .block_param = .{ .block = BlockId.fromIndex(1), .param_index = 1 } }, .i64));
|
||||
const z2 = fb.add(b1, inst(.{ .const_int = 0 }, .i64));
|
||||
const cond = fb.add(b1, inst(.{ .cmp_gt = .{ .lhs = ref(i_h), .rhs = ref(z2) } }, .bool));
|
||||
_ = fb.add(b1, inst(.{ .cond_br = .{ .cond = ref(cond), .then_target = BlockId.fromIndex(2), .then_args = &.{ ref(sum_h), ref(i_h) }, .else_target = BlockId.fromIndex(3), .else_args = &.{ref(sum_h)} } }, .void));
|
||||
|
||||
// b2 body(sum, i): br b1(sum+i, i-1)
|
||||
const b2 = fb.block(&loop_p);
|
||||
const sum_b = fb.add(b2, inst(.{ .block_param = .{ .block = BlockId.fromIndex(2), .param_index = 0 } }, .i64));
|
||||
const i_b = fb.add(b2, inst(.{ .block_param = .{ .block = BlockId.fromIndex(2), .param_index = 1 } }, .i64));
|
||||
const ns = fb.add(b2, inst(.{ .add = .{ .lhs = ref(sum_b), .rhs = ref(i_b) } }, .i64));
|
||||
const one = fb.add(b2, inst(.{ .const_int = 1 }, .i64));
|
||||
const ni = fb.add(b2, inst(.{ .sub = .{ .lhs = ref(i_b), .rhs = ref(one) } }, .i64));
|
||||
_ = fb.add(b2, inst(.{ .br = .{ .target = BlockId.fromIndex(1), .args = &.{ ref(ns), ref(ni) } } }, .void));
|
||||
|
||||
// b3 exit(sum): ret sum
|
||||
const b3 = fb.block(&exit_p);
|
||||
const sum_e = fb.add(b3, inst(.{ .block_param = .{ .block = BlockId.fromIndex(3), .param_index = 0 } }, .i64));
|
||||
_ = fb.add(b3, inst(.{ .ret = .{ .operand = ref(sum_e) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(std.testing.allocator);
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 15), toI64(try v.run(&fb.func, &.{fromI64(5)}))); // 5+4+3+2+1
|
||||
try std.testing.expectEqual(@as(i64, 55), toI64(try v.run(&fb.func, &.{fromI64(10)})));
|
||||
try std.testing.expectEqual(@as(i64, 0), toI64(try v.run(&fb.func, &.{fromI64(0)})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: struct_init + struct_get round-trips a flat struct" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
// Point :: struct { x: i64, y: i64 }
|
||||
const pfields = [_]types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = table.internString("x"), .ty = .i64 },
|
||||
.{ .name = table.internString("y"), .ty = .i64 },
|
||||
};
|
||||
const point = table.intern(.{ .@"struct" = .{ .name = table.internString("Point"), .fields = &pfields } });
|
||||
|
||||
// f() -> i64 { p := Point.{ x = 7, y = 9 }; return p.x + p.y }
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const x = fb.add(b0, inst(.{ .const_int = 7 }, .i64));
|
||||
const y = fb.add(b0, inst(.{ .const_int = 9 }, .i64));
|
||||
const finit = [_]Ref{ ref(x), ref(y) };
|
||||
const p = fb.add(b0, inst(.{ .struct_init = .{ .fields = &finit } }, point));
|
||||
const px = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(p), .field_index = 0, .base_type = point } }, .i64));
|
||||
const py = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(p), .field_index = 1, .base_type = point } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(px), .rhs = ref(py) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 16), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: alloca + struct_gep + store + load" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const pfields = [_]types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = table.internString("x"), .ty = .i64 },
|
||||
.{ .name = table.internString("y"), .ty = .i64 },
|
||||
};
|
||||
const point = table.intern(.{ .@"struct" = .{ .name = table.internString("Point"), .fields = &pfields } });
|
||||
const pptr = table.intern(.{ .pointer = .{ .pointee = point } });
|
||||
|
||||
// p := alloca Point; p.x = 5; p.y = 11; return load p.x + load p.y
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const p = fb.add(b0, inst(.{ .alloca = point }, pptr));
|
||||
const gx = fb.add(b0, inst(.{ .struct_gep = .{ .base = ref(p), .field_index = 0, .base_type = point } }, pptr));
|
||||
const c5 = fb.add(b0, inst(.{ .const_int = 5 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(gx), .val = ref(c5), .val_ty = .i64 } }, .void));
|
||||
const gy = fb.add(b0, inst(.{ .struct_gep = .{ .base = ref(p), .field_index = 1, .base_type = point } }, pptr));
|
||||
const c11 = fb.add(b0, inst(.{ .const_int = 11 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(gy), .val = ref(c11), .val_ty = .i64 } }, .void));
|
||||
const lx = fb.add(b0, inst(.{ .load = .{ .operand = ref(gx) } }, .i64));
|
||||
const ly = fb.add(b0, inst(.{ .load = .{ .operand = ref(gy) } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(lx), .rhs = ref(ly) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 16), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: nested struct (aggregate field copy + nested read)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const pfields = [_]types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = table.internString("x"), .ty = .i64 },
|
||||
.{ .name = table.internString("y"), .ty = .i64 },
|
||||
};
|
||||
const point = table.intern(.{ .@"struct" = .{ .name = table.internString("Point"), .fields = &pfields } });
|
||||
const lfields = [_]types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = table.internString("a"), .ty = point },
|
||||
.{ .name = table.internString("b"), .ty = point },
|
||||
};
|
||||
const line = table.intern(.{ .@"struct" = .{ .name = table.internString("Line"), .fields = &lfields } });
|
||||
|
||||
// L := Line.{ a = Point.{1,2}, b = Point.{3,4} }; return L.a.x + L.b.y → 1 + 4 = 5
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const c1 = fb.add(b0, inst(.{ .const_int = 1 }, .i64));
|
||||
const c2 = fb.add(b0, inst(.{ .const_int = 2 }, .i64));
|
||||
const pr = [_]Ref{ ref(c1), ref(c2) };
|
||||
const p = fb.add(b0, inst(.{ .struct_init = .{ .fields = &pr } }, point));
|
||||
const c3 = fb.add(b0, inst(.{ .const_int = 3 }, .i64));
|
||||
const c4 = fb.add(b0, inst(.{ .const_int = 4 }, .i64));
|
||||
const qr = [_]Ref{ ref(c3), ref(c4) };
|
||||
const q = fb.add(b0, inst(.{ .struct_init = .{ .fields = &qr } }, point));
|
||||
const lr = [_]Ref{ ref(p), ref(q) };
|
||||
const l = fb.add(b0, inst(.{ .struct_init = .{ .fields = &lr } }, line));
|
||||
const la = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(l), .field_index = 0, .base_type = line } }, point));
|
||||
const lax = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(la), .field_index = 0, .base_type = point } }, .i64));
|
||||
const lb = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(l), .field_index = 1, .base_type = line } }, point));
|
||||
const lby = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(lb), .field_index = 1, .base_type = point } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(lax), .rhs = ref(lby) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 5), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: tuple_init + tuple_get (mixed i64/f64)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const tfields = [_]TypeId{ .i64, .f64 };
|
||||
const tup = table.intern(.{ .tuple = .{ .fields = &tfields, .names = null } });
|
||||
|
||||
// t := (5, 2.5); return t.0 + int(t.1) → 5 + 2 = 7
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const a = fb.add(b0, inst(.{ .const_int = 5 }, .i64));
|
||||
const b = fb.add(b0, inst(.{ .const_float = 2.5 }, .f64));
|
||||
const tinit = [_]Ref{ ref(a), ref(b) };
|
||||
const t = fb.add(b0, inst(.{ .tuple_init = .{ .fields = &tinit } }, tup));
|
||||
const t0 = fb.add(b0, inst(.{ .tuple_get = .{ .base = ref(t), .field_index = 0, .base_type = tup } }, .i64));
|
||||
const t1 = fb.add(b0, inst(.{ .tuple_get = .{ .base = ref(t), .field_index = 1, .base_type = tup } }, .f64));
|
||||
const t1i = fb.add(b0, inst(.{ .float_to_int = .{ .operand = ref(t1), .from = .f64, .to = .i64 } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(t0), .rhs = ref(t1i) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 7), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: array index_gep/store + index_get sum, and length" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const arr = table.intern(.{ .array = .{ .element = .i64, .length = 3 } });
|
||||
const aptr = table.intern(.{ .pointer = .{ .pointee = arr } });
|
||||
const i64ptr = table.intern(.{ .pointer = .{ .pointee = .i64 } });
|
||||
|
||||
// a := alloca [3]i64; a[0]=10; a[1]=20; a[2]=12; return a[0]+a[1]+a[2] → 42
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const a = fb.add(b0, inst(.{ .alloca = arr }, aptr));
|
||||
const vals = [_]i64{ 10, 20, 12 };
|
||||
var gep: [3]u32 = undefined;
|
||||
inline for (0..3) |k| {
|
||||
const ik = fb.add(b0, inst(.{ .const_int = @intCast(k) }, .i64));
|
||||
gep[k] = fb.add(b0, inst(.{ .index_gep = .{ .lhs = ref(a), .rhs = ref(ik) } }, i64ptr));
|
||||
const cv = fb.add(b0, inst(.{ .const_int = vals[k] }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(gep[k]), .val = ref(cv), .val_ty = .i64 } }, .void));
|
||||
}
|
||||
const idx0 = fb.add(b0, inst(.{ .const_int = 0 }, .i64));
|
||||
const e0 = fb.add(b0, inst(.{ .index_get = .{ .lhs = ref(a), .rhs = ref(idx0) } }, .i64));
|
||||
const idx1 = fb.add(b0, inst(.{ .const_int = 1 }, .i64));
|
||||
const e1 = fb.add(b0, inst(.{ .index_get = .{ .lhs = ref(a), .rhs = ref(idx1) } }, .i64));
|
||||
const idx2 = fb.add(b0, inst(.{ .const_int = 2 }, .i64));
|
||||
const e2 = fb.add(b0, inst(.{ .index_get = .{ .lhs = ref(a), .rhs = ref(idx2) } }, .i64));
|
||||
const s01 = fb.add(b0, inst(.{ .add = .{ .lhs = ref(e0), .rhs = ref(e1) } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(s01), .rhs = ref(e2) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 42), toI64(try v.run(&fb.func, &.{})));
|
||||
|
||||
// length(array value) → static length 3
|
||||
var fb2 = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb2.deinit();
|
||||
const c0 = fb2.block(&.{});
|
||||
const a2 = fb2.add(c0, inst(.{ .alloca = arr }, aptr));
|
||||
const av = fb2.add(c0, inst(.{ .load = .{ .operand = ref(a2) } }, arr));
|
||||
const len = fb2.add(c0, inst(.{ .length = .{ .operand = ref(av) } }, .i64));
|
||||
_ = fb2.add(c0, inst(.{ .ret = .{ .operand = ref(len) } }, .void));
|
||||
try std.testing.expectEqual(@as(i64, 3), toI64(try v.run(&fb2.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: const_string length + str_eq/str_ne" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const foo = table.internString("foo");
|
||||
const foo2 = table.internString("foo"); // interns to the same id, but distinct const_string sites
|
||||
const bar = table.internString("bar");
|
||||
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const a = fb.add(b0, inst(.{ .const_string = foo }, .string));
|
||||
const b = fb.add(b0, inst(.{ .const_string = foo2 }, .string));
|
||||
const c = fb.add(b0, inst(.{ .const_string = bar }, .string));
|
||||
const la = fb.add(b0, inst(.{ .length = .{ .operand = ref(a) } }, .i64)); // 3
|
||||
const eq = fb.add(b0, inst(.{ .str_eq = .{ .lhs = ref(a), .rhs = ref(b) } }, .bool)); // true
|
||||
const ne = fb.add(b0, inst(.{ .str_ne = .{ .lhs = ref(a), .rhs = ref(c) } }, .bool)); // true
|
||||
const both = fb.add(b0, inst(.{ .bool_and = .{ .lhs = ref(eq), .rhs = ref(ne) } }, .bool));
|
||||
// return length(a) when both predicates hold, else 0 → 3
|
||||
const z = fb.add(b0, inst(.{ .const_int = 0 }, .i64));
|
||||
const sel = fb.add(b0, inst(.{ .mul = .{ .lhs = ref(la), .rhs = ref(both) } }, .i64)); // 3 * 1
|
||||
_ = z;
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(sel) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 3), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: array_to_slice + index through slice + slice length" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const arr = table.intern(.{ .array = .{ .element = .i64, .length = 3 } });
|
||||
const aptr = table.intern(.{ .pointer = .{ .pointee = arr } });
|
||||
const i64ptr = table.intern(.{ .pointer = .{ .pointee = .i64 } });
|
||||
const sl = table.intern(.{ .slice = .{ .element = .i64 } });
|
||||
|
||||
// a := alloca [3]i64 = {10,20,12}; s := a[..]; return len(s) + s[1] → 3 + 20 = 23
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const a = fb.add(b0, inst(.{ .alloca = arr }, aptr));
|
||||
const vals = [_]i64{ 10, 20, 12 };
|
||||
inline for (0..3) |k| {
|
||||
const ik = fb.add(b0, inst(.{ .const_int = @intCast(k) }, .i64));
|
||||
const g = fb.add(b0, inst(.{ .index_gep = .{ .lhs = ref(a), .rhs = ref(ik) } }, i64ptr));
|
||||
const cv = fb.add(b0, inst(.{ .const_int = vals[k] }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(g), .val = ref(cv), .val_ty = .i64 } }, .void));
|
||||
}
|
||||
const av = fb.add(b0, inst(.{ .load = .{ .operand = ref(a) } }, arr));
|
||||
const s = fb.add(b0, inst(.{ .array_to_slice = .{ .operand = ref(av) } }, sl));
|
||||
const slen = fb.add(b0, inst(.{ .length = .{ .operand = ref(s) } }, .i64));
|
||||
const one = fb.add(b0, inst(.{ .const_int = 1 }, .i64));
|
||||
const e1 = fb.add(b0, inst(.{ .index_get = .{ .lhs = ref(s), .rhs = ref(one) } }, .i64));
|
||||
const sum = fb.add(b0, inst(.{ .add = .{ .lhs = ref(slen), .rhs = ref(e1) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(sum) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 23), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: subslice of an array" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const arr = table.intern(.{ .array = .{ .element = .i64, .length = 5 } });
|
||||
const aptr = table.intern(.{ .pointer = .{ .pointee = arr } });
|
||||
const i64ptr = table.intern(.{ .pointer = .{ .pointee = .i64 } });
|
||||
const sl = table.intern(.{ .slice = .{ .element = .i64 } });
|
||||
|
||||
// a := {0,10,20,30,40}; s := a[1..4] = {10,20,30}; return len(s) + s[0] + s[2] → 3+10+30 = 43
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const a = fb.add(b0, inst(.{ .alloca = arr }, aptr));
|
||||
inline for (0..5) |k| {
|
||||
const ik = fb.add(b0, inst(.{ .const_int = @intCast(k) }, .i64));
|
||||
const g = fb.add(b0, inst(.{ .index_gep = .{ .lhs = ref(a), .rhs = ref(ik) } }, i64ptr));
|
||||
const cv = fb.add(b0, inst(.{ .const_int = @as(i64, @intCast(k)) * 10 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(g), .val = ref(cv), .val_ty = .i64 } }, .void));
|
||||
}
|
||||
const av = fb.add(b0, inst(.{ .load = .{ .operand = ref(a) } }, arr));
|
||||
const lo = fb.add(b0, inst(.{ .const_int = 1 }, .i64));
|
||||
const hi = fb.add(b0, inst(.{ .const_int = 4 }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .subslice = .{ .base = ref(av), .lo = ref(lo), .hi = ref(hi), .base_ty = arr } }, sl));
|
||||
const slen = fb.add(b0, inst(.{ .length = .{ .operand = ref(s) } }, .i64));
|
||||
const z = fb.add(b0, inst(.{ .const_int = 0 }, .i64));
|
||||
const e0 = fb.add(b0, inst(.{ .index_get = .{ .lhs = ref(s), .rhs = ref(z) } }, .i64));
|
||||
const two = fb.add(b0, inst(.{ .const_int = 2 }, .i64));
|
||||
const e2 = fb.add(b0, inst(.{ .index_get = .{ .lhs = ref(s), .rhs = ref(two) } }, .i64));
|
||||
const t = fb.add(b0, inst(.{ .add = .{ .lhs = ref(slen), .rhs = ref(e0) } }, .i64));
|
||||
const sum = fb.add(b0, inst(.{ .add = .{ .lhs = ref(t), .rhs = ref(e2) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(sum) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 43), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: non-pointer optional wrap/unwrap/has_value/coalesce" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const opt_i64 = table.intern(.{ .optional = .{ .child = .i64 } });
|
||||
|
||||
// o := ?i64(42); n := null; return (unwrap o + (n ?? 7) + (o ?? 7)) * has_value(o)
|
||||
// = (42 + 7 + 42) * 1 = 91
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const c = fb.add(b0, inst(.{ .const_int = 42 }, .i64));
|
||||
const o = fb.add(b0, inst(.{ .optional_wrap = .{ .operand = ref(c) } }, opt_i64));
|
||||
const n = fb.add(b0, inst(.const_null, opt_i64));
|
||||
const h = fb.add(b0, inst(.{ .optional_has_value = .{ .operand = ref(o) } }, .bool));
|
||||
const u = fb.add(b0, inst(.{ .optional_unwrap = .{ .operand = ref(o) } }, .i64));
|
||||
const fb7 = fb.add(b0, inst(.{ .const_int = 7 }, .i64));
|
||||
const co_n = fb.add(b0, inst(.{ .optional_coalesce = .{ .lhs = ref(n), .rhs = ref(fb7) } }, .i64));
|
||||
const co_o = fb.add(b0, inst(.{ .optional_coalesce = .{ .lhs = ref(o), .rhs = ref(fb7) } }, .i64));
|
||||
const s1 = fb.add(b0, inst(.{ .add = .{ .lhs = ref(u), .rhs = ref(co_n) } }, .i64));
|
||||
const s2 = fb.add(b0, inst(.{ .add = .{ .lhs = ref(s1), .rhs = ref(co_o) } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .mul = .{ .lhs = ref(s2), .rhs = ref(h) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 91), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: pointer optional (null == 0)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const i64ptr = table.intern(.{ .pointer = .{ .pointee = .i64 } });
|
||||
const opt_ptr = table.intern(.{ .optional = .{ .child = i64ptr } });
|
||||
|
||||
// p := alloca i64; *p = 99; op := ?*i64(p); n := null;
|
||||
// return load(unwrap op) * has_value(op) + has_value(n) → 99 * 1 + 0 = 99
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const p = fb.add(b0, inst(.{ .alloca = .i64 }, i64ptr));
|
||||
const c = fb.add(b0, inst(.{ .const_int = 99 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(p), .val = ref(c), .val_ty = .i64 } }, .void));
|
||||
const op = fb.add(b0, inst(.{ .optional_wrap = .{ .operand = ref(p) } }, opt_ptr));
|
||||
const h = fb.add(b0, inst(.{ .optional_has_value = .{ .operand = ref(op) } }, .bool));
|
||||
const up = fb.add(b0, inst(.{ .optional_unwrap = .{ .operand = ref(op) } }, i64ptr));
|
||||
const val = fb.add(b0, inst(.{ .load = .{ .operand = ref(up) } }, .i64));
|
||||
const n = fb.add(b0, inst(.const_null, opt_ptr));
|
||||
const hn = fb.add(b0, inst(.{ .optional_has_value = .{ .operand = ref(n) } }, .bool));
|
||||
const prod = fb.add(b0, inst(.{ .mul = .{ .lhs = ref(val), .rhs = ref(h) } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(prod), .rhs = ref(hn) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 99), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: payloadless enum_init + enum_tag" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const variants = [_]types.StringId{ table.internString("red"), table.internString("green"), table.internString("blue") };
|
||||
const color = table.intern(.{ .@"enum" = .{ .name = table.internString("Color"), .variants = &variants } });
|
||||
|
||||
// g := Color.green (tag 1); return enum_tag(g) + 10 → 11
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const g = fb.add(b0, inst(.{ .enum_init = .{ .tag = 1, .payload = Ref.none } }, color));
|
||||
const t = fb.add(b0, inst(.{ .enum_tag = .{ .operand = ref(g) } }, .i64));
|
||||
const ten = fb.add(b0, inst(.{ .const_int = 10 }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(t), .rhs = ref(ten) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 11), toI64(try v.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: deref a pointer; addr_of passes through a struct address" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const i64ptr = table.intern(.{ .pointer = .{ .pointee = .i64 } });
|
||||
const pfields = [_]types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = table.internString("x"), .ty = .i64 },
|
||||
.{ .name = table.internString("y"), .ty = .i64 },
|
||||
};
|
||||
const point = table.intern(.{ .@"struct" = .{ .name = table.internString("Point"), .fields = &pfields } });
|
||||
|
||||
// p := alloca i64; *p = 77; v := p.*; (deref)
|
||||
// pt := Point.{3,4}; pa := @pt; px := pa.x (addr_of pass-through + field read)
|
||||
// return v + px → 77 + 3 = 80
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const p = fb.add(b0, inst(.{ .alloca = .i64 }, i64ptr));
|
||||
const c = fb.add(b0, inst(.{ .const_int = 77 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .store = .{ .ptr = ref(p), .val = ref(c), .val_ty = .i64 } }, .void));
|
||||
const v = fb.add(b0, inst(.{ .deref = .{ .operand = ref(p) } }, .i64));
|
||||
const x = fb.add(b0, inst(.{ .const_int = 3 }, .i64));
|
||||
const y = fb.add(b0, inst(.{ .const_int = 4 }, .i64));
|
||||
const finit = [_]Ref{ ref(x), ref(y) };
|
||||
const pt = fb.add(b0, inst(.{ .struct_init = .{ .fields = &finit } }, point));
|
||||
const pa = fb.add(b0, inst(.{ .addr_of = .{ .operand = ref(pt) } }, point));
|
||||
const px = fb.add(b0, inst(.{ .struct_get = .{ .base = ref(pa), .field_index = 0, .base_type = point } }, .i64));
|
||||
const s = fb.add(b0, inst(.{ .add = .{ .lhs = ref(v), .rhs = ref(px) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
|
||||
var vm_ = vm.Vm.init(alloc);
|
||||
vm_.table = &table;
|
||||
defer vm_.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 80), toI64(try vm_.run(&fb.func, &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: direct call to another function" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = Module.init(alloc);
|
||||
defer module.deinit();
|
||||
|
||||
// fn add(a, b) = a + b (FuncId 0)
|
||||
const add_params = [_]Function.Param{ .{ .name = dummy, .ty = .i64 }, .{ .name = dummy, .ty = .i64 } };
|
||||
var cb = Fb.init(alloc, &add_params, .i64);
|
||||
const cbb = cb.block(&.{});
|
||||
const csum = cb.add(cbb, inst(.{ .add = .{ .lhs = ref(0), .rhs = ref(1) } }, .i64));
|
||||
_ = cb.add(cbb, inst(.{ .ret = .{ .operand = ref(csum) } }, .void));
|
||||
const add_id = module.addFunction(cb.func); // module now owns it (no cb.deinit)
|
||||
|
||||
// fn main() = add(20, 22) + 100 (FuncId 1)
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
const b0 = fb.block(&.{});
|
||||
const a20 = fb.add(b0, inst(.{ .const_int = 20 }, .i64));
|
||||
const a22 = fb.add(b0, inst(.{ .const_int = 22 }, .i64));
|
||||
const cargs = [_]Ref{ ref(a20), ref(a22) };
|
||||
const r = fb.add(b0, inst(.{ .call = .{ .callee = add_id, .args = &cargs } }, .i64));
|
||||
const c100 = fb.add(b0, inst(.{ .const_int = 100 }, .i64));
|
||||
const sum = fb.add(b0, inst(.{ .add = .{ .lhs = ref(r), .rhs = ref(c100) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(sum) } }, .void));
|
||||
const main_id = module.addFunction(fb.func);
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &module.types;
|
||||
v.module = &module;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 142), toI64(try v.run(module.getFunction(main_id), &.{})));
|
||||
}
|
||||
|
||||
test "comptime_vm exec: recursive call (sum 0..n)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = Module.init(alloc);
|
||||
defer module.deinit();
|
||||
|
||||
// fn sum(n) = if n == 0 then 0 else n + sum(n-1) (FuncId 0 — references itself)
|
||||
const self_id = FuncId.fromIndex(0);
|
||||
const params = [_]Function.Param{.{ .name = dummy, .ty = .i64 }};
|
||||
var fb = Fb.init(alloc, ¶ms, .i64);
|
||||
const b0 = fb.block(&.{});
|
||||
const z = fb.add(b0, inst(.{ .const_int = 0 }, .i64));
|
||||
const c = fb.add(b0, inst(.{ .cmp_eq = .{ .lhs = ref(0), .rhs = ref(z) } }, .bool));
|
||||
_ = fb.add(b0, inst(.{ .cond_br = .{ .cond = ref(c), .then_target = BlockId.fromIndex(1), .then_args = &.{}, .else_target = BlockId.fromIndex(2), .else_args = &.{} } }, .void));
|
||||
// b1: base case → 0
|
||||
const b1 = fb.block(&.{});
|
||||
const zero = fb.add(b1, inst(.{ .const_int = 0 }, .i64));
|
||||
_ = fb.add(b1, inst(.{ .ret = .{ .operand = ref(zero) } }, .void));
|
||||
// b2: recurse → n + sum(n-1)
|
||||
const b2 = fb.block(&.{});
|
||||
const one = fb.add(b2, inst(.{ .const_int = 1 }, .i64));
|
||||
const nm1 = fb.add(b2, inst(.{ .sub = .{ .lhs = ref(0), .rhs = ref(one) } }, .i64));
|
||||
const rargs = [_]Ref{ref(nm1)};
|
||||
const rec = fb.add(b2, inst(.{ .call = .{ .callee = self_id, .args = &rargs } }, .i64));
|
||||
const s = fb.add(b2, inst(.{ .add = .{ .lhs = ref(0), .rhs = ref(rec) } }, .i64));
|
||||
_ = fb.add(b2, inst(.{ .ret = .{ .operand = ref(s) } }, .void));
|
||||
const sum_id = module.addFunction(fb.func);
|
||||
try std.testing.expectEqual(@as(u32, 0), sum_id.index()); // confirms the self-reference id
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &module.types;
|
||||
v.module = &module;
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 15), toI64(try v.run(module.getFunction(sum_id), &.{fromI64(5)})));
|
||||
try std.testing.expectEqual(@as(i64, 55), toI64(try v.run(module.getFunction(sum_id), &.{fromI64(10)})));
|
||||
}
|
||||
|
||||
test "comptime_vm bridge: Value <-> Reg round-trips (scalar, string, struct)" {
|
||||
const alloc = std.testing.allocator;
|
||||
var table = types.TypeTable.init(alloc);
|
||||
defer table.deinit();
|
||||
const pfields = [_]types.TypeInfo.StructInfo.Field{
|
||||
.{ .name = table.internString("x"), .ty = .i64 },
|
||||
.{ .name = table.internString("y"), .ty = .i64 },
|
||||
};
|
||||
const point = table.intern(.{ .@"struct" = .{ .name = table.internString("Point"), .fields = &pfields } });
|
||||
|
||||
var v = vm.Vm.init(alloc);
|
||||
v.table = &table;
|
||||
defer v.deinit();
|
||||
|
||||
// scalar i64
|
||||
const r_i = try v.valueToReg(&table, .{ .int = 42 }, .i64);
|
||||
try std.testing.expectEqual(@as(i64, 42), toI64(r_i));
|
||||
const back_i = try v.regToValue(alloc, &table, r_i, .i64);
|
||||
try std.testing.expectEqual(@as(i64, 42), back_i.int);
|
||||
|
||||
// string (materialized into flat memory, read back + deep-copied out)
|
||||
const r_s = try v.valueToReg(&table, .{ .string = "hi" }, .string);
|
||||
const back_s = try v.regToValue(alloc, &table, r_s, .string);
|
||||
defer alloc.free(back_s.string);
|
||||
try std.testing.expectEqualStrings("hi", back_s.string);
|
||||
|
||||
// struct {x:i64, y:i64}
|
||||
const fvals = [_]Value{ .{ .int = 3 }, .{ .int = 4 } };
|
||||
const r_p = try v.valueToReg(&table, .{ .aggregate = &fvals }, point);
|
||||
const back_p = try v.regToValue(alloc, &table, r_p, point);
|
||||
defer alloc.free(back_p.aggregate);
|
||||
try std.testing.expectEqual(@as(i64, 3), back_p.aggregate[0].int);
|
||||
try std.testing.expectEqual(@as(i64, 4), back_p.aggregate[1].int);
|
||||
}
|
||||
|
||||
test "comptime_vm tryEval: pure function → Value; unsupported → null" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = Module.init(alloc);
|
||||
defer module.deinit();
|
||||
|
||||
// fn k() -> i64 { return 6 * 7 } → tryEval yields Value.int(42)
|
||||
var fb = Fb.init(alloc, &.{}, .i64);
|
||||
const b0 = fb.block(&.{});
|
||||
const a = fb.add(b0, inst(.{ .const_int = 6 }, .i64));
|
||||
const b = fb.add(b0, inst(.{ .const_int = 7 }, .i64));
|
||||
const m = fb.add(b0, inst(.{ .mul = .{ .lhs = ref(a), .rhs = ref(b) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(m) } }, .void));
|
||||
const ok_id = module.addFunction(fb.func);
|
||||
|
||||
const v = vm.tryEval(alloc, &module, ok_id) orelse return error.VmShouldHaveHandledIt;
|
||||
try std.testing.expectEqual(@as(i64, 42), v.int);
|
||||
|
||||
// fn bad() { unbox_any(1) } → tryEval yields null (caller falls back to legacy)
|
||||
var fb2 = Fb.init(alloc, &.{}, .void);
|
||||
const c0 = fb2.block(&.{});
|
||||
const c = fb2.add(c0, inst(.{ .const_int = 1 }, .i64));
|
||||
_ = fb2.add(c0, inst(.{ .unbox_any = .{ .operand = ref(c) } }, .i64));
|
||||
_ = fb2.add(c0, inst(.ret_void, .void));
|
||||
const bad_id = module.addFunction(fb2.func);
|
||||
|
||||
try std.testing.expect(vm.tryEval(alloc, &module, bad_id) == null);
|
||||
}
|
||||
|
||||
test "comptime_vm exec: division by zero and unsupported op bail loudly" {
|
||||
// a / b
|
||||
{
|
||||
const params = [_]Function.Param{ param(.i64), param(.i64) };
|
||||
var fb = Fb.init(std.testing.allocator, ¶ms, .i64);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const q = fb.add(b0, inst(.{ .div = .{ .lhs = ref(0), .rhs = ref(1) } }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .ret = .{ .operand = ref(q) } }, .void));
|
||||
|
||||
var v = vm.Vm.init(std.testing.allocator);
|
||||
defer v.deinit();
|
||||
try std.testing.expectEqual(@as(i64, 4), toI64(try v.run(&fb.func, &.{ fromI64(12), fromI64(3) })));
|
||||
try std.testing.expectError(error.DivisionByZero, v.run(&fb.func, &.{ fromI64(12), fromI64(0) }));
|
||||
}
|
||||
// A not-yet-ported op (unbox_any) → Unsupported with the op name in `detail`.
|
||||
{
|
||||
var fb = Fb.init(std.testing.allocator, &.{}, .void);
|
||||
defer fb.deinit();
|
||||
const b0 = fb.block(&.{});
|
||||
const c = fb.add(b0, inst(.{ .const_int = 1 }, .i64));
|
||||
_ = fb.add(b0, inst(.{ .unbox_any = .{ .operand = ref(c) } }, .i64));
|
||||
_ = fb.add(b0, inst(.ret_void, .void));
|
||||
|
||||
var v = vm.Vm.init(std.testing.allocator);
|
||||
defer v.deinit();
|
||||
try std.testing.expectError(error.Unsupported, v.run(&fb.func, &.{}));
|
||||
try std.testing.expectEqualStrings("unbox_any", v.detail.?);
|
||||
}
|
||||
}
|
||||
|
||||
test "comptime_vm: allocBytes never returns null_addr and respects alignment" {
|
||||
var m = vm.Machine.init(std.testing.allocator);
|
||||
defer m.deinit();
|
||||
|
||||
const a = m.allocBytes(1, 1);
|
||||
try std.testing.expect(a != vm.null_addr);
|
||||
|
||||
// An 8-aligned allocation lands on an 8-multiple address.
|
||||
const b = m.allocBytes(4, 8);
|
||||
try std.testing.expectEqual(@as(u64, 0), b % 8);
|
||||
|
||||
// Distinct allocations don't overlap.
|
||||
const c = m.allocBytes(4, 8);
|
||||
try std.testing.expect(c >= b + 4);
|
||||
|
||||
// A zero-size allocation is still a valid, non-null, aligned address.
|
||||
const z = m.allocBytes(0, 4);
|
||||
try std.testing.expect(z != vm.null_addr);
|
||||
try std.testing.expectEqual(@as(u64, 0), z % 4);
|
||||
}
|
||||
|
||||
test "comptime_vm: writeWord/readWord round-trip at each scalar size" {
|
||||
var m = vm.Machine.init(std.testing.allocator);
|
||||
defer m.deinit();
|
||||
|
||||
const sizes = [_]usize{ 1, 2, 4, 8 };
|
||||
const vals = [_]u64{ 0xAB, 0xBEEF, 0xDEADBEEF, 0x0123456789ABCDEF };
|
||||
for (sizes, vals) |size, val| {
|
||||
const addr = m.allocBytes(size, size);
|
||||
m.writeWord(addr, size, val);
|
||||
try std.testing.expectEqual(val, m.readWord(addr, size));
|
||||
}
|
||||
}
|
||||
|
||||
test "comptime_vm: writeWord truncates to size and readWord zero-extends" {
|
||||
var m = vm.Machine.init(std.testing.allocator);
|
||||
defer m.deinit();
|
||||
|
||||
// Write a full 64-bit word's worth of bits through a 1-byte store: only the
|
||||
// low byte lands; the read zero-extends it.
|
||||
const addr = m.allocBytes(1, 1);
|
||||
m.writeWord(addr, 1, 0xFFFF_FF42);
|
||||
try std.testing.expectEqual(@as(u64, 0x42), m.readWord(addr, 1));
|
||||
}
|
||||
|
||||
test "comptime_vm: bytes() view reflects word writes (little-endian)" {
|
||||
var m = vm.Machine.init(std.testing.allocator);
|
||||
defer m.deinit();
|
||||
|
||||
const addr = m.allocBytes(4, 4);
|
||||
m.writeWord(addr, 4, 0xDEADBEEF);
|
||||
const view = m.bytes(addr, 4);
|
||||
try std.testing.expectEqual(@as(u8, 0xEF), view[0]);
|
||||
try std.testing.expectEqual(@as(u8, 0xBE), view[1]);
|
||||
try std.testing.expectEqual(@as(u8, 0xAD), view[2]);
|
||||
try std.testing.expectEqual(@as(u8, 0xDE), view[3]);
|
||||
}
|
||||
|
||||
test "comptime_vm: mark/reset reclaims the stack region" {
|
||||
var m = vm.Machine.init(std.testing.allocator);
|
||||
defer m.deinit();
|
||||
|
||||
_ = m.allocBytes(16, 8);
|
||||
const top = m.mark();
|
||||
const reclaimed = m.allocBytes(64, 8);
|
||||
try std.testing.expect(m.mark() > top);
|
||||
m.reset(top);
|
||||
try std.testing.expectEqual(top, m.mark());
|
||||
|
||||
// After reset the freed region is handed back out again (same address).
|
||||
const reused = m.allocBytes(64, 8);
|
||||
try std.testing.expectEqual(reclaimed, reused);
|
||||
}
|
||||
|
||||
test "comptime_vm: Frame register file round-trips (no stack reclaim)" {
|
||||
var frame = vm.Frame.init(std.testing.allocator, 4);
|
||||
defer frame.deinit();
|
||||
|
||||
// Registers default to zero, then round-trip.
|
||||
try std.testing.expectEqual(@as(vm.Reg, 0), frame.get(2));
|
||||
frame.set(2, 0x1234);
|
||||
try std.testing.expectEqual(@as(vm.Reg, 0x1234), frame.get(2));
|
||||
}
|
||||
Reference in New Issue
Block a user