Files
sx/src/ir/emit_llvm.test.zig
agra d6078c2e6b test(backend): lock LLVM type/ABI shapes before A7.1 extraction (A7.1 scaffolding step 1)
Test-first scaffolding for LLVM backend modularization (Phase A7.1) before the
type/ABI helpers move into src/backend/llvm/{types,abi}.zig. Visibility-only
change to the targets — no behavior change. Closes the ARCH-SAFETY "no generic
ABI snapshot" gap.

- 2 new emit_llvm.test.zig tests:
  - abiCoerceParamType across every C-ABI size bucket: <=8 -> i64, 9-16 ->
    [2 x i64], >16 -> ptr, HFA (all-float/all-double, <=4 fields) -> unchanged,
    string -> ptr, slice -> ptr, scalar -> unchanged. Built via a local
    internStruct helper (field slice in the module arena -> no testing-allocator
    leak); asserts against emitter.cached_* + LLVMArrayType2.
  - needsByval: true only for >16-byte non-HFA struct; false for <=16 / HFA /
    string / slice / non-struct.
- 1 new .ir snapshot: 1202-ffi-cc-c-large-aggregate (the canonical callconv(.c)
  >16-byte byval example that directly documents abiCoerceParamType) — pins the
  byval param path end-to-end (5 byval + entry reload + 2 sret from Arena.init).
  Path-free + idempotent (verified across two captures). Suite count unchanged
  (snapshot added to an existing example).
- Widened abiCoerceParamType + needsByval to pub (visibility only;
  abiCoerceParamTypeEx/materializeByvalArg/verifySizes stay private — move with
  callers in sub-step 2). No logic touched.
- Recorded the A7.1 coverage inventory + residual gaps (wasm32 usize->i32 branch,
  fn-ptr large-aggregate 1203/1204) in ARCH-SAFETY.md.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (no churn
beyond the new 1202 .ir).
2026-06-03 08:53:51 +03:00

