diff --git a/src/ir/emit_llvm.test.zig b/src/ir/emit_llvm.test.zig index fa6c64e..b59234e 100644 --- a/src/ir/emit_llvm.test.zig +++ b/src/ir/emit_llvm.test.zig @@ -48,7 +48,9 @@ test "emit: main() returns 42" { // Check LLVM IR contains expected patterns const ir_str = emitter.dumpToString(); try std.testing.expect(std.mem.indexOf(u8, ir_str, "define") != null); - try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64 42") != null); + // `main` is emitted with the C entry-point convention: it returns i32, so + // the s64 const 42 is truncated to `ret i32 42`. + try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i32 42") != null); } test "emit: add(a, b) returns a + b" { @@ -100,12 +102,17 @@ test "emit: float arithmetic" { var b = Builder.init(&module); - _ = b.beginFunction(str(&module, "fmath"), &.{}, .f64); + // Operands must be non-constant (function params) or LLVM constant-folds + // the arithmetic away and no fadd/fmul instruction is emitted. + _ = b.beginFunction(str(&module, "fmath"), &[_]Function.Param{ + .{ .name = str(&module, "x"), .ty = .f64 }, + .{ .name = str(&module, "y"), .ty = .f64 }, + }, .f64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const a = b.constFloat(3.14, .f64); - const a_b = b.constFloat(2.0, .f64); + const a = Ref.fromIndex(0); + const a_b = Ref.fromIndex(1); const sum = b.add(a, a_b, .f64); const product = b.mul(sum, a_b, .f64); b.ret(product, .f64); @@ -129,11 +136,14 @@ test "emit: negation" { var b = Builder.init(&module); - _ = b.beginFunction(str(&module, "negate"), &.{}, .s64); + // Negating a constant folds; negate a param so `sub 0, %x` is emitted. + _ = b.beginFunction(str(&module, "negate"), &[_]Function.Param{ + .{ .name = str(&module, "x"), .ty = .s64 }, + }, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const val = b.constInt(7, .s64); + const val = Ref.fromIndex(0); const neg = b.emit(.{ .neg = .{ .operand = val } }, .s64); b.ret(neg, .s64); b.finalize(); @@ -211,15 +221,19 @@ test "emit: comparison and branch" { var b = Builder.init(&module); - // func f() -> s64 { if (10 < 20) return 1; else return 0; } - _ = b.beginFunction(str(&module, "cmpfn"), &.{}, .s64); + // func f(a, b) -> s64 { if (a < b) return 1; else return 0; } + // Params (not constants) so the icmp isn't folded. + _ = b.beginFunction(str(&module, "cmpfn"), &[_]Function.Param{ + .{ .name = str(&module, "a"), .ty = .s64 }, + .{ .name = str(&module, "b"), .ty = .s64 }, + }, .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 a = b.constInt(10, .s64); - const b_val = b.constInt(20, .s64); + const a = Ref.fromIndex(0); + const b_val = Ref.fromIndex(1); const cond = b.cmpLt(a, b_val); b.condBr(cond, then_bb, &.{}, else_bb, &.{}); @@ -291,11 +305,14 @@ test "emit: widen conversion s32 to s64" { var b = Builder.init(&module); - _ = b.beginFunction(str(&module, "wfn"), &.{}, .s64); + // sext of a constant folds; widen a param so `sext` is emitted. + _ = b.beginFunction(str(&module, "wfn"), &[_]Function.Param{ + .{ .name = str(&module, "x"), .ty = .s32 }, + }, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const val = b.constInt(42, .s32); + const val = Ref.fromIndex(0); const wide = b.widen(val, .s32, .s64); b.ret(wide, .s64); b.finalize(); @@ -357,12 +374,16 @@ test "emit: struct_init and struct_get" { var b = Builder.init(&module); - // func f() -> s64 { p = Point{10, 20}; return p.y; } - _ = b.beginFunction(str(&module, "f"), &.{}, .s64); + // func f(v) -> s64 { p = Point{v, 20}; return p.y; } + // A param operand keeps the aggregate non-constant so insertvalue / + // extractvalue survive (a fully-constant struct would be folded). + _ = b.beginFunction(str(&module, "f"), &[_]Function.Param{ + .{ .name = str(&module, "v"), .ty = .s64 }, + }, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const x = b.constInt(10, .s64); + const x = Ref.fromIndex(0); const y = b.constInt(20, .s64); const p = b.structInit(&.{ x, y }, point_ty); const py = b.structGet(p, 1, .s64); @@ -489,12 +510,15 @@ test "emit: tagged union (enum_init with payload, enum_tag, enum_payload)" { var b = Builder.init(&module); - // func f() -> f64 { s = Shape.Circle(3.14); tag = enum_tag(s); payload = enum_payload(s, 0); return payload; } - _ = b.beginFunction(str(&module, "unionfn"), &.{}, .f64); + // func f(r) -> f64 { s = Shape.Circle(r); ...; return payload; } + // Param payload keeps the union value non-constant (else folded). + _ = b.beginFunction(str(&module, "unionfn"), &[_]Function.Param{ + .{ .name = str(&module, "r"), .ty = .f64 }, + }, .f64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const radius = b.constFloat(3.14, .f64); + const radius = Ref.fromIndex(0); const shape = b.enumInit(0, radius, shape_ty); // Circle = tag 0 const tag = b.emit(.{ .enum_tag = .{ .operand = shape } }, .s64); _ = tag; // tag is used but we just check it doesn't crash @@ -509,7 +533,11 @@ test "emit: tagged union (enum_init with payload, enum_tag, enum_payload)" { try std.testing.expect(emitter.verify()); const ir_str = emitter.dumpToString(); - try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null); + // Tagged-union enum_init/enum_payload lower to a memory pattern + // (alloca + GEP + store/load), not SSA insert/extractvalue. enum_tag + // does emit extractvalue. + try std.testing.expect(std.mem.indexOf(u8, ir_str, "alloca") != null); + try std.testing.expect(std.mem.indexOf(u8, ir_str, "getelementptr") != null); try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null); } @@ -596,12 +624,14 @@ test "emit: length on slice" { var b = Builder.init(&module); // func f(s: string) -> s64 { return s.len; } - _ = b.beginFunction(str(&module, "strlen"), &.{}, .s64); + // A string param keeps the value non-constant so extractvalue survives. + _ = b.beginFunction(str(&module, "strlen"), &[_]Function.Param{ + .{ .name = str(&module, "s"), .ty = .string }, + }, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - // Build a string constant {ptr, len} - const s = b.constString(str(&module, "hello")); + const s = Ref.fromIndex(0); const len = b.emit(.{ .length = .{ .operand = s } }, .s64); b.ret(len, .s64); b.finalize(); @@ -626,12 +656,15 @@ test "emit: data_ptr on slice" { var b = Builder.init(&module); - // func f() -> *u8 { s = "hello"; return s.ptr; } - _ = b.beginFunction(str(&module, "dptr"), &.{}, ptr_ty); + // func f(s: string) -> *u8 { return s.ptr; } + // Param string → extractvalue survives (a constant string would fold). + _ = b.beginFunction(str(&module, "dptr"), &[_]Function.Param{ + .{ .name = str(&module, "s"), .ty = .string }, + }, ptr_ty); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const s = b.constString(str(&module, "test")); + const s = Ref.fromIndex(0); const ptr = b.emit(.{ .data_ptr = .{ .operand = s } }, ptr_ty); b.ret(ptr, ptr_ty); b.finalize(); @@ -688,14 +721,20 @@ test "emit: subslice" { var b = Builder.init(&module); - // func f() -> []u8 { s = "hello"; return s[1..3]; } - _ = b.beginFunction(str(&module, "ssfn"), &.{}, slice_ty); + // func f(s: []u8, lo: s64, hi: s64) -> []u8 { return s[lo..hi]; } + // All operands are params: a constant base folds the GEP, and constant + // lo/hi fold the `hi - lo` subtraction. + _ = b.beginFunction(str(&module, "ssfn"), &[_]Function.Param{ + .{ .name = str(&module, "s"), .ty = slice_ty }, + .{ .name = str(&module, "lo"), .ty = .s64 }, + .{ .name = str(&module, "hi"), .ty = .s64 }, + }, slice_ty); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const s = b.constString(str(&module, "hello")); - const lo = b.constInt(1, .s64); - const hi = b.constInt(3, .s64); + const s = Ref.fromIndex(0); + const lo = Ref.fromIndex(1); + const hi = Ref.fromIndex(2); const sub = b.emit(.{ .subslice = .{ .base = s, .lo = lo, .hi = hi } }, slice_ty); b.ret(sub, slice_ty); b.finalize(); @@ -724,12 +763,15 @@ test "emit: optional_wrap and optional_unwrap (value type)" { var b = Builder.init(&module); - // func f() -> s64 { opt = wrap(42); return unwrap(opt); } - _ = b.beginFunction(str(&module, "optfn"), &.{}, .s64); + // func f(v) -> s64 { opt = wrap(v); return unwrap(opt); } + // Param value keeps the optional non-constant (else insertvalue folds). + _ = b.beginFunction(str(&module, "optfn"), &[_]Function.Param{ + .{ .name = str(&module, "v"), .ty = .s64 }, + }, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const val = b.constInt(42, .s64); + const val = Ref.fromIndex(0); const wrapped = b.optionalWrap(val, opt_ty); const unwrapped = b.optionalUnwrap(wrapped, .s64); b.ret(unwrapped, .s64); @@ -757,11 +799,14 @@ test "emit: optional_has_value" { var b = Builder.init(&module); - _ = b.beginFunction(str(&module, "hasfn"), &.{}, .bool); + // Param value keeps the optional non-constant (else extractvalue folds). + _ = b.beginFunction(str(&module, "hasfn"), &[_]Function.Param{ + .{ .name = str(&module, "v"), .ty = .s64 }, + }, .bool); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const val = b.constInt(10, .s64); + const val = Ref.fromIndex(0); const wrapped = b.optionalWrap(val, opt_ty); const has = b.optionalHasValue(wrapped); b.ret(has, .bool); @@ -849,12 +894,17 @@ test "emit: closure_create" { b.ret(b.constInt(0, .s64), .s64); b.finalize(); - // func f() -> closure { return closure_create(tramp, null); } - _ = b.beginFunction(str(&module, "mkclose"), &.{}, closure_ty); + // func f(e: *void) -> closure { return closure_create(tramp, e); } + // A non-constant env keeps the {fn_ptr, env} aggregate non-constant so + // the insertvalue isn't folded (a null env + constant fn_ptr would fold). + const env_ty = module.types.ptrTo(.void); + _ = b.beginFunction(str(&module, "mkclose"), &[_]inst_mod.Function.Param{ + .{ .name = str(&module, "e"), .ty = env_ty }, + }, closure_ty); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const cl = b.emit(.{ .closure_create = .{ .func = tramp_id, .env = Ref.none } }, closure_ty); + const cl = b.emit(.{ .closure_create = .{ .func = tramp_id, .env = Ref.fromIndex(0) } }, closure_ty); b.ret(cl, closure_ty); b.finalize(); @@ -877,12 +927,15 @@ test "emit: box_any and unbox_any" { var b = Builder.init(&module); - // func f() -> s64 { a = box(42); return unbox(a); } - _ = b.beginFunction(str(&module, "anyfn"), &.{}, .s64); + // func f(v) -> s64 { a = box(v); return unbox(a); } + // Param value keeps the boxed Any non-constant (else insertvalue folds). + _ = b.beginFunction(str(&module, "anyfn"), &[_]Function.Param{ + .{ .name = str(&module, "v"), .ty = .s64 }, + }, .s64); const entry = b.appendBlock(str(&module, "entry"), &.{}); b.switchToBlock(entry); - const val = b.constInt(42, .s64); + const val = Ref.fromIndex(0); const boxed = b.emit(.{ .box_any = .{ .operand = val, .source_type = .s64 } }, .any); const unboxed = b.emit(.{ .unbox_any = .{ .operand = boxed } }, .s64); b.ret(unboxed, .s64); diff --git a/src/ir/interp.test.zig b/src/ir/interp.test.zig index 4901481..9d943d8 100644 --- a/src/ir/interp.test.zig +++ b/src/ir/interp.test.zig @@ -26,7 +26,9 @@ fn str(module: *Module, s: []const u8) types.StringId { // ── Basic interpreter tests (migrated from interp.zig) ────────────────── test "interpret: compute(5) = 25" { - const alloc = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var module = Module.init(alloc); defer module.deinit(); @@ -51,7 +53,9 @@ test "interpret: compute(5) = 25" { } test "interpret: if/else branching" { - const alloc = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var module = Module.init(alloc); defer module.deinit(); @@ -89,7 +93,9 @@ test "interpret: if/else branching" { } test "interpret: function calling another function" { - const alloc = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var module = Module.init(alloc); defer module.deinit(); @@ -129,7 +135,9 @@ test "interpret: function calling another function" { } test "interpret: alloca/store/load" { - const alloc = std.testing.allocator; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var module = Module.init(alloc); defer module.deinit(); @@ -164,7 +172,9 @@ test "interpret: alloca/store/load" { // Expected: 55 test "comptime: while loop — sumOf10 = 55" { - const alloc = std.testing.allocator; + 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); @@ -222,7 +232,9 @@ test "comptime: while loop — sumOf10 = 55" { // Expected: 42 + 99 = 141 test "comptime: optional coalesce — ct_sum = 141" { - const alloc = std.testing.allocator; + 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); @@ -267,7 +279,9 @@ test "comptime: optional coalesce — ct_sum = 141" { // Expected: 77 test "comptime: optional unwrap — 77" { - const alloc = std.testing.allocator; + 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); @@ -296,7 +310,9 @@ test "comptime: optional unwrap — 77" { // Expected: fib(10) = 55 test "comptime: recursive fibonacci — fib(10) = 55" { - const alloc = std.testing.allocator; + 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); @@ -341,7 +357,9 @@ test "comptime: recursive fibonacci — fib(10) = 55" { // Expected: compute(5) = 7 test "comptime: compute(5) = 7" { - const alloc = std.testing.allocator; + 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); @@ -370,7 +388,9 @@ test "comptime: compute(5) = 7" { // Simulates calling add(25, 5) to verify chaining works. test "comptime: chained — add(add(10,15), 5) = 30" { - const alloc = std.testing.allocator; + 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); @@ -406,7 +426,9 @@ test "comptime: chained — add(add(10,15), 5) = 30" { // Expected: 7 test "comptime: struct init and field access — 7" { - const alloc = std.testing.allocator; + 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); @@ -438,7 +460,9 @@ test "comptime: struct init and field access — 7" { // Expected: compute(3.0) = 8.5 test "comptime: float arithmetic — 8.5" { - const alloc = std.testing.allocator; + 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); @@ -469,7 +493,9 @@ test "comptime: float arithmetic — 8.5" { // test(false, true) → (false and true) or (not false) = false or true = true test "comptime: boolean logic" { - const alloc = std.testing.allocator; + 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); @@ -505,7 +531,9 @@ test "comptime: boolean logic" { // ── Test: negation ────────────────────────────────────────────────────── test "comptime: negation — int and float" { - const alloc = std.testing.allocator; + 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); @@ -529,7 +557,9 @@ test "comptime: negation — int and float" { // ── Test: modulo ──────────────────────────────────────────────────────── test "comptime: modulo — 17 mod 5 = 2" { - const alloc = std.testing.allocator; + 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); @@ -554,7 +584,9 @@ test "comptime: modulo — 17 mod 5 = 2" { // Simulates: match tag { 0 => 10, 1 => 20, else => 30 } test "comptime: switch_br dispatch" { - const alloc = std.testing.allocator; + 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); @@ -603,7 +635,9 @@ test "comptime: switch_br dispatch" { // ── Test: enum init + tag extraction ──────────────────────────────────── test "comptime: enum init and tag" { - const alloc = std.testing.allocator; + 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); @@ -627,7 +661,9 @@ test "comptime: enum init and tag" { // ── Test: conversion (widen/narrow passthrough) ───────────────────────── test "comptime: widen/narrow passthrough" { - const alloc = std.testing.allocator; + 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); @@ -651,7 +687,9 @@ test "comptime: widen/narrow passthrough" { // ── Test: const_type produces a Value.type_tag ────────────────────────── test "comptime: const_type yields type_tag" { - const alloc = std.testing.allocator; + 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); @@ -680,7 +718,9 @@ test "comptime: const_type yields type_tag" { // ── Test: type equality via cmp_eq on .type_tag operands ──────────────── test "comptime: type_tag comparison" { - const alloc = std.testing.allocator; + 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); @@ -717,7 +757,9 @@ test "comptime: type_tag comparison" { // ── Test: type_name builtin reads .type_tag, returns the typeName ─────── test "comptime: type_name builtin on type_tag" { - const alloc = std.testing.allocator; + 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); @@ -740,7 +782,9 @@ test "comptime: type_name builtin on type_tag" { // ── Test: type_eq builtin on two .type_tag operands ──────────────────── test "comptime: type_eq builtin on type_tag values" { - const alloc = std.testing.allocator; + 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); diff --git a/src/ir/jni_java_emit.test.zig b/src/ir/jni_java_emit.test.zig index f016dfd..86a1d80 100644 --- a/src/ir/jni_java_emit.test.zig +++ b/src/ir/jni_java_emit.test.zig @@ -342,9 +342,11 @@ test "#implements clauses on the class header" { const out = try emit.emitJavaSource(a, &fcd, .{ .classes = ®istry }); defer a.free(out); + // Registry value `android/view/SurfaceHolder$Callback` is emitted in Java + // *source* form: `/` → `.` and the nested-class `$` → `.`. try std.testing.expect(std.mem.indexOf( u8, out, - "public class SxApp extends android.app.Activity implements android.view.SurfaceHolder$Callback, java.lang.Runnable {", + "public class SxApp extends android.app.Activity implements android.view.SurfaceHolder.Callback, java.lang.Runnable {", ) != null); } diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 53a6ea5..6b51667 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -64,7 +64,7 @@ test "lower: simple function with arithmetic" { }; var lowering = Lowering.init(&module); - lowering.lowerFunction(&fn_decl, "add"); + lowering.lowerFunction(&fn_decl, "add", false); // Verify try std.testing.expectEqual(@as(usize, 1), module.functions.items.len); @@ -91,10 +91,16 @@ test "lower: if/else generates basic blocks" { var module = ir_mod.Module.init(alloc); defer module.deinit(); - // Build AST: test :: () -> s64 { if true { return 1; } else { return 2; } } + // Build AST: test :: (c: bool) -> s64 { if c { return 1; } else { return 2; } } + // The condition must be a runtime value (a param) — a constant `if true` + // is folded by lowering to a single block, defeating the branch test. const cond_node = alloc.create(Node) catch unreachable; defer alloc.destroy(cond_node); - cond_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .bool_literal = .{ .value = true } } }; + cond_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "c" } } }; + + const cond_ty = alloc.create(Node) catch unreachable; + defer alloc.destroy(cond_ty); + cond_ty.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "bool", .is_generic = false } } }; const ret1_val = alloc.create(Node) catch unreachable; defer alloc.destroy(ret1_val); @@ -139,13 +145,13 @@ test "lower: if/else generates basic blocks" { const fn_decl = ast.FnDecl{ .name = "test_if", - .params = &.{}, + .params = &.{.{ .name = "c", .name_span = .{ .start = 0, .end = 0 }, .type_expr = cond_ty }}, .return_type = ret_type, .body = fn_body, }; var lowering = Lowering.init(&module); - lowering.lowerFunction(&fn_decl, "test_if"); + lowering.lowerFunction(&fn_decl, "test_if", false); // Verify: should have 4 blocks (entry, if.then, if.else, if.merge) const func = module.getFunction(FuncId.fromIndex(0)); @@ -202,7 +208,7 @@ test "lower: while loop generates header/body/exit blocks" { }; var lowering = Lowering.init(&module); - lowering.lowerFunction(&fn_decl, "loop_test"); + lowering.lowerFunction(&fn_decl, "loop_test", false); // Verify: should have 4 blocks (entry, while.hdr, while.body, while.exit) const func = module.getFunction(FuncId.fromIndex(0)); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index acd5e82..7d4e1f9 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1216,17 +1216,21 @@ pub const Lowering = struct { const wants_ctx = self.funcWantsImplicitCtx(fd); - // Build param list + // Build param list. `Function.init` borrows the slice (it does not + // dupe), so this storage must outlive the local — build it in the + // module's slice arena (freed at module deinit) rather than via + // `self.alloc`, which would leak (Function.deinit never frees params). + const param_alloc = self.module.slice_arena.allocator(); var params = std.ArrayList(Function.Param).empty; if (wants_ctx) { - params.append(self.alloc, .{ + params.append(param_alloc, .{ .name = self.module.types.internString("__sx_ctx"), .ty = self.module.types.ptrTo(.void), }) catch unreachable; } for (fd.params) |p| { const pty = self.resolveParamType(&p); - params.append(self.alloc, .{ + params.append(param_alloc, .{ .name = self.module.types.internString(p.name), .ty = pty, }) catch unreachable; @@ -5054,7 +5058,7 @@ pub const Lowering = struct { /// patterns rule). /// /// Returns an allocator-owned slice; caller frees via `self.alloc`. - fn objcTypeEncodingFromSignature( + pub fn objcTypeEncodingFromSignature( self: *Lowering, return_ty: TypeId, param_tys: []const TypeId, @@ -5268,11 +5272,16 @@ pub const Lowering = struct { /// Foreign-class members other than `.field` are ignored here — /// methods / `#extends` / `#implements` don't contribute to the /// state layout. - fn objcDefinedStateStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { + pub fn objcDefinedStateStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { const state_name = std.fmt.allocPrint(self.alloc, "__{s}State", .{fcd.name}) catch unreachable; + defer self.alloc.free(state_name); // internString copies; the temp isn't needed after. const name_id = self.module.types.internString(state_name); if (self.module.types.findByName(name_id)) |existing| return existing; + // The interned struct's `fields` slice lives for the module's lifetime; + // allocate it (and the building ArrayList) in the module arena so it's + // freed at module deinit rather than leaking through `self.alloc`. + const field_alloc = self.module.slice_arena.allocator(); var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; // M4.0: prepend __sx_allocator at field index 0 — captured at +alloc // time, read at -dealloc time to free the state struct through the @@ -5280,7 +5289,7 @@ pub const Lowering = struct { // emitObjcDefinedClassPropertyImps + lookupObjcDefinedStateFieldOnPointer) // naturally finds user fields at their post-shift indices. if (self.objcStateAllocatorType()) |allocator_ty| { - fields.append(self.alloc, .{ + fields.append(field_alloc, .{ .name = self.module.types.internString("__sx_allocator"), .ty = allocator_ty, }) catch unreachable; @@ -5290,14 +5299,14 @@ pub const Lowering = struct { .field => |f| { const f_name_id = self.module.types.internString(f.name); const f_ty = self.resolveType(f.field_type); - fields.append(self.alloc, .{ .name = f_name_id, .ty = f_ty }) catch unreachable; + fields.append(field_alloc, .{ .name = f_name_id, .ty = f_ty }) catch unreachable; }, else => {}, } } return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, - .fields = fields.toOwnedSlice(self.alloc) catch unreachable, + .fields = fields.toOwnedSlice(field_alloc) catch unreachable, } }); } diff --git a/src/ir/module.zig b/src/ir/module.zig index 9affb95..031731b 100644 --- a/src/ir/module.zig +++ b/src/ir/module.zig @@ -52,6 +52,12 @@ pub const Module = struct { /// `members` for fields / methods / `#extends` / `#implements`. objc_defined_class_cache: std.ArrayList(ObjcDefinedClassEntry), alloc: Allocator, + /// Owns the per-instruction operand slices the Builder dupes (aggregate + /// fields, call args, branch args, switch cases, block params). These live + /// for the module's lifetime and are never freed individually — an arena + /// reclaims them all in `deinit`, matching the compiler's arena-style + /// memory model and keeping the leak-checking test allocator clean. + slice_arena: std.heap.ArenaAllocator, /// True when this module's program imports `std.sx` (and therefore /// has the `Context` type). Set by lowering's Pass 0 pre-scan. Read /// by emit_llvm to decide whether closure/fn-pointer call sites @@ -95,6 +101,7 @@ pub const Module = struct { .objc_class_cache = std.ArrayList(ObjcClassEntry).empty, .objc_defined_class_cache = std.ArrayList(ObjcDefinedClassEntry).empty, .alloc = alloc, + .slice_arena = std.heap.ArenaAllocator.init(alloc), }; } @@ -109,6 +116,7 @@ pub const Module = struct { self.objc_class_cache.deinit(self.alloc); self.objc_defined_class_cache.deinit(self.alloc); self.types.deinit(); + self.slice_arena.deinit(); } /// Linear scan — N is the count of UNIQUE selectors per program, @@ -258,7 +266,7 @@ pub const Builder = struct { if (existing.name == name and existing.is_extern) { existing.is_extern = false; existing.linkage = .internal; - existing.params = self.module.alloc.dupe(Function.Param, params) catch params; + existing.params = self.module.slice_arena.allocator().dupe(Function.Param, params) catch params; existing.ret = ret_ty; const id = FuncId.fromIndex(@intCast(i)); self.func = id; @@ -298,7 +306,7 @@ pub const Builder = struct { const id = BlockId.fromIndex(@intCast(f.blocks.items.len)); // Dupe params so the block owns the memory (callers may pass stack slices). const owned_params = if (params.len > 0) - (self.module.alloc.dupe(TypeId, params) catch unreachable) + (self.module.slice_arena.allocator().dupe(TypeId, params) catch unreachable) else params; f.blocks.append(self.module.alloc, Block.init(name, owned_params)) catch unreachable; @@ -443,7 +451,7 @@ pub const Builder = struct { // ── Struct ops ────────────────────────────────────────────────── pub fn structInit(self: *Builder, fields: []const Ref, ty: TypeId) Ref { - const owned = self.module.alloc.dupe(Ref, fields) catch unreachable; + const owned = self.module.slice_arena.allocator().dupe(Ref, fields) catch unreachable; return self.emit(.{ .struct_init = .{ .fields = owned } }, ty); } @@ -486,23 +494,23 @@ pub const Builder = struct { // ── Calls ─────────────────────────────────────────────────────── pub fn call(self: *Builder, callee: FuncId, args: []const Ref, ret_ty: TypeId) Ref { - const owned = self.module.alloc.dupe(Ref, args) catch unreachable; + const owned = self.module.slice_arena.allocator().dupe(Ref, args) catch unreachable; return self.emit(.{ .call = .{ .callee = callee, .args = owned } }, ret_ty); } pub fn callClosure(self: *Builder, callee: Ref, args: []const Ref, ret_ty: TypeId) Ref { - const owned = self.module.alloc.dupe(Ref, args) catch unreachable; + const owned = self.module.slice_arena.allocator().dupe(Ref, args) catch unreachable; return self.emit(.{ .call_closure = .{ .callee = callee, .args = owned } }, ret_ty); } pub fn callBuiltin(self: *Builder, builtin: inst.BuiltinId, args: []const Ref, ret_ty: TypeId) Ref { - const owned = self.module.alloc.dupe(Ref, args) catch unreachable; + const owned = self.module.slice_arena.allocator().dupe(Ref, args) catch unreachable; return self.emit(.{ .call_builtin = .{ .builtin = builtin, .args = owned } }, ret_ty); } pub fn compilerCall(self: *Builder, name: []const u8, args: []const Ref, ret_ty: TypeId) Ref { const name_id = self.module.types.strings.intern(self.module.alloc, name); - const owned = self.module.alloc.dupe(Ref, args) catch unreachable; + const owned = self.module.slice_arena.allocator().dupe(Ref, args) catch unreachable; return self.emit(.{ .compiler_call = .{ .name = @intFromEnum(name_id), .args = owned } }, ret_ty); } @@ -531,13 +539,13 @@ pub const Builder = struct { // ── Terminators ───────────────────────────────────────────────── pub fn br(self: *Builder, target: BlockId, args: []const Ref) void { - const owned = self.module.alloc.dupe(Ref, args) catch unreachable; + const owned = self.module.slice_arena.allocator().dupe(Ref, args) catch unreachable; self.emitVoid(.{ .br = .{ .target = target, .args = owned } }, .void); } pub fn condBr(self: *Builder, cond: Ref, then_target: BlockId, then_args: []const Ref, else_target: BlockId, else_args: []const Ref) void { - const t_args = self.module.alloc.dupe(Ref, then_args) catch unreachable; - const e_args = self.module.alloc.dupe(Ref, else_args) catch unreachable; + const t_args = self.module.slice_arena.allocator().dupe(Ref, then_args) catch unreachable; + const e_args = self.module.slice_arena.allocator().dupe(Ref, else_args) catch unreachable; self.emitVoid(.{ .cond_br = .{ .cond = cond, .then_target = then_target, @@ -556,8 +564,8 @@ pub const Builder = struct { } pub fn switchBr(self: *Builder, operand: Ref, cases: []const inst.SwitchBranch.Case, default: BlockId, default_args: []const Ref) void { - const owned_cases = self.module.alloc.dupe(inst.SwitchBranch.Case, cases) catch unreachable; - const owned_default_args = self.module.alloc.dupe(Ref, default_args) catch unreachable; + const owned_cases = self.module.slice_arena.allocator().dupe(inst.SwitchBranch.Case, cases) catch unreachable; + const owned_default_args = self.module.slice_arena.allocator().dupe(Ref, default_args) catch unreachable; self.emitVoid(.{ .switch_br = .{ .operand = operand, .cases = owned_cases, diff --git a/src/ir/print.test.zig b/src/ir/print.test.zig index 3bdbb49..6614cc9 100644 --- a/src/ir/print.test.zig +++ b/src/ir/print.test.zig @@ -48,8 +48,9 @@ test "print simple add function" { try std.testing.expect(std.mem.indexOf(u8, output, "func @add(a: s64, b: s64) -> s64") != null); try std.testing.expect(std.mem.indexOf(u8, output, "entry:") != null); try std.testing.expect(std.mem.indexOf(u8, output, "const 10 : s64") != null); - try std.testing.expect(std.mem.indexOf(u8, output, "add %0, %1 : s64") != null); - try std.testing.expect(std.mem.indexOf(u8, output, "ret %2") != null); + // Params occupy value slots %0/%1, so the two consts are %2/%3 and their sum %4. + try std.testing.expect(std.mem.indexOf(u8, output, "add %2, %3 : s64") != null); + try std.testing.expect(std.mem.indexOf(u8, output, "ret %4") != null); } test "print conditional branch" { diff --git a/src/parser.zig b/src/parser.zig index e47e530..f0e2193 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -3487,8 +3487,13 @@ test "parse void function with arrow body" { const decl = root.data.root.decls[0]; try std.testing.expect(decl.data == .fn_decl); try std.testing.expectEqualStrings("foo", decl.data.fn_decl.name); - try std.testing.expect(decl.data.fn_decl.body.data == .int_literal); - try std.testing.expectEqual(@as(i64, 42), decl.data.fn_decl.body.data.int_literal.value); + try std.testing.expect(decl.data.fn_decl.is_arrow); + // Arrow bodies are wrapped in a block; the expression is the sole stmt. + const body = decl.data.fn_decl.body; + try std.testing.expect(body.data == .block); + try std.testing.expectEqual(@as(usize, 1), body.data.block.stmts.len); + try std.testing.expect(body.data.block.stmts[0].data == .int_literal); + try std.testing.expectEqual(@as(i64, 42), body.data.block.stmts[0].data.int_literal.value); } test "parse hex and binary literals" { @@ -3526,14 +3531,13 @@ test "parse lambda with generic params" { var parser = Parser.init(arena.allocator(), source); const root = try parser.parse(); const decl = root.data.root.decls[0]; - try std.testing.expect(decl.data == .const_decl); - const lambda = decl.data.const_decl.value; - try std.testing.expect(lambda.data == .lambda); - try std.testing.expectEqual(@as(usize, 1), lambda.data.lambda.params.len); - try std.testing.expectEqualStrings("x", lambda.data.lambda.params[0].name); - // has generic type param - try std.testing.expectEqual(@as(usize, 1), lambda.data.lambda.type_params.len); - try std.testing.expectEqualStrings("T", lambda.data.lambda.type_params[0].name); + // A named `::` arrow function is a fn_decl (carrying its own type params). + try std.testing.expect(decl.data == .fn_decl); + const fd = decl.data.fn_decl; + try std.testing.expectEqual(@as(usize, 1), fd.params.len); + try std.testing.expectEqualStrings("x", fd.params[0].name); + try std.testing.expectEqual(@as(usize, 1), fd.type_params.len); + try std.testing.expectEqualStrings("T", fd.type_params[0].name); } test "parse lambda with return type" { @@ -3543,12 +3547,11 @@ test "parse lambda with return type" { var parser = Parser.init(arena.allocator(), source); const root = try parser.parse(); const decl = root.data.root.decls[0]; - try std.testing.expect(decl.data == .const_decl); - const lambda = decl.data.const_decl.value; - try std.testing.expect(lambda.data == .lambda); - try std.testing.expect(lambda.data.lambda.return_type != null); - try std.testing.expect(lambda.data.lambda.return_type.?.data == .type_expr); - try std.testing.expectEqualStrings("s32", lambda.data.lambda.return_type.?.data.type_expr.name); + try std.testing.expect(decl.data == .fn_decl); + const fd = decl.data.fn_decl; + try std.testing.expect(fd.return_type != null); + try std.testing.expect(fd.return_type.?.data == .type_expr); + try std.testing.expectEqualStrings("s32", fd.return_type.?.data.type_expr.name); } test "parse match with else arm" { diff --git a/src/root.zig b/src/root.zig index fbef98f..271e069 100644 --- a/src/root.zig +++ b/src/root.zig @@ -20,3 +20,12 @@ pub const lsp = struct { pub const types = @import("lsp/types.zig"); pub const document = @import("lsp/document.zig"); }; + +test { + // Discover every test in the module graph so `zig build test` actually + // runs them. Without this, the test binary finds no `test` blocks at the + // root and trivially "passes" while exercising nothing. Nested barrels + // (e.g. ir/ir.zig) carry their own `test { refAllDecls }`, so this chains + // into them. + @import("std").testing.refAllDecls(@This()); +}