// 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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.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().?);
}

// ── Test: const_type produces a Value.type_tag ──────────────────────────

test "comptime: const_type yields type_tag" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = Module.init(alloc);
    defer module.deinit();
    var b = Builder.init(&module);

    // Build a fn that returns `s64` as a Type-typed Any value (matches
    // the .any IR type assigned by `constType`). The interp returns the
    // raw Value; we assert on the variant.
    _ = b.beginFunction(str(&module, "test_type_tag"), &.{}, .any);
    const entry = b.appendBlock(str(&module, "entry"), &.{});
    b.switchToBlock(entry);
    const t = b.constType(.s64);
    b.ret(t, .any);
    b.finalize();

    var interp = Interpreter.init(&module, alloc);
    defer interp.deinit();
    const result = try interp.call(FuncId.fromIndex(0), &.{});

    // The Value MUST be a .type_tag, not an .int — proves the variant
    // is honestly distinguished. asTypeId returns the inner TypeId;
    // asInt MUST return null (no coercion).
    try std.testing.expectEqual(@as(?TypeId, .s64), result.asTypeId());
    try std.testing.expectEqual(@as(?i64, null), result.asInt());
}

// ── Test: type equality via cmp_eq on .type_tag operands ────────────────

test "comptime: type_tag comparison" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = Module.init(alloc);
    defer module.deinit();
    var b = Builder.init(&module);

    // Returns (s64 == s64) — should yield bool true via the new
    // evalCmp arm for .type_tag operands.
    _ = b.beginFunction(str(&module, "test_type_eq_true"), &.{}, .bool);
    const entry = b.appendBlock(str(&module, "entry"), &.{});
    b.switchToBlock(entry);
    const a = b.constType(.s64);
    const c = b.constType(.s64);
    const eq = b.cmpEq(a, c);
    b.ret(eq, .bool);
    b.finalize();

    // Different TypeIds: (s64 == s32) should be false.
    _ = b.beginFunction(str(&module, "test_type_eq_false"), &.{}, .bool);
    const entry2 = b.appendBlock(str(&module, "entry"), &.{});
    b.switchToBlock(entry2);
    const a2 = b.constType(.s64);
    const c2 = b.constType(.s32);
    const eq2 = b.cmpEq(a2, c2);
    b.ret(eq2, .bool);
    b.finalize();

    var interp = Interpreter.init(&module, alloc);
    defer interp.deinit();
    const r_true = try interp.call(FuncId.fromIndex(0), &.{});
    try std.testing.expectEqual(true, r_true.asBool().?);
    const r_false = try interp.call(FuncId.fromIndex(1), &.{});
    try std.testing.expectEqual(false, r_false.asBool().?);
}

// ── Test: type_name builtin reads .type_tag, returns the typeName ───────

test "comptime: type_name builtin on type_tag" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = Module.init(alloc);
    defer module.deinit();
    var b = Builder.init(&module);

    _ = b.beginFunction(str(&module, "test_type_name"), &.{}, .string);
    const entry = b.appendBlock(str(&module, "entry"), &.{});
    b.switchToBlock(entry);
    const t = b.constType(.s64);
    var args = [_]inst_mod.Ref{t};
    const r = b.callBuiltin(.type_name, &args, .string);
    b.ret(r, .string);
    b.finalize();

    var interp = Interpreter.init(&module, alloc);
    defer interp.deinit();
    const result = try interp.call(FuncId.fromIndex(0), &.{});
    try std.testing.expectEqualStrings("s64", result.asString(&interp).?);
}

// ── Test: type_eq builtin on two .type_tag operands ────────────────────

test "comptime: type_eq builtin on type_tag values" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var module = Module.init(alloc);
    defer module.deinit();
    var b = Builder.init(&module);

    _ = b.beginFunction(str(&module, "test_type_eq_builtin"), &.{}, .bool);
    const entry = b.appendBlock(str(&module, "entry"), &.{});
    b.switchToBlock(entry);
    const a = b.constType(.string);
    const c = b.constType(.string);
    var args = [_]inst_mod.Ref{ a, c };
    const r = b.callBuiltin(.type_eq, &args, .bool);
    b.ret(r, .bool);
    b.finalize();

    var interp = Interpreter.init(&module, alloc);
    defer interp.deinit();
    const result = try interp.call(FuncId.fromIndex(0), &.{});
    try std.testing.expectEqual(true, result.asBool().?);
}
