A source path with no directory component (`sx build main.sx` from the project dir — what the chess app does) made `diFileFor` emit a `DIFile` with an empty `directory:`, so the compile unit's `DW_AT_comp_dir` was "". Apple's ld then silently drops the *entire* object's debug map (0 N_OSO) and the binary is undebuggable — lldb resolves no sx source. Builds whose path had any directory (`.sx-tmp/x.sx`, `examples/x.sx`) were unaffected, which is why small repros + the stepping smoke passed and only the bundled chess app hit it. Fix: diFileFor falls back to "." (and "/" for a root-level file) when the path has no directory component, so comp_dir is never empty. Verified: chess (`sx build --target macos --emit-obj main.sx`) now links with OSO=1 and lldb resolves `frame at main.sx:82:8`. Regression guard added to the DWARF unit test (asserts `DIFile(... directory: ".")` for a bare filename). Gates: zig build, zig build test, run_examples.sh -> 291 passed, debug-stepping smoke ok.
1023 lines
36 KiB
Zig
1023 lines
36 KiB
Zig
// 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);
|
|
}
|
|
|
|
// ── 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);
|
|
}
|