test: make zig build test actually run all tests + fix latent rot

root.zig had no `test` block, so the test binary discovered zero tests and
trivially "passed" — every src test had silently rotted. Add
`refAllDecls(@This())` to root.zig so all 185 tests run, then fix the rot it
surfaced:

- emit_llvm.test: operands were constants, so LLVM folded the very
  instructions being asserted (fadd/sub/icmp/insertvalue/extractvalue/sext).
  Rewrite to use function-parameter operands; `main` now returns i32 (entry
  convention); tagged-union enum_init lowers via memory, not insertvalue.
- interp.test: switch the per-test allocator to an arena (the interpreter is
  arena-style and intentionally frees little) — clears the transient-Value
  leaks without an ownership-ambiguous source change.
- lower.test: pass `is_imported` to lowerFunction; mark two helpers `pub`; the
  if/else block test now uses a runtime (param) condition since lowering folds
  `if true`.
- print.test: SSA numbering — params occupy %0/%1, so consts start at %2.
- jni_java_emit.test: nested-class refs render in Java source form
  (`SurfaceHolder.Callback`), not the JNI `$` form.

Leaks fixed at the source where ownership was clear: Module gains an arena for
the operand slices the Builder dupes (struct/call/branch/switch args, block
params, lowerFunction params); objcDefinedStateStructType builds its field
slice in that arena and frees its temp name string.
This commit is contained in:
agra
2026-05-29 15:25:00 +03:00
parent 92638ae9b5
commit 4defadf513
9 changed files with 243 additions and 108 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -342,9 +342,11 @@ test "#implements clauses on the class header" {
const out = try emit.emitJavaSource(a, &fcd, .{ .classes = &registry });
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);
}

View File

@@ -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));

View File

@@ -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,
} });
}

View File

@@ -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,

View File

@@ -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" {

View File

@@ -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" {

View File

@@ -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());
}