1100 lines
41 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Tests for the IR-to-LLVM emitter (emit_llvm.zig).
const std = @import("std");
const types = @import("types.zig");
const inst_mod = @import("inst.zig");
const mod_mod = @import("module.zig");
const emit_mod = @import("emit_llvm.zig");
const TypeId = types.TypeId;
const Ref = inst_mod.Ref;
const FuncId = inst_mod.FuncId;
const Function = inst_mod.Function;
const Module = mod_mod.Module;
const Builder = mod_mod.Builder;
const LLVMEmitter = emit_mod.LLVMEmitter;
// ── Helper ──────────────────────────────────────────────────────────────
fn str(module: *Module, s: []const u8) types.StringId {
return module.types.internString(s);
}
// ── Tests ───────────────────────────────────────────────────────────────
test "emit: main() returns 42" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func main() -> s64 { return 42; }
_ = b.beginFunction(str(&module, "main"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const c42 = b.constInt(42, .s64);
b.ret(c42, .s64);
b.finalize();
// Emit to LLVM
var emitter = LLVMEmitter.init(alloc, &module, "test_ret42", .{});
defer emitter.deinit();
emitter.emit();
// Verify the module is valid
try std.testing.expect(emitter.verify());
// Check LLVM IR contains expected patterns
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "define") != 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" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func add(a: s64, b: s64) -> 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);
// Parameters are refs 0 and 1 — but in our IR they're passed as
// arguments to the interpreter. For the LLVM emitter, we need to
// load them from LLVM function params. For now, use constInt as
// placeholders since we haven't wired up param→ref mapping yet.
//
// Actually, looking at the IR design: the Builder's inst_counter starts
// at 0, and params are accessed differently. The lowering pass emits
// alloca+store for params. For this test, we use const_int to test
// the add instruction directly.
const a = b.constInt(10, .s64);
const a_b = b.constInt(32, .s64);
const sum = b.add(a, a_b, .s64);
b.ret(sum, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_add", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "add") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: float arithmetic" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// 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 = 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);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_float", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "fadd") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "fmul") != null);
}
test "emit: negation" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const neg = b.emit(.{ .neg = .{ .operand = val } }, .s64);
b.ret(neg, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_neg", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// LLVM represents neg as "sub nsw i64 0, %val" or "sub i64 0, %val"
try std.testing.expect(std.mem.indexOf(u8, ir_str, "sub") != null);
}
test "emit: void function" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "noop"), &.{}, .void);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
b.retVoid();
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_void", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret void") != null);
}
test "emit: alloca, store, load" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func f() -> s64 { var x: s64 = 10; return x; }
_ = b.beginFunction(str(&module, "f"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const x_ptr = b.alloca(.s64); // alloca s64 → *s64
const ten = b.constInt(10, .s64);
b.store(x_ptr, ten); // store 10 → *x
const loaded = b.load(x_ptr, .s64); // load *x → s64
b.ret(loaded, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_mem", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "alloca") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "store") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "load") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: comparison and branch" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const b_val = Ref.fromIndex(1);
const cond = b.cmpLt(a, b_val);
b.condBr(cond, then_bb, &.{}, else_bb, &.{});
b.switchToBlock(then_bb);
const one = b.constInt(1, .s64);
b.ret(one, .s64);
b.switchToBlock(else_bb);
const zero = b.constInt(0, .s64);
b.ret(zero, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_cmp", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "icmp") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "br i1") != null);
}
test "emit: function call" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func add(a: s64, b: s64) -> s64 { return a + b; } (using constants)
const add_id = b.beginFunction(str(&module, "addfn"), &[_]Function.Param{
.{ .name = str(&module, "a"), .ty = .s64 },
.{ .name = str(&module, "b"), .ty = .s64 },
}, .s64);
const add_entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(add_entry);
const p0 = b.constInt(0, .s64); // placeholder
const p1 = b.constInt(0, .s64);
const sum = b.add(p0, p1, .s64);
b.ret(sum, .s64);
b.finalize();
// func main() -> s64 { return addfn(3, 4); }
_ = b.beginFunction(str(&module, "main"), &.{}, .s64);
const main_entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(main_entry);
const three = b.constInt(3, .s64);
const four = b.constInt(4, .s64);
const result = b.call(add_id, &.{ three, four }, .s64);
b.ret(result, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_call", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "call") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "addfn") != null);
}
test "emit: widen conversion s32 to s64" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const wide = b.widen(val, .s32, .s64);
b.ret(wide, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_widen", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "sext") != null);
}
test "emit: type conversion toLLVMType" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var emitter = LLVMEmitter.init(alloc, &module, "test_types", .{});
defer emitter.deinit();
// Just verify toLLVMType doesn't crash for all builtin types
_ = emitter.toLLVMType(.void);
_ = emitter.toLLVMType(.bool);
_ = emitter.toLLVMType(.s8);
_ = emitter.toLLVMType(.s16);
_ = emitter.toLLVMType(.s32);
_ = emitter.toLLVMType(.s64);
_ = emitter.toLLVMType(.u8);
_ = emitter.toLLVMType(.u16);
_ = emitter.toLLVMType(.u32);
_ = emitter.toLLVMType(.u64);
_ = emitter.toLLVMType(.f32);
_ = emitter.toLLVMType(.f64);
_ = emitter.toLLVMType(.string);
_ = emitter.toLLVMType(.any);
_ = emitter.toLLVMType(.noreturn);
}
// ── A7.1 scaffolding: ABI param coercion ────────────────────────────
// Lock the C-ABI struct-coercion buckets (abiCoerceParamType / needsByval),
// which feed callconv(.c) / #foreign signatures, before they move to
// src/backend/llvm/abi.zig in A7.1 sub-step 2.
const llvm = @import("../llvm_api.zig");
const cc = llvm.c;
fn internStruct(module: *Module, name: []const u8, field_tys: []const TypeId) TypeId {
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
defer fields.deinit(std.testing.allocator);
for (field_tys, 0..) |fty, i| {
var nb: [8]u8 = undefined;
const fname = std.fmt.bufPrint(&nb, "f{d}", .{i}) catch unreachable;
fields.append(std.testing.allocator, .{ .name = str(module, fname), .ty = fty }) catch unreachable;
}
// Dupe into the module arena so the interned struct's field slice lives for
// the module's lifetime (freed at module.deinit) — no testing-allocator leak.
const owned = module.slice_arena.allocator().dupe(types.TypeInfo.StructInfo.Field, fields.items) catch unreachable;
return module.types.intern(.{ .@"struct" = .{ .name = str(module, name), .fields = owned } });
}
test "emit: abiCoerceParamType coerces C-ABI structs by size bucket" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
// Intern the shapes before building the emitter (toLLVMType reads live).
const small = internStruct(&module, "Small", &.{ .s32, .s32 }); // 8 bytes
const mid = internStruct(&module, "Mid", &.{ .s64, .s64 }); // 16 bytes
const big = internStruct(&module, "Big", &.{ .s64, .s64, .s64 }); // 24 bytes
const hfa_f = internStruct(&module, "HfaF", &.{ .f32, .f32, .f32, .f32 }); // 16, all-float
const hfa_d = internStruct(&module, "HfaD", &.{ .f64, .f64 }); // 16, all-double
const sl = module.types.sliceOf(.s32);
var emitter = LLVMEmitter.init(alloc, &module, "test_abi", .{});
defer emitter.deinit();
// ≤ 8 bytes → i64.
try std.testing.expect(emitter.abiCoerceParamType(small, emitter.toLLVMType(small)) == emitter.cached_i64);
// 916 bytes → [2 x i64].
try std.testing.expect(emitter.abiCoerceParamType(mid, emitter.toLLVMType(mid)) == cc.LLVMArrayType2(emitter.cached_i64, 2));
// > 16 bytes → ptr (passed byval at the call/sig sites).
try std.testing.expect(emitter.abiCoerceParamType(big, emitter.toLLVMType(big)) == emitter.cached_ptr);
// HFA (all-float / all-double, ≤ 4 fields) → unchanged.
try std.testing.expect(emitter.abiCoerceParamType(hfa_f, emitter.toLLVMType(hfa_f)) == emitter.toLLVMType(hfa_f));
try std.testing.expect(emitter.abiCoerceParamType(hfa_d, emitter.toLLVMType(hfa_d)) == emitter.toLLVMType(hfa_d));
// string / slice collapse to ptr at the C-API boundary (len dropped).
try std.testing.expect(emitter.abiCoerceParamType(.string, emitter.toLLVMType(.string)) == emitter.cached_ptr);
try std.testing.expect(emitter.abiCoerceParamType(sl, emitter.toLLVMType(sl)) == emitter.cached_ptr);
// Scalars pass through unchanged.
try std.testing.expect(emitter.abiCoerceParamType(.s32, emitter.toLLVMType(.s32)) == emitter.toLLVMType(.s32));
}
test "emit: needsByval only for > 16-byte non-HFA structs" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const small = internStruct(&module, "Small", &.{ .s32, .s32 });
const mid = internStruct(&module, "Mid", &.{ .s64, .s64 });
const big = internStruct(&module, "Big", &.{ .s64, .s64, .s64 });
const hfa_d = internStruct(&module, "HfaD", &.{ .f64, .f64 });
const sl = module.types.sliceOf(.s32);
var emitter = LLVMEmitter.init(alloc, &module, "test_byval", .{});
defer emitter.deinit();
try std.testing.expect(emitter.needsByval(big, emitter.toLLVMType(big))); // > 16
try std.testing.expect(!emitter.needsByval(small, emitter.toLLVMType(small)));
try std.testing.expect(!emitter.needsByval(mid, emitter.toLLVMType(mid))); // exactly 16
try std.testing.expect(!emitter.needsByval(hfa_d, emitter.toLLVMType(hfa_d))); // HFA
try std.testing.expect(!emitter.needsByval(.string, emitter.toLLVMType(.string)));
try std.testing.expect(!emitter.needsByval(sl, emitter.toLLVMType(sl)));
try std.testing.expect(!emitter.needsByval(.s32, emitter.toLLVMType(.s32))); // non-struct
}
// ── Struct/Enum/Union tests ─────────────────────────────────────────
test "emit: struct_init and struct_get" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
// Create a struct type: Point { x: s64, y: s64 }
const fields = &[_]types.TypeInfo.StructInfo.Field{
.{ .name = str(&module, "x"), .ty = .s64 },
.{ .name = str(&module, "y"), .ty = .s64 },
};
const owned_fields = alloc.dupe(types.TypeInfo.StructInfo.Field, fields) catch unreachable;
defer alloc.free(owned_fields);
const point_ty = module.types.intern(.{ .@"struct" = .{
.name = str(&module, "Point"),
.fields = owned_fields,
} });
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const y = b.constInt(20, .s64);
const p = b.structInit(&.{ x, y }, point_ty);
const py = b.structGet(p, 1, .s64);
b.ret(py, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_struct", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: struct_gep (pointer to field)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
// Create struct type
const fields = &[_]types.TypeInfo.StructInfo.Field{
.{ .name = str(&module, "x"), .ty = .s64 },
.{ .name = str(&module, "y"), .ty = .s64 },
};
const owned_fields = alloc.dupe(types.TypeInfo.StructInfo.Field, fields) catch unreachable;
defer alloc.free(owned_fields);
const point_ty = module.types.intern(.{ .@"struct" = .{
.name = str(&module, "Point"),
.fields = owned_fields,
} });
const ptr_s64 = module.types.ptrTo(.s64);
var b = Builder.init(&module);
// func f() -> s64 { var p: Point; p.y = 42; return p.y; }
_ = b.beginFunction(str(&module, "gepfn"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const p_ptr = b.alloca(point_ty);
const y_ptr = b.structGepTyped(p_ptr, 1, ptr_s64, point_ty);
const c42 = b.constInt(42, .s64);
b.store(y_ptr, c42);
const loaded = b.load(y_ptr, .s64);
b.ret(loaded, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_gep", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "getelementptr") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "store") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: enum_init and enum_tag (plain enum)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
// Create a plain enum type: Color { Red, Green, Blue }
const variants = &[_]types.StringId{
str(&module, "Red"),
str(&module, "Green"),
str(&module, "Blue"),
};
const owned_variants = alloc.dupe(types.StringId, variants) catch unreachable;
defer alloc.free(owned_variants);
const color_ty = module.types.intern(.{ .@"enum" = .{
.name = str(&module, "Color"),
.variants = owned_variants,
} });
var b = Builder.init(&module);
// func f() -> s64 { c = Color.Green; return tag(c); }
_ = b.beginFunction(str(&module, "enumfn"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const green = b.enumInit(1, Ref.none, color_ty); // Green = tag 1
const tag = b.enumTag(green, .s32);
// Widen tag from s32 to s64 for the return
const wide = b.widen(tag, .s32, .s64);
b.ret(wide, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_enum", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// Plain enum is just an integer constant
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: tagged union (enum_init with payload, enum_tag, enum_payload)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
// Create a tagged union: Shape { Circle: f64, Rect: s64 }
const ufields = &[_]types.TypeInfo.StructInfo.Field{
.{ .name = str(&module, "Circle"), .ty = .f64 },
.{ .name = str(&module, "Rect"), .ty = .s64 },
};
const owned_ufields = alloc.dupe(types.TypeInfo.StructInfo.Field, ufields) catch unreachable;
defer alloc.free(owned_ufields);
const shape_ty = module.types.intern(.{ .tagged_union = .{
.name = str(&module, "Shape"),
.fields = owned_ufields,
.tag_type = .s64,
} });
var b = Builder.init(&module);
// 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 = 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
const payload = b.emit(.{ .enum_payload = .{ .base = shape, .field_index = 0 } }, .f64);
b.ret(payload, .f64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_union", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// 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);
}
test "emit: union_get (reinterpret union field)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
// Untagged union: Data { as_int: s64, as_float: f64 }
const ufields = &[_]types.TypeInfo.StructInfo.Field{
.{ .name = str(&module, "as_int"), .ty = .s64 },
.{ .name = str(&module, "as_float"), .ty = .f64 },
};
const owned_ufields = alloc.dupe(types.TypeInfo.StructInfo.Field, ufields) catch unreachable;
defer alloc.free(owned_ufields);
const data_ty = module.types.intern(.{ .@"union" = .{
.name = str(&module, "Data"),
.fields = owned_ufields,
} });
var b = Builder.init(&module);
// func f() -> s64 { d = Data.as_int(42); return union_get(d, 0) as s64; }
_ = b.beginFunction(str(&module, "ugfn"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const val = b.constInt(42, .s64);
const d = b.enumInit(0, val, data_ty);
const got = b.emit(.{ .union_get = .{ .base = d, .field_index = 0 } }, .s64);
b.ret(got, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_union_get", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// Should contain alloca + store + GEP + load pattern
try std.testing.expect(std.mem.indexOf(u8, ir_str, "alloca") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "load") != null);
}
// ── Array/Slice tests ───────────────────────────────────────────────
test "emit: array index_get" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const arr_ty = module.types.arrayOf(.s64, 3);
var b = Builder.init(&module);
// func f() -> s64 { arr: [3]s64 = ---; return arr[1]; }
_ = b.beginFunction(str(&module, "arr_idx"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const undef_arr = b.emit(.{ .const_undef = {} }, arr_ty);
const idx = b.constInt(1, .s64);
const elem = b.emit(.{ .index_get = .{ .lhs = undef_arr, .rhs = idx } }, .s64);
b.ret(elem, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_arr_idx", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "getelementptr") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: length on slice" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func f(s: string) -> s64 { return s.len; }
// 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);
const s = Ref.fromIndex(0);
const len = b.emit(.{ .length = .{ .operand = s } }, .s64);
b.ret(len, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_len", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: data_ptr on slice" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const ptr_ty = module.types.ptrTo(.u8);
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const ptr = b.emit(.{ .data_ptr = .{ .operand = s } }, ptr_ty);
b.ret(ptr, ptr_ty);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_dptr", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret ptr") != null);
}
test "emit: array_to_slice" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const arr_ty = module.types.arrayOf(.s64, 4);
const slice_ty = module.types.sliceOf(.s64);
var b = Builder.init(&module);
// func f() -> []s64 { var arr: [4]s64 = ---; return arr[:]; }
_ = b.beginFunction(str(&module, "a2s"), &.{}, slice_ty);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
const undef_arr = b.emit(.{ .const_undef = {} }, arr_ty);
const slice = b.emit(.{ .array_to_slice = .{ .operand = undef_arr } }, slice_ty);
b.ret(slice, slice_ty);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_a2s", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// Should have GEP for array decay + insertvalue for slice construction
try std.testing.expect(std.mem.indexOf(u8, ir_str, "getelementptr") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
}
test "emit: subslice" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const slice_ty = module.types.sliceOf(.u8);
var b = Builder.init(&module);
// 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 = 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();
var emitter = LLVMEmitter.init(alloc, &module, "test_subslice", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// Should have GEP for ptr+lo and sub for hi-lo
try std.testing.expect(std.mem.indexOf(u8, ir_str, "getelementptr") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "sub") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
}
// ── Optional tests ──────────────────────────────────────────────────
test "emit: optional_wrap and optional_unwrap (value type)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const opt_ty = module.types.optionalOf(.s64);
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const wrapped = b.optionalWrap(val, opt_ty);
const unwrapped = b.optionalUnwrap(wrapped, .s64);
b.ret(unwrapped, .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_opt", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// wrap = insertvalue, unwrap = extractvalue
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64") != null);
}
test "emit: optional_has_value" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const opt_ty = module.types.optionalOf(.s64);
var b = Builder.init(&module);
// 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 = Ref.fromIndex(0);
const wrapped = b.optionalWrap(val, opt_ty);
const has = b.optionalHasValue(wrapped);
b.ret(has, .bool);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_has", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i1") != null);
}
// ── Switch branch test ──────────────────────────────────────────────
test "emit: switch_br" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func f(x: s64) -> s64 { match x { 0 => 10, 1 => 20, _ => 30 } }
_ = b.beginFunction(str(&module, "swfn"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
const case0 = b.appendBlock(str(&module, "case0"), &.{});
const case1 = b.appendBlock(str(&module, "case1"), &.{});
const default_bb = b.appendBlock(str(&module, "default"), &.{});
b.switchToBlock(entry);
const x = b.constInt(1, .s64);
const cases = alloc.dupe(inst_mod.SwitchBranch.Case, &.{
.{ .value = 0, .target = case0, .args = &.{} },
.{ .value = 1, .target = case1, .args = &.{} },
}) catch unreachable;
defer alloc.free(cases);
b.emitVoid(.{ .switch_br = .{
.operand = x,
.cases = cases,
.default = default_bb,
.default_args = &.{},
} }, .void);
b.switchToBlock(case0);
b.ret(b.constInt(10, .s64), .s64);
b.switchToBlock(case1);
b.ret(b.constInt(20, .s64), .s64);
b.switchToBlock(default_bb);
b.ret(b.constInt(30, .s64), .s64);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_switch", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "switch") != null);
}
// ── Closure test ────────────────────────────────────────────────────
test "emit: closure_create" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
const closure_ty = module.types.closureType(&.{.s64}, .s64);
var b = Builder.init(&module);
// Create a dummy trampoline function
const tramp_id = b.beginFunction(str(&module, "tramp"), &[_]inst_mod.Function.Param{
.{ .name = str(&module, "env"), .ty = .s64 },
.{ .name = str(&module, "x"), .ty = .s64 },
}, .s64);
const tramp_entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(tramp_entry);
b.ret(b.constInt(0, .s64), .s64);
b.finalize();
// 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.fromIndex(0) } }, closure_ty);
b.ret(cl, closure_ty);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_closure", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
}
// ── Box/Unbox Any test ──────────────────────────────────────────────
test "emit: box_any and unbox_any" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// 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 = 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);
b.finalize();
var emitter = LLVMEmitter.init(alloc, &module, "test_any", .{});
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "insertvalue") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "extractvalue") != null);
}
test "emit: ERR E3.0 — DWARF debug info (compile unit + subprogram + per-inst location)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
// func main() -> s64 { return 42; } — with the `return` instruction
// carrying a span that lands on line 3 of the source map below.
_ = b.beginFunction(str(&module, "main"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
// "a\nb\nXYZ" — byte offset 4 ('X') is line 3, col 1.
b.current_span = .{ .start = 4, .end = 5 };
const c42 = b.constInt(42, .s64);
b.ret(c42, .s64);
b.finalize();
// Source map keyed on the main file. setDebugContext + opt none
// turns DWARF emission on (release opt levels skip it entirely).
var sources = std.StringHashMap([:0]const u8).init(alloc);
defer sources.deinit();
try sources.put("probe.sx", "a\nb\nXYZ");
var emitter = LLVMEmitter.init(alloc, &module, "test_dwarf", .{ .opt_level = .none });
defer emitter.deinit();
emitter.setDebugContext(&sources, "probe.sx");
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
// Module flags, compile unit on the main file, a subprogram for main,
// and the return instruction's location resolved to line 3.
try std.testing.expect(std.mem.indexOf(u8, ir_str, "\"Debug Info Version\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "\"Dwarf Version\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DICompileUnit") != null);
// Regression (issue 0058): a bare filename (no directory component) must
// still get a NON-EMPTY `directory:` — an empty `DW_AT_comp_dir` makes ld
// silently drop the whole debug map, so the binary becomes undebuggable.
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DIFile(filename: \"probe.sx\", directory: \".\")") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DISubprogram(name: \"main\"") != null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DILocation(line: 3") != null);
}
test "emit: ERR E3.0 — no DWARF without a debug context (unit-test default)" {
const alloc = std.testing.allocator;
var module = Module.init(alloc);
defer module.deinit();
var b = Builder.init(&module);
_ = b.beginFunction(str(&module, "main"), &.{}, .s64);
const entry = b.appendBlock(str(&module, "entry"), &.{});
b.switchToBlock(entry);
b.ret(b.constInt(42, .s64), .s64);
b.finalize();
// No setDebugContext call → no source map → debug info off even at
// opt none. Confirms the gate keeps the metadata out by default.
var emitter = LLVMEmitter.init(alloc, &module, "test_no_dwarf", .{ .opt_level = .none });
defer emitter.deinit();
emitter.emit();
try std.testing.expect(emitter.verify());
const ir_str = emitter.dumpToString();
try std.testing.expect(std.mem.indexOf(u8, ir_str, "DICompileUnit") == null);
try std.testing.expect(std.mem.indexOf(u8, ir_str, "!dbg") == null);
}