// Tests for the IR interpreter (interp.zig). // Includes basic interpreter tests and comptime parity tests. const std = @import("std"); const types = @import("types.zig"); const inst_mod = @import("inst.zig"); const mod_mod = @import("module.zig"); const interp_mod = @import("interp.zig"); const TypeId = types.TypeId; const Ref = inst_mod.Ref; const BlockId = inst_mod.BlockId; const FuncId = inst_mod.FuncId; const Function = inst_mod.Function; const Module = mod_mod.Module; const Builder = mod_mod.Builder; const Interpreter = interp_mod.Interpreter; const Value = interp_mod.Value; // ── Helper ────────────────────────────────────────────────────────────── fn str(module: *Module, s: []const u8) types.StringId { return module.types.internString(s); } // ── Basic interpreter tests (migrated from interp.zig) ────────────────── test "interpret: compute(5) = 25" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); // func compute(x: s64) -> s64 { return x * x; } const params = &[_]Function.Param{.{ .name = str(&module, "compute"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "compute"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const x_ref = Ref.fromIndex(0); const result = b.mul(x_ref, x_ref, .s64); b.ret(result, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 5 }}); try std.testing.expectEqual(@as(i64, 25), val.asInt().?); } test "interpret: if/else branching" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); const params = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "abs"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); const then_bb = b.appendBlock(str(&module, "then"), &.{}); const else_bb = b.appendBlock(str(&module, "else"), &.{}); b.switchToBlock(entry); const x = Ref.fromIndex(0); const zero = b.constInt(0, .s64); const is_neg = b.cmpLt(x, zero); b.condBr(is_neg, then_bb, &.{}, else_bb, &.{}); b.switchToBlock(then_bb); const neg_x = b.emit(.{ .neg = .{ .operand = x } }, .s64); b.ret(neg_x, .s64); b.switchToBlock(else_bb); b.ret(x, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val1 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = -7 }}); try std.testing.expectEqual(@as(i64, 7), val1.asInt().?); const val2 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 3 }}); try std.testing.expectEqual(@as(i64, 3), val2.asInt().?); } test "interpret: function calling another function" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); // func square(x: s64) -> s64 { return x * x; } const params_sq = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "square"), params_sq, .s64); const entry1 = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry1); const x = Ref.fromIndex(0); const sq = b.mul(x, x, .s64); b.ret(sq, .s64); b.finalize(); // func sum_of_squares(a, b) -> s64 { return square(a) + square(b); } const params_ss = &[_]Function.Param{ .{ .name = str(&module, "a"), .ty = .s64 }, .{ .name = str(&module, "b"), .ty = .s64 }, }; _ = b.beginFunction(str(&module, "sum_of_squares"), params_ss, .s64); const entry2 = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry2); const a = Ref.fromIndex(0); const b_param = Ref.fromIndex(1); const sq_a = b.call(FuncId.fromIndex(0), &.{a}, .s64); const sq_b = b.call(FuncId.fromIndex(0), &.{b_param}, .s64); const sum = b.add(sq_a, sq_b, .s64); b.ret(sum, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(1), &.{ .{ .int = 3 }, .{ .int = 4 } }); try std.testing.expectEqual(@as(i64, 25), val.asInt().?); } test "interpret: alloca/store/load" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "test"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const slot = b.alloca(.s64); const ten = b.constInt(10, .s64); b.store(slot, ten); const loaded = b.load(slot, .s64); const five = b.constInt(5, .s64); const sum = b.add(loaded, five, .s64); b.store(slot, sum); const result = b.load(slot, .s64); b.ret(result, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 15), val.asInt().?); } // ── Comptime parity tests ─────────────────────────────────────────────── // ── Test: while loop (sumOf10 from 15-while.sx) ───────────────────────── // sumOf10 :: () -> s32 { i:=1; s:=0; while i<=10 { s+=i; i+=1; } s; } // Expected: 55 test "comptime: while loop — sumOf10 = 55" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "sumOf10"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); const hdr = b.appendBlock(str(&module, "while.hdr"), &.{}); const body = b.appendBlock(str(&module, "while.body"), &.{}); const exit = b.appendBlock(str(&module, "while.exit"), &.{}); // entry: i=1, s=0, br while.hdr b.switchToBlock(entry); const i_slot = b.alloca(.s64); const one = b.constInt(1, .s64); b.store(i_slot, one); const s_slot = b.alloca(.s64); const zero = b.constInt(0, .s64); b.store(s_slot, zero); b.br(hdr, &.{}); // while.hdr: if i <= 10 → body, else → exit b.switchToBlock(hdr); const i_load = b.load(i_slot, .s64); const ten = b.constInt(10, .s64); const cond = b.emit(.{ .cmp_le = .{ .lhs = i_load, .rhs = ten } }, .bool); b.condBr(cond, body, &.{}, exit, &.{}); // while.body: s += i; i += 1; br while.hdr b.switchToBlock(body); const s_load = b.load(s_slot, .s64); const i_load2 = b.load(i_slot, .s64); const s_new = b.add(s_load, i_load2, .s64); b.store(s_slot, s_new); const i_load3 = b.load(i_slot, .s64); const one2 = b.constInt(1, .s64); const i_new = b.add(i_load3, one2, .s64); b.store(i_slot, i_new); b.br(hdr, &.{}); // while.exit: return s b.switchToBlock(exit); const s_final = b.load(s_slot, .s64); b.ret(s_final, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 55), val.asInt().?); } // ── Test: optional coalesce (ct_sum from 32-optionals.sx) ──────────────── // ct_sum :: () -> s32 { x:?s32=42; y:?s32=null; return (x??0)+(y??99); } // Expected: 42 + 99 = 141 test "comptime: optional coalesce — ct_sum = 141" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "ct_sum"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); // x: ?s32 = 42 → alloca, store 42 const x_slot = b.alloca(.s64); const forty_two = b.constInt(42, .s64); b.store(x_slot, forty_two); // y: ?s32 = null → alloca, store null const y_slot = b.alloca(.s64); const null_val = b.constNull(.s64); b.store(y_slot, null_val); // (x ?? 0) const x_load = b.load(x_slot, .s64); const zero = b.constInt(0, .s64); const x_coalesced = b.emit(.{ .optional_coalesce = .{ .lhs = x_load, .rhs = zero } }, .s64); // (y ?? 99) const y_load = b.load(y_slot, .s64); const ninety_nine = b.constInt(99, .s64); const y_coalesced = b.emit(.{ .optional_coalesce = .{ .lhs = y_load, .rhs = ninety_nine } }, .s64); // return x_coalesced + y_coalesced const sum = b.add(x_coalesced, y_coalesced, .s64); b.ret(sum, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 141), val.asInt().?); } // ── Test: optional unwrap (ct_opt_unwrap from 50-smoke.sx) ─────────────── // ct_opt_unwrap :: () -> s32 { x:?s32 = 77; return x!; } // Expected: 77 test "comptime: optional unwrap — 77" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "ct_opt_unwrap"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const slot = b.alloca(.s64); const val77 = b.constInt(77, .s64); b.store(slot, val77); const loaded = b.load(slot, .s64); const unwrapped = b.optionalUnwrap(loaded, .s64); b.ret(unwrapped, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 77), val.asInt().?); } // ── Test: recursive fibonacci ──────────────────────────────────────────── // fib :: (n: s64) -> s64 { if n <= 1 return n; return fib(n-1) + fib(n-2); } // Expected: fib(10) = 55 test "comptime: recursive fibonacci — fib(10) = 55" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); const params = &[_]Function.Param{.{ .name = str(&module, "n"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "fib"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); const base_bb = b.appendBlock(str(&module, "base"), &.{}); const rec_bb = b.appendBlock(str(&module, "recurse"), &.{}); // entry: if n <= 1 → base, else → recurse b.switchToBlock(entry); const n = Ref.fromIndex(0); const one = b.constInt(1, .s64); const is_base = b.emit(.{ .cmp_le = .{ .lhs = n, .rhs = one } }, .bool); b.condBr(is_base, base_bb, &.{}, rec_bb, &.{}); // base: return n b.switchToBlock(base_bb); b.ret(n, .s64); // recurse: return fib(n-1) + fib(n-2) b.switchToBlock(rec_bb); const n_minus_1 = b.sub(n, one, .s64); const two = b.constInt(2, .s64); const n_minus_2 = b.sub(n, two, .s64); const fib1 = b.call(FuncId.fromIndex(0), &.{n_minus_1}, .s64); const fib2 = b.call(FuncId.fromIndex(0), &.{n_minus_2}, .s64); const sum = b.add(fib1, fib2, .s64); b.ret(sum, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 10 }}); try std.testing.expectEqual(@as(i64, 55), val.asInt().?); } // ── Test: compute(5) = 7 (from 05-run.sx) ────────────────────────────── // compute :: (v: s32) -> s32 => v + 2; // Expected: compute(5) = 7 test "comptime: compute(5) = 7" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); const params = &[_]Function.Param{.{ .name = str(&module, "v"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "compute"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const v = Ref.fromIndex(0); const two = b.constInt(2, .s64); const result = b.add(v, two, .s64); b.ret(result, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 5 }}); try std.testing.expectEqual(@as(i64, 7), val.asInt().?); } // ── Test: chained comptime (CT_CHAIN from 50-smoke.sx) ─────────────────── // add :: (a: s32, b: s32) -> s32 => a + b; // CT_VAL :: #run add(10, 15); → 25 // CT_CHAIN :: #run add(CT_VAL, 5); → 30 // Simulates calling add(25, 5) to verify chaining works. test "comptime: chained — add(add(10,15), 5) = 30" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); // func add(a, b) -> s64 { return a + b; } const params = &[_]Function.Param{ .{ .name = str(&module, "a"), .ty = .s64 }, .{ .name = str(&module, "b"), .ty = .s64 }, }; _ = b.beginFunction(str(&module, "add"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const a = Ref.fromIndex(0); const b_ref = Ref.fromIndex(1); const sum = b.add(a, b_ref, .s64); b.ret(sum, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); // First: add(10, 15) = 25 const ct_val = try interp.call(FuncId.fromIndex(0), &.{ .{ .int = 10 }, .{ .int = 15 } }); try std.testing.expectEqual(@as(i64, 25), ct_val.asInt().?); // Then: add(25, 5) = 30 (chained) const ct_chain = try interp.call(FuncId.fromIndex(0), &.{ ct_val, .{ .int = 5 } }); try std.testing.expectEqual(@as(i64, 30), ct_chain.asInt().?); } // ── Test: struct init + field access ───────────────────────────────────── // p := Point{x: 3, y: 4}; return p.x + p.y; // Expected: 7 test "comptime: struct init and field access — 7" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "test_struct"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); // Point{x: 3, y: 4} const three = b.constInt(3, .s64); const four = b.constInt(4, .s64); const point = b.structInit(&.{ three, four }, .s64); // p.x + p.y const px = b.structGet(point, 0, .s64); const py = b.structGet(point, 1, .s64); const sum = b.add(px, py, .s64); b.ret(sum, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 7), val.asInt().?); } // ── Test: float arithmetic ────────────────────────────────────────────── // compute :: (x: f64) -> f64 { return x * 2.5 + 1.0; } // Expected: compute(3.0) = 8.5 test "comptime: float arithmetic — 8.5" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); const params = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .f64 }}; _ = b.beginFunction(str(&module, "compute_f"), params, .f64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const x = Ref.fromIndex(0); const two_five = b.constFloat(2.5, .f64); const product = b.mul(x, two_five, .f64); const one = b.constFloat(1.0, .f64); const result = b.add(product, one, .f64); b.ret(result, .f64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{.{ .float = 3.0 }}); try std.testing.expectEqual(@as(f64, 8.5), val.asFloat().?); } // ── Test: boolean logic ───────────────────────────────────────────────── // test :: (a: bool, b: bool) -> bool { return (a and b) or (not a); } // Expected: test(true, false) = true (because not a = false, a and b = false, false or false... wait) // Actually: a=true, b=false → (true and false) or (not true) = false or false = false // test(false, true) → (false and true) or (not false) = false or true = true test "comptime: boolean logic" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); const params = &[_]Function.Param{ .{ .name = str(&module, "a"), .ty = .bool }, .{ .name = str(&module, "b"), .ty = .bool }, }; _ = b.beginFunction(str(&module, "bool_test"), params, .bool); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const a_ref = Ref.fromIndex(0); const b_ref = Ref.fromIndex(1); const and_ab = b.emit(.{ .bool_and = .{ .lhs = a_ref, .rhs = b_ref } }, .bool); const not_a = b.emit(.{ .bool_not = .{ .operand = a_ref } }, .bool); const result = b.emit(.{ .bool_or = .{ .lhs = and_ab, .rhs = not_a } }, .bool); b.ret(result, .bool); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); // test(true, false) = false or false = false const val1 = try interp.call(FuncId.fromIndex(0), &.{ .{ .boolean = true }, .{ .boolean = false } }); try std.testing.expectEqual(false, val1.asBool().?); // test(false, true) = false or true = true const val2 = try interp.call(FuncId.fromIndex(0), &.{ .{ .boolean = false }, .{ .boolean = true } }); try std.testing.expectEqual(true, val2.asBool().?); } // ── Test: negation ────────────────────────────────────────────────────── test "comptime: negation — int and float" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); // func neg_int(x: s64) -> s64 { return -x; } const params = &[_]Function.Param{.{ .name = str(&module, "x"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "neg_int"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const x = Ref.fromIndex(0); const neg = b.emit(.{ .neg = .{ .operand = x } }, .s64); b.ret(neg, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 42 }}); try std.testing.expectEqual(@as(i64, -42), val.asInt().?); } // ── Test: modulo ──────────────────────────────────────────────────────── test "comptime: modulo — 17 mod 5 = 2" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "test_mod"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const seventeen = b.constInt(17, .s64); const five = b.constInt(5, .s64); const result = b.emit(.{ .mod = .{ .lhs = seventeen, .rhs = five } }, .s64); b.ret(result, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 2), val.asInt().?); } // ── Test: switch_br (enum tag dispatch) ────────────────────────────────── // Simulates: match tag { 0 => 10, 1 => 20, else => 30 } test "comptime: switch_br dispatch" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); const params = &[_]Function.Param{.{ .name = str(&module, "tag"), .ty = .s64 }}; _ = b.beginFunction(str(&module, "dispatch"), params, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); const case0 = b.appendBlock(str(&module, "case0"), &.{}); const case1 = b.appendBlock(str(&module, "case1"), &.{}); const default = b.appendBlock(str(&module, "default"), &.{}); b.switchToBlock(entry); const tag = Ref.fromIndex(0); b.switchBr(tag, &.{ .{ .value = 0, .target = case0, .args = &.{} }, .{ .value = 1, .target = case1, .args = &.{} }, }, default, &.{}); b.switchToBlock(case0); const ten = b.constInt(10, .s64); b.ret(ten, .s64); b.switchToBlock(case1); const twenty = b.constInt(20, .s64); b.ret(twenty, .s64); b.switchToBlock(default); const thirty = b.constInt(30, .s64); b.ret(thirty, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const v0 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 0 }}); try std.testing.expectEqual(@as(i64, 10), v0.asInt().?); const v1 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 1 }}); try std.testing.expectEqual(@as(i64, 20), v1.asInt().?); const v2 = try interp.call(FuncId.fromIndex(0), &.{.{ .int = 99 }}); try std.testing.expectEqual(@as(i64, 30), v2.asInt().?); } // ── Test: enum init + tag extraction ──────────────────────────────────── test "comptime: enum init and tag" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "test_enum"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); // Create enum with tag=2, no payload const e = b.enumInit(2, Ref.none, .s64); const tag = b.emit(.{ .enum_tag = .{ .operand = e } }, .s64); b.ret(tag, .s64); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const val = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 2), val.asInt().?); } // ── Test: conversion (widen/narrow passthrough) ───────────────────────── test "comptime: widen/narrow passthrough" { const alloc = std.testing.allocator; var module = Module.init(alloc); defer module.deinit(); var b = Builder.init(&module); _ = b.beginFunction(str(&module, "test_conv"), &.{}, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); const val = b.constInt(42, .s32); const widened = b.emit(.{ .widen = .{ .operand = val, .from = .s32, .to = .s64 } }, .s64); const narrowed = b.emit(.{ .narrow = .{ .operand = widened, .from = .s64, .to = .s32 } }, .s32); b.ret(narrowed, .s32); b.finalize(); var interp = Interpreter.init(&module, alloc); defer interp.deinit(); const result = try interp.call(FuncId.fromIndex(0), &.{}); try std.testing.expectEqual(@as(i64, 42), result.asInt().?); }