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

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