05
This commit is contained in:
@@ -187,6 +187,8 @@ pub const CodeGen = struct {
|
||||
sema_result: ?*const sema.SemaResult = null,
|
||||
// Root declarations from the AST (for VM on-demand function compilation)
|
||||
root_decls: []const *Node = &.{},
|
||||
// Cached IR module for comptime evaluation (built once, reused)
|
||||
cached_ir_module: ?ir.Module = null,
|
||||
// Cached LLVM struct type for string slices {ptr, i32}
|
||||
string_struct_type: c.LLVMTypeRef = null,
|
||||
// Cached LLVM struct type for Any {i32 tag, i64 value}
|
||||
@@ -1541,22 +1543,25 @@ pub const CodeGen = struct {
|
||||
/// Try to evaluate a comptime expression using the IR interpreter.
|
||||
/// Returns null if the interpreter can't handle the expression (no diagnostics emitted).
|
||||
fn tryIrComptimeEval(self: *CodeGen, expr: *Node) ?comptime_mod.Value {
|
||||
// Build an IR module with all top-level decls lowered
|
||||
var module = ir.Module.init(self.allocator);
|
||||
var lowering = ir.Lowering.init(&module);
|
||||
|
||||
// Lower all root declarations so called functions are available
|
||||
lowering.lowerDecls(self.root_decls);
|
||||
// Build the IR module once (lowering all root decls), then reuse it
|
||||
if (self.cached_ir_module == null) {
|
||||
self.cached_ir_module = ir.Module.init(self.allocator);
|
||||
var lowering = ir.Lowering.init(&self.cached_ir_module.?);
|
||||
lowering.lowerDecls(self.root_decls);
|
||||
}
|
||||
|
||||
// Create a comptime function that wraps the expression
|
||||
var lowering = ir.Lowering.init(&self.cached_ir_module.?);
|
||||
const func_id = lowering.createComptimeFunction("ct_eval", expr, .s64);
|
||||
|
||||
// Interpret the comptime function
|
||||
var interp = ir.Interpreter.init(&module, self.allocator);
|
||||
var interp = ir.Interpreter.init(&self.cached_ir_module.?, self.allocator);
|
||||
const result = interp.call(func_id, &.{}) catch return null;
|
||||
|
||||
// Convert ir.Value → comptime_mod.Value
|
||||
return irValueToComptimeValue(result);
|
||||
// Convert ir.Value → comptime_mod.Value; return null if the
|
||||
// interpreter produced void (couldn't fully evaluate the expression).
|
||||
const cv = irValueToComptimeValue(result);
|
||||
return if (cv == .void_val) null else cv;
|
||||
}
|
||||
|
||||
/// Convert an IR interpreter value to a comptime module value.
|
||||
@@ -1570,7 +1575,7 @@ pub const CodeGen = struct {
|
||||
.null_val => .{ .null_val = {} },
|
||||
.aggregate => .{ .void_val = {} }, // TODO: struct/array conversion
|
||||
.undef => .{ .void_val = {} },
|
||||
.slot_ptr, .func_ref, .closure, .type_tag => .{ .void_val = {} },
|
||||
.slot_ptr, .func_ref, .closure, .type_tag, .heap_ptr => .{ .void_val = {} },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
17
src/core.zig
17
src/core.zig
@@ -25,6 +25,7 @@ pub const Compilation = struct {
|
||||
import_sources: std.StringHashMap([:0]const u8),
|
||||
sema_result: ?sema.SemaResult = null,
|
||||
cg: ?codegen.CodeGen = null,
|
||||
ir_emitter: ?ir.LLVMEmitter = null,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, io: std.Io, file_path: []const u8, source: [:0]const u8, target_config: TargetConfig) Compilation {
|
||||
return .{
|
||||
@@ -39,6 +40,7 @@ pub const Compilation = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Compilation) void {
|
||||
if (self.ir_emitter) |*e| e.deinit();
|
||||
if (self.cg) |*cg| cg.deinit();
|
||||
self.diagnostics.deinit();
|
||||
}
|
||||
@@ -106,6 +108,19 @@ pub const Compilation = struct {
|
||||
self.cg = cg;
|
||||
}
|
||||
|
||||
/// Generate code via the IR pipeline: lower AST → IR → LLVM.
|
||||
pub fn generateCodeViaIR(self: *Compilation) !void {
|
||||
// Heap-allocate the IR module so its address is stable during emit
|
||||
const ir_mod_ptr = try self.allocator.create(ir.Module);
|
||||
ir_mod_ptr.* = self.lowerToIR();
|
||||
var emitter = ir.LLVMEmitter.init(self.allocator, ir_mod_ptr, "sx_module", self.target_config);
|
||||
emitter.emit();
|
||||
// IR module is no longer needed after LLVM IR has been generated
|
||||
ir_mod_ptr.deinit();
|
||||
self.allocator.destroy(ir_mod_ptr);
|
||||
self.ir_emitter = emitter;
|
||||
}
|
||||
|
||||
/// Collect C import source info from the resolved AST.
|
||||
/// Called after generateCode() to compile C sources natively (not merged into LLVM module).
|
||||
pub fn collectCImportSources(self: *Compilation) ![]c_import.CImportInfo {
|
||||
@@ -118,6 +133,8 @@ pub const Compilation = struct {
|
||||
const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator);
|
||||
var module = ir.Module.init(self.allocator);
|
||||
var lowering = ir.Lowering.init(&module);
|
||||
lowering.main_file = self.file_path;
|
||||
lowering.resolved_root = root;
|
||||
lowering.lowerRoot(root);
|
||||
return module;
|
||||
}
|
||||
|
||||
931
src/ir/emit_llvm.test.zig
Normal file
931
src/ir/emit_llvm.test.zig
Normal file
@@ -0,0 +1,931 @@
|
||||
// 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);
|
||||
try std.testing.expect(std.mem.indexOf(u8, ir_str, "ret i64 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);
|
||||
|
||||
_ = b.beginFunction(str(&module, "fmath"), &.{}, .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 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);
|
||||
|
||||
_ = b.beginFunction(str(&module, "negate"), &.{}, .s64);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const val = b.constInt(7, .s64);
|
||||
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() -> s64 { if (10 < 20) return 1; else return 0; }
|
||||
_ = b.beginFunction(str(&module, "cmpfn"), &.{}, .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 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: heap_alloc and heap_free" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = Module.init(alloc);
|
||||
defer module.deinit();
|
||||
|
||||
var b = Builder.init(&module);
|
||||
|
||||
// func f() -> void { p = malloc(64); free(p); }
|
||||
_ = b.beginFunction(str(&module, "heapfn"), &.{}, .void);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const size = b.constInt(64, .s64);
|
||||
const ptr_ty = module.types.ptrTo(.void);
|
||||
const ptr = b.emit(.{ .heap_alloc = .{ .operand = size } }, ptr_ty);
|
||||
b.emit(.{ .heap_free = .{ .operand = ptr } }, .void);
|
||||
b.retVoid();
|
||||
b.finalize();
|
||||
|
||||
var emitter = LLVMEmitter.init(alloc, &module, "test_heap", .{});
|
||||
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, "malloc") != null);
|
||||
try std.testing.expect(std.mem.indexOf(u8, ir_str, "free") != 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);
|
||||
|
||||
_ = b.beginFunction(str(&module, "wfn"), &.{}, .s64);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const val = b.constInt(42, .s32);
|
||||
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() -> s64 { p = Point{10, 20}; return p.y; }
|
||||
_ = b.beginFunction(str(&module, "f"), &.{}, .s64);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const x = b.constInt(10, .s64);
|
||||
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.structGep(p_ptr, 1, ptr_s64);
|
||||
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);
|
||||
// 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(.{ .@"union" = .{
|
||||
.name = str(&module, "Shape"),
|
||||
.fields = owned_ufields,
|
||||
.tag_type = null,
|
||||
} });
|
||||
|
||||
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);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const radius = b.constFloat(3.14, .f64);
|
||||
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();
|
||||
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: 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,
|
||||
.tag_type = null,
|
||||
} });
|
||||
|
||||
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; }
|
||||
_ = b.beginFunction(str(&module, "strlen"), &.{}, .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 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() -> *u8 { s = "hello"; return s.ptr; }
|
||||
_ = b.beginFunction(str(&module, "dptr"), &.{}, ptr_ty);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const s = b.constString(str(&module, "test"));
|
||||
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() -> []u8 { s = "hello"; return s[1..3]; }
|
||||
_ = b.beginFunction(str(&module, "ssfn"), &.{}, 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 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() -> s64 { opt = wrap(42); return unwrap(opt); }
|
||||
_ = b.beginFunction(str(&module, "optfn"), &.{}, .s64);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const val = b.constInt(42, .s64);
|
||||
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);
|
||||
|
||||
_ = b.beginFunction(str(&module, "hasfn"), &.{}, .bool);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const val = b.constInt(10, .s64);
|
||||
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() -> closure { return closure_create(tramp, null); }
|
||||
_ = b.beginFunction(str(&module, "mkclose"), &.{}, 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);
|
||||
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() -> s64 { a = box(42); return unbox(a); }
|
||||
_ = b.beginFunction(str(&module, "anyfn"), &.{}, .s64);
|
||||
const entry = b.appendBlock(str(&module, "entry"), &.{});
|
||||
b.switchToBlock(entry);
|
||||
|
||||
const val = b.constInt(42, .s64);
|
||||
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);
|
||||
}
|
||||
2145
src/ir/emit_llvm.zig
Normal file
2145
src/ir/emit_llvm.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -200,6 +200,10 @@ pub const Op = union(enum) {
|
||||
box_any: BoxAny, // T → Any (erase type)
|
||||
unbox_any: UnaryOp, // Any → T (restore type)
|
||||
|
||||
// ── Reflection ─────────────────────────────────────────────────
|
||||
field_name_get: FieldReflect, // field_name(T, i) → string (runtime index)
|
||||
field_value_get: FieldReflect, // field_value(s, i) → Any (runtime struct + index)
|
||||
|
||||
// ── Terminators ─────────────────────────────────────────────────
|
||||
br: Branch,
|
||||
cond_br: CondBranch,
|
||||
@@ -277,7 +281,6 @@ pub const BuiltinCall = struct {
|
||||
};
|
||||
|
||||
pub const BuiltinId = enum(u16) {
|
||||
print,
|
||||
out,
|
||||
sqrt,
|
||||
size_of,
|
||||
@@ -286,6 +289,9 @@ pub const BuiltinId = enum(u16) {
|
||||
free,
|
||||
memcpy,
|
||||
memset,
|
||||
type_of,
|
||||
alloc,
|
||||
dealloc,
|
||||
};
|
||||
|
||||
pub const ProtocolCall = struct {
|
||||
@@ -324,6 +330,12 @@ pub const BoxAny = struct {
|
||||
source_type: TypeId,
|
||||
};
|
||||
|
||||
pub const FieldReflect = struct {
|
||||
base: Ref, // struct value (for field_value_get) or Ref.none (for field_name_get)
|
||||
index: Ref, // runtime field index
|
||||
struct_type: TypeId, // compile-time resolved struct type
|
||||
};
|
||||
|
||||
pub const Branch = struct {
|
||||
target: BlockId,
|
||||
args: []const Ref, // block param values
|
||||
@@ -356,6 +368,7 @@ pub const Block = struct {
|
||||
name: StringId,
|
||||
params: []const TypeId, // block parameter types (SSA phi alternative)
|
||||
insts: std.ArrayList(Inst),
|
||||
first_ref: u32 = 0, // ref index of the first instruction in this block
|
||||
|
||||
pub fn init(name: StringId, params: []const TypeId) Block {
|
||||
return .{
|
||||
|
||||
@@ -32,12 +32,19 @@ pub const Value = union(enum) {
|
||||
func_ref: FuncId,
|
||||
closure: ClosureVal,
|
||||
type_tag: TypeId,
|
||||
heap_ptr: HeapPtr, // pointer into heap-allocated memory
|
||||
|
||||
pub const ClosureVal = struct {
|
||||
func: FuncId,
|
||||
env: ?[]const Value,
|
||||
};
|
||||
|
||||
/// A pointer to heap-allocated memory, with an optional byte offset.
|
||||
pub const HeapPtr = struct {
|
||||
id: u32, // index into Interpreter.heap
|
||||
offset: u32 = 0,
|
||||
};
|
||||
|
||||
pub fn asInt(self: Value) ?i64 {
|
||||
return switch (self) {
|
||||
.int => |v| v,
|
||||
@@ -63,6 +70,29 @@ pub const Value = union(enum) {
|
||||
pub fn isNull(self: Value) bool {
|
||||
return self == .null_val;
|
||||
}
|
||||
|
||||
/// Get the string content, whether from a literal or a heap-backed string aggregate.
|
||||
pub fn asString(self: Value, interp: *const Interpreter) ?[]const u8 {
|
||||
return switch (self) {
|
||||
.string => |s| s,
|
||||
.aggregate => |fields| {
|
||||
// String fat pointer: { heap_ptr/string, int(len) }
|
||||
if (fields.len == 2) {
|
||||
const len: usize = @intCast(fields[1].asInt() orelse return null);
|
||||
switch (fields[0]) {
|
||||
.heap_ptr => |hp| {
|
||||
const mem = interp.heapSlice(hp) orelse return null;
|
||||
return if (len <= mem.len) mem[0..len] else null;
|
||||
},
|
||||
.string => |s| return if (len <= s.len) s[0..len] else s,
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// ── Error ───────────────────────────────────────────────────────────────
|
||||
@@ -85,16 +115,93 @@ pub const Interpreter = struct {
|
||||
call_depth: u32 = 0,
|
||||
max_call_depth: u32 = 256,
|
||||
|
||||
// Heap: dynamically allocated memory blocks
|
||||
heap: std.ArrayList([]u8),
|
||||
|
||||
// Global values: evaluated comptime globals, indexed by GlobalId
|
||||
global_values: std.AutoHashMap(u32, Value),
|
||||
|
||||
pub fn init(module: *const Module, alloc: Allocator) Interpreter {
|
||||
return .{
|
||||
.module = module,
|
||||
.alloc = alloc,
|
||||
.output = std.ArrayList(u8).empty,
|
||||
.heap = std.ArrayList([]u8).empty,
|
||||
.global_values = std.AutoHashMap(u32, Value).init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Interpreter) void {
|
||||
// Free all heap allocations
|
||||
for (self.heap.items) |block| {
|
||||
self.alloc.free(block);
|
||||
}
|
||||
self.heap.deinit(self.alloc);
|
||||
self.output.deinit(self.alloc);
|
||||
self.global_values.deinit();
|
||||
}
|
||||
|
||||
// ── Heap operations ────────────────────────────────────────────
|
||||
|
||||
fn heapAlloc(self: *Interpreter, size: usize) Value.HeapPtr {
|
||||
const mem = self.alloc.alloc(u8, size) catch unreachable;
|
||||
@memset(mem, 0);
|
||||
const id: u32 = @intCast(self.heap.items.len);
|
||||
self.heap.append(self.alloc, mem) catch unreachable;
|
||||
return .{ .id = id };
|
||||
}
|
||||
|
||||
fn heapFree(self: *Interpreter, hp: Value.HeapPtr) void {
|
||||
if (hp.id < self.heap.items.len) {
|
||||
self.alloc.free(self.heap.items[hp.id]);
|
||||
self.heap.items[hp.id] = &.{};
|
||||
}
|
||||
}
|
||||
|
||||
fn heapSlice(self: *const Interpreter, hp: Value.HeapPtr) ?[]u8 {
|
||||
if (hp.id >= self.heap.items.len) return null;
|
||||
const mem = self.heap.items[hp.id];
|
||||
if (hp.offset >= mem.len) return null;
|
||||
return mem[hp.offset..];
|
||||
}
|
||||
|
||||
fn heapMemcpy(self: *Interpreter, dst: Value.HeapPtr, src_bytes: []const u8, len: usize) void {
|
||||
const dst_mem = self.heapSlice(dst) orelse return;
|
||||
const copy_len = @min(len, @min(dst_mem.len, src_bytes.len));
|
||||
@memcpy(dst_mem[0..copy_len], src_bytes[0..copy_len]);
|
||||
}
|
||||
|
||||
fn heapMemset(self: *Interpreter, dst: Value.HeapPtr, val: u8, len: usize) void {
|
||||
const dst_mem = self.heapSlice(dst) orelse return;
|
||||
const set_len = @min(len, dst_mem.len);
|
||||
@memset(dst_mem[0..set_len], val);
|
||||
}
|
||||
|
||||
fn heapStoreByte(self: *Interpreter, dst: Value.HeapPtr, val: u8) void {
|
||||
const mem = self.heapSlice(dst) orelse return;
|
||||
if (mem.len > 0) mem[0] = val;
|
||||
}
|
||||
|
||||
/// Look up a global value, lazy-evaluating its comptime_func if needed.
|
||||
fn getGlobal(self: *Interpreter, gid: inst_mod.GlobalId) InterpError!Value {
|
||||
const idx = gid.index();
|
||||
// Check cache first
|
||||
if (self.global_values.get(idx)) |v| return v;
|
||||
|
||||
// Not cached — evaluate from global definition
|
||||
const global = &self.module.globals.items[idx];
|
||||
if (global.comptime_func) |func_id| {
|
||||
const result = try self.call(func_id, &.{});
|
||||
self.global_values.put(idx, result) catch {};
|
||||
return result;
|
||||
}
|
||||
// Static init value
|
||||
if (global.init_val) |iv| {
|
||||
const val: Value = self.constToValue(iv);
|
||||
self.global_values.put(idx, val) catch {};
|
||||
return val;
|
||||
}
|
||||
return .undef;
|
||||
}
|
||||
|
||||
pub fn call(self: *Interpreter, func_id: FuncId, args: []const Value) InterpError!Value {
|
||||
@@ -103,15 +210,22 @@ pub const Interpreter = struct {
|
||||
defer self.call_depth -= 1;
|
||||
|
||||
const func = self.module.getFunction(func_id);
|
||||
if (func.is_extern) return error.CannotEvalComptime;
|
||||
if (func.blocks.items.len == 0) return error.CannotEvalComptime;
|
||||
if (func.is_extern or func.blocks.items.len == 0) {
|
||||
return error.CannotEvalComptime;
|
||||
}
|
||||
|
||||
var frame = Frame.init(self.alloc);
|
||||
// Compute total refs: params + all instructions across all blocks
|
||||
var total_refs: u32 = @intCast(func.params.len);
|
||||
for (func.blocks.items) |blk| {
|
||||
total_refs += @intCast(blk.insts.items.len);
|
||||
}
|
||||
|
||||
var frame = Frame.initSized(self.alloc, total_refs);
|
||||
defer frame.deinit();
|
||||
|
||||
// Bind parameters as initial refs
|
||||
for (args) |arg| {
|
||||
frame.pushRef(self.alloc, arg);
|
||||
// Bind parameters as initial refs (indices 0..N-1)
|
||||
for (args, 0..) |arg, i| {
|
||||
frame.setRef(@intCast(i), arg);
|
||||
}
|
||||
|
||||
// Start at the entry block (index 0)
|
||||
@@ -119,18 +233,40 @@ pub const Interpreter = struct {
|
||||
var block_args: []const Value = &.{};
|
||||
|
||||
while (true) {
|
||||
const block = &func.blocks.items[current_block.index()];
|
||||
const block_idx = current_block.index();
|
||||
const block = &func.blocks.items[block_idx];
|
||||
var ref_counter: u32 = block.first_ref;
|
||||
|
||||
// Bind block params
|
||||
for (block_args) |arg| {
|
||||
frame.pushRef(self.alloc, arg);
|
||||
// Bind block params (block_param instructions handle this, but we
|
||||
// also need to pre-set the values for them)
|
||||
for (block_args) |_| {
|
||||
// block_param instructions will read from frame refs when executed
|
||||
// The block_param instruction itself produces the value
|
||||
}
|
||||
|
||||
for (block.insts.items) |*instruction| {
|
||||
const result = try self.execInst(instruction, &frame, ¤t_block, &block_args);
|
||||
// Special handling for block_param: bind the arg value
|
||||
if (instruction.op == .block_param) {
|
||||
const bp = instruction.op.block_param;
|
||||
if (bp.param_index < block_args.len) {
|
||||
frame.setRef(ref_counter, block_args[bp.param_index]);
|
||||
}
|
||||
ref_counter += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = self.execInst(instruction, &frame, ¤t_block, &block_args) catch |err| {
|
||||
return err;
|
||||
};
|
||||
switch (result) {
|
||||
.value => |val| frame.pushRef(self.alloc, val),
|
||||
.branch => break, // current_block and block_args updated by execInst
|
||||
.value => |val| {
|
||||
frame.setRef(ref_counter, val);
|
||||
ref_counter += 1;
|
||||
},
|
||||
.branch => {
|
||||
ref_counter += 1; // terminator consumes a ref slot
|
||||
break;
|
||||
},
|
||||
.ret_val => |val| return val,
|
||||
.ret_nothing => return .void_val,
|
||||
}
|
||||
@@ -229,7 +365,14 @@ pub const Interpreter = struct {
|
||||
.load => |u| {
|
||||
const ptr = frame.getRef(u.operand);
|
||||
switch (ptr) {
|
||||
.slot_ptr => |slot| return .{ .value = frame.loadSlot(slot) },
|
||||
.slot_ptr => |slot| {
|
||||
const slot_val = frame.loadSlot(slot);
|
||||
// Check if this is a field pointer (from struct_gep)
|
||||
if (self.resolveFieldLoad(frame, slot_val)) |field_val| {
|
||||
return .{ .value = field_val };
|
||||
}
|
||||
return .{ .value = slot_val };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
@@ -237,7 +380,20 @@ pub const Interpreter = struct {
|
||||
const ptr = frame.getRef(s.ptr);
|
||||
const val = frame.getRef(s.val);
|
||||
switch (ptr) {
|
||||
.slot_ptr => |slot| frame.storeSlot(slot, val),
|
||||
.slot_ptr => |slot| {
|
||||
const slot_val = frame.loadSlot(slot);
|
||||
// Check if this is a field pointer (from struct_gep)
|
||||
if (self.resolveFieldStore(frame, slot_val, val)) {
|
||||
// Field store handled
|
||||
} else {
|
||||
frame.storeSlot(slot, val);
|
||||
}
|
||||
},
|
||||
.heap_ptr => |hp| {
|
||||
// Store a byte into heap memory (from index_gep on string)
|
||||
const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF);
|
||||
self.heapStoreByte(hp, byte);
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
return .{ .value = .void_val };
|
||||
@@ -252,12 +408,32 @@ pub const Interpreter = struct {
|
||||
return .{ .value = .{ .aggregate = fields } };
|
||||
},
|
||||
.struct_get => |fa| {
|
||||
const base = frame.getRef(fa.base);
|
||||
var base = frame.getRef(fa.base);
|
||||
// Auto-deref slot_ptr → load the value
|
||||
if (base == .slot_ptr) {
|
||||
const loaded = frame.loadSlot(base.slot_ptr);
|
||||
if (self.resolveFieldLoad(frame, loaded)) |resolved| {
|
||||
base = resolved;
|
||||
} else {
|
||||
base = loaded;
|
||||
}
|
||||
}
|
||||
switch (base) {
|
||||
.aggregate => |fields| {
|
||||
if (fa.field_index >= fields.len) return error.OutOfBounds;
|
||||
return .{ .value = fields[fa.field_index] };
|
||||
},
|
||||
.string => |s| {
|
||||
// String as fat pointer: field 0 = ptr (string), field 1 = len
|
||||
if (fa.field_index == 0) return .{ .value = .{ .string = s } };
|
||||
if (fa.field_index == 1) return .{ .value = .{ .int = @intCast(s.len) } };
|
||||
return error.OutOfBounds;
|
||||
},
|
||||
.int => |v| {
|
||||
// Scalar boxed as "struct" — field 0 is the value itself
|
||||
if (fa.field_index == 0) return .{ .value = .{ .int = v } };
|
||||
return error.OutOfBounds;
|
||||
},
|
||||
else => return error.TypeError,
|
||||
}
|
||||
},
|
||||
@@ -392,8 +568,360 @@ pub const Interpreter = struct {
|
||||
.ret_void => return .ret_nothing,
|
||||
.@"unreachable" => return error.Unreachable,
|
||||
|
||||
// ── Not evaluable at comptime ───────────────────────
|
||||
.heap_alloc, .heap_free, .call_indirect, .call_closure, .call_builtin, .protocol_call_dynamic, .protocol_erase, .closure_create, .context_load, .context_store, .context_save, .context_restore, .global_get, .global_set, .box_any, .unbox_any, .struct_gep, .union_get, .union_gep, .index_get, .index_gep, .length, .data_ptr, .subslice, .array_to_slice, .tuple_init, .tuple_get, .addr_of, .deref, .vec_splat, .vec_extract, .vec_insert, .bit_and, .bit_or, .bit_xor, .bit_not, .shl, .shr, .placeholder => {
|
||||
// ── Heap operations ─────────────────────────────────
|
||||
.heap_alloc => |u| {
|
||||
const size_val = frame.getRef(u.operand);
|
||||
const size: usize = @intCast(size_val.asInt() orelse return error.TypeError);
|
||||
const hp = self.heapAlloc(size);
|
||||
return .{ .value = .{ .heap_ptr = hp } };
|
||||
},
|
||||
.heap_free => |u| {
|
||||
const ptr = frame.getRef(u.operand);
|
||||
switch (ptr) {
|
||||
.heap_ptr => |hp| self.heapFree(hp),
|
||||
else => {},
|
||||
}
|
||||
return .{ .value = .void_val };
|
||||
},
|
||||
|
||||
// ── Builtin calls ──────────────────────────────────
|
||||
.call_builtin => |bi| {
|
||||
return self.execBuiltin(bi, frame, instruction.ty);
|
||||
},
|
||||
|
||||
// ── Struct GEP (field pointer) ─────────────────────
|
||||
.struct_gep => |fa| {
|
||||
const base = frame.getRef(fa.base);
|
||||
switch (base) {
|
||||
.slot_ptr => |slot| {
|
||||
// Create a field-pointer: we encode as a slot_ptr with field info
|
||||
// When loading, we extract the field; when storing, we modify the field
|
||||
const field_slot = frame.allocSlot(self.alloc);
|
||||
// Store a field reference: { parent_slot, field_index }
|
||||
const field_ref = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
||||
field_ref[0] = .{ .int = @intCast(slot) };
|
||||
field_ref[1] = .{ .int = @intCast(fa.field_index) };
|
||||
frame.storeSlot(field_slot, .{ .aggregate = field_ref });
|
||||
return .{ .value = .{ .slot_ptr = field_slot } };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
|
||||
// ── String/slice operations ────────────────────────
|
||||
.index_get => |idx| {
|
||||
const base = frame.getRef(idx.lhs);
|
||||
const index_val = frame.getRef(idx.rhs);
|
||||
const i: usize = @intCast(index_val.asInt() orelse return error.TypeError);
|
||||
// Try as string value
|
||||
if (base.asString(self)) |s| {
|
||||
if (i >= s.len) return error.OutOfBounds;
|
||||
return .{ .value = .{ .int = s[i] } };
|
||||
}
|
||||
// Try as aggregate array or slice
|
||||
switch (base) {
|
||||
.aggregate => |fields| {
|
||||
// Check for slice-like: {data_ptr, len} where data_ptr is slot_ptr
|
||||
if (fields.len == 2 and fields[1] == .int) {
|
||||
const data = fields[0];
|
||||
if (data == .slot_ptr) {
|
||||
// The data field is a ptr — resolve through slots to get the array
|
||||
const arr = self.resolveSlotChain(frame, data);
|
||||
switch (arr) {
|
||||
.aggregate => |arr_fields| {
|
||||
if (i < arr_fields.len) return .{ .value = arr_fields[i] };
|
||||
return error.OutOfBounds;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
} else if (data == .aggregate) {
|
||||
// Inline array data
|
||||
const arr_fields = data.aggregate;
|
||||
if (i < arr_fields.len) return .{ .value = arr_fields[i] };
|
||||
return error.OutOfBounds;
|
||||
}
|
||||
}
|
||||
// Plain aggregate indexing
|
||||
if (i >= fields.len) return error.OutOfBounds;
|
||||
return .{ .value = fields[i] };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
.length => |u| {
|
||||
const val = frame.getRef(u.operand);
|
||||
if (val.asString(self)) |s| {
|
||||
return .{ .value = .{ .int = @intCast(s.len) } };
|
||||
}
|
||||
switch (val) {
|
||||
.aggregate => |fields| {
|
||||
// For fat pointers {ptr, len}, len is field[1]
|
||||
if (fields.len == 2) {
|
||||
return .{ .value = fields[1] };
|
||||
}
|
||||
return .{ .value = .{ .int = @intCast(fields.len) } };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
.data_ptr => |u| {
|
||||
const val = frame.getRef(u.operand);
|
||||
switch (val) {
|
||||
.aggregate => |fields| {
|
||||
if (fields.len >= 1) return .{ .value = fields[0] };
|
||||
return error.OutOfBounds;
|
||||
},
|
||||
.string => return .{ .value = val },
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
.subslice => |sub| {
|
||||
const base = frame.getRef(sub.base);
|
||||
const lo_val = frame.getRef(sub.lo);
|
||||
const hi_val = frame.getRef(sub.hi);
|
||||
const lo: usize = @intCast(lo_val.asInt() orelse return error.TypeError);
|
||||
const hi: usize = @intCast(hi_val.asInt() orelse return error.TypeError);
|
||||
if (base.asString(self)) |s| {
|
||||
if (hi > s.len) return error.OutOfBounds;
|
||||
return .{ .value = .{ .string = s[lo..hi] } };
|
||||
}
|
||||
return error.CannotEvalComptime;
|
||||
},
|
||||
|
||||
// ── Addr/deref ─────────────────────────────────────
|
||||
.addr_of => |u| {
|
||||
const val = frame.getRef(u.operand);
|
||||
return .{ .value = val }; // pass through pointer-like values
|
||||
},
|
||||
.deref => |u| {
|
||||
const val = frame.getRef(u.operand);
|
||||
switch (val) {
|
||||
.slot_ptr => |slot| return .{ .value = frame.loadSlot(slot) },
|
||||
else => return .{ .value = val },
|
||||
}
|
||||
},
|
||||
|
||||
// ── Bitwise operations ─────────────────────────────
|
||||
.bit_and => |b| {
|
||||
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
||||
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
||||
return .{ .value = .{ .int = lhs & rhs } };
|
||||
},
|
||||
.bit_or => |b| {
|
||||
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
||||
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
||||
return .{ .value = .{ .int = lhs | rhs } };
|
||||
},
|
||||
.bit_xor => |b| {
|
||||
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
||||
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
||||
return .{ .value = .{ .int = lhs ^ rhs } };
|
||||
},
|
||||
.bit_not => |u| {
|
||||
const val = frame.getRef(u.operand).asInt() orelse return error.TypeError;
|
||||
return .{ .value = .{ .int = ~val } };
|
||||
},
|
||||
.shl => |b| {
|
||||
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
||||
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
||||
const shift: u6 = @intCast(@min(rhs, 63));
|
||||
return .{ .value = .{ .int = lhs << shift } };
|
||||
},
|
||||
.shr => |b| {
|
||||
const lhs = frame.getRef(b.lhs).asInt() orelse return error.TypeError;
|
||||
const rhs = frame.getRef(b.rhs).asInt() orelse return error.TypeError;
|
||||
const shift: u6 = @intCast(@min(rhs, 63));
|
||||
return .{ .value = .{ .int = lhs >> shift } };
|
||||
},
|
||||
|
||||
// ── Tuple ops (same as struct) ─────────────────────
|
||||
.tuple_init => |agg| {
|
||||
const fields = self.alloc.alloc(Value, agg.fields.len) catch return error.CannotEvalComptime;
|
||||
for (agg.fields, 0..) |ref, i| {
|
||||
fields[i] = frame.getRef(ref);
|
||||
}
|
||||
return .{ .value = .{ .aggregate = fields } };
|
||||
},
|
||||
.tuple_get => |fa| {
|
||||
const base = frame.getRef(fa.base);
|
||||
switch (base) {
|
||||
.aggregate => |fields| {
|
||||
if (fa.field_index >= fields.len) return error.OutOfBounds;
|
||||
return .{ .value = fields[fa.field_index] };
|
||||
},
|
||||
else => return error.TypeError,
|
||||
}
|
||||
},
|
||||
|
||||
// ── Box/unbox (Any type) ───────────────────────────
|
||||
.box_any => |ba| {
|
||||
const val = frame.getRef(ba.operand);
|
||||
// Box as aggregate: { type_tag, value } — matches LLVM layout
|
||||
const fields = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
||||
fields[0] = .{ .int = @intFromEnum(ba.source_type) };
|
||||
fields[1] = val;
|
||||
return .{ .value = .{ .aggregate = fields } };
|
||||
},
|
||||
.unbox_any => |ua| {
|
||||
const val = frame.getRef(ua.operand);
|
||||
switch (val) {
|
||||
.aggregate => |fields| {
|
||||
// Value is at field 1 in { tag, value } layout
|
||||
if (fields.len >= 2) return .{ .value = fields[1] };
|
||||
if (fields.len >= 1) return .{ .value = fields[0] };
|
||||
return error.OutOfBounds;
|
||||
},
|
||||
else => return .{ .value = val },
|
||||
}
|
||||
},
|
||||
|
||||
// ── Reflection ─────────────────────────────────────
|
||||
.field_name_get => |fr| {
|
||||
const idx_val = frame.getRef(fr.index);
|
||||
const idx: usize = @intCast(switch (idx_val) {
|
||||
.int => |i| i,
|
||||
else => return error.CannotEvalComptime,
|
||||
});
|
||||
const info = self.module.types.get(fr.struct_type);
|
||||
const fields = switch (info) {
|
||||
.@"struct" => |s| s.fields,
|
||||
.@"union" => |u| u.fields,
|
||||
else => return error.CannotEvalComptime,
|
||||
};
|
||||
if (idx >= fields.len) return error.OutOfBounds;
|
||||
const name = self.module.types.getString(fields[idx].name);
|
||||
return .{ .value = .{ .string = name } };
|
||||
},
|
||||
.field_value_get => |fr| {
|
||||
const base_val = frame.getRef(fr.base);
|
||||
const idx_val = frame.getRef(fr.index);
|
||||
const idx: usize = @intCast(switch (idx_val) {
|
||||
.int => |i| i,
|
||||
else => return error.CannotEvalComptime,
|
||||
});
|
||||
switch (base_val) {
|
||||
.aggregate => |agg| {
|
||||
if (idx >= agg.len) return error.OutOfBounds;
|
||||
// Box as Any: { value, type_tag }
|
||||
const info = self.module.types.get(fr.struct_type);
|
||||
const fields = switch (info) {
|
||||
.@"struct" => |s| s.fields,
|
||||
.@"union" => |u| u.fields,
|
||||
else => return error.CannotEvalComptime,
|
||||
};
|
||||
const field_ty_tag: i64 = if (idx < fields.len) @intFromEnum(fields[idx].ty) else 0;
|
||||
const boxed = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
||||
boxed[0] = agg[idx];
|
||||
boxed[1] = .{ .int = field_ty_tag };
|
||||
return .{ .value = .{ .aggregate = boxed } };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
|
||||
// ── Global access ──────────────────────────────────
|
||||
.global_get => |gid| {
|
||||
const val = try self.getGlobal(gid);
|
||||
return .{ .value = val };
|
||||
},
|
||||
.global_set => |gs| {
|
||||
const val = frame.getRef(gs.value);
|
||||
self.global_values.put(gs.global.index(), val) catch {};
|
||||
return .{ .value = .void_val };
|
||||
},
|
||||
|
||||
// ── Index GEP (array element pointer) ─────────────
|
||||
.index_gep => |b| {
|
||||
const base = frame.getRef(b.lhs);
|
||||
const idx = frame.getRef(b.rhs);
|
||||
switch (base) {
|
||||
.slot_ptr => |slot| {
|
||||
// Create an indexed element pointer: { parent_slot, index, is_index_gep=1 }
|
||||
const field_slot = frame.allocSlot(self.alloc);
|
||||
const ref = self.alloc.alloc(Value, 3) catch return error.CannotEvalComptime;
|
||||
ref[0] = .{ .int = @intCast(slot) };
|
||||
ref[1] = idx;
|
||||
ref[2] = .{ .int = 1 }; // marker: this is index_gep, not struct_gep
|
||||
frame.storeSlot(field_slot, .{ .aggregate = ref });
|
||||
return .{ .value = .{ .slot_ptr = field_slot } };
|
||||
},
|
||||
.aggregate => |fields| {
|
||||
// String/slice aggregate {data_ptr, len} — compute data_ptr + index
|
||||
if (fields.len >= 2) {
|
||||
const data_ptr = fields[0];
|
||||
const offset = idx.asInt() orelse return error.TypeError;
|
||||
switch (data_ptr) {
|
||||
.heap_ptr => |hp| {
|
||||
return .{ .value = .{ .heap_ptr = .{
|
||||
.id = hp.id,
|
||||
.offset = hp.offset + @as(u32, @intCast(offset)),
|
||||
} } };
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return error.CannotEvalComptime;
|
||||
},
|
||||
.string => |s| {
|
||||
// String literal — copy to heap and return heap_ptr at offset
|
||||
const offset: usize = @intCast(@as(u64, @bitCast(idx.asInt() orelse return error.TypeError)));
|
||||
const hp = self.heapAlloc(s.len);
|
||||
self.heapMemcpy(hp, s, s.len);
|
||||
return .{ .value = .{ .heap_ptr = .{
|
||||
.id = hp.id,
|
||||
.offset = @intCast(offset),
|
||||
} } };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
|
||||
// ── Array to slice ────────────────────────────────
|
||||
.array_to_slice => |u| {
|
||||
const val = frame.getRef(u.operand);
|
||||
switch (val) {
|
||||
.aggregate => |fields| {
|
||||
// Convert array aggregate to slice: { aggregate_ref, len }
|
||||
const slice = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
||||
slice[0] = val; // the array data
|
||||
slice[1] = .{ .int = @intCast(fields.len) };
|
||||
return .{ .value = .{ .aggregate = slice } };
|
||||
},
|
||||
.slot_ptr => |slot| {
|
||||
const arr = frame.loadSlot(slot);
|
||||
switch (arr) {
|
||||
.aggregate => |fields| {
|
||||
const slice = self.alloc.alloc(Value, 2) catch return error.CannotEvalComptime;
|
||||
slice[0] = arr;
|
||||
slice[1] = .{ .int = @intCast(fields.len) };
|
||||
return .{ .value = .{ .aggregate = slice } };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
|
||||
// ── Call indirect (function pointer) ──────────────
|
||||
.call_indirect => |ci| {
|
||||
const callee = frame.getRef(ci.callee);
|
||||
switch (callee) {
|
||||
.func_ref => |fid| {
|
||||
const args = self.alloc.alloc(Value, ci.args.len) catch return error.CannotEvalComptime;
|
||||
defer self.alloc.free(args);
|
||||
for (ci.args, 0..) |ref, i| {
|
||||
args[i] = frame.getRef(ref);
|
||||
}
|
||||
const result = try self.call(fid, args);
|
||||
return .{ .value = result };
|
||||
},
|
||||
else => return error.CannotEvalComptime,
|
||||
}
|
||||
},
|
||||
|
||||
// ── Not yet evaluable at comptime ──────────────────
|
||||
.call_closure, .protocol_call_dynamic, .protocol_erase, .closure_create, .context_load, .context_store, .context_save, .context_restore, .union_get, .union_gep, .vec_splat, .vec_extract, .vec_insert, .placeholder => {
|
||||
return error.CannotEvalComptime;
|
||||
},
|
||||
}
|
||||
@@ -487,38 +1015,241 @@ pub const Interpreter = struct {
|
||||
|
||||
return error.TypeError;
|
||||
}
|
||||
|
||||
// ── Slot chain resolution ────────────────────────────────────
|
||||
|
||||
/// Follow a slot_ptr through field-pointer / index-gep chains
|
||||
/// to get the underlying value. Handles nested dereferences.
|
||||
fn resolveSlotChain(self: *Interpreter, frame: *Frame, val: Value) Value {
|
||||
_ = self;
|
||||
var current = val;
|
||||
var depth: u32 = 0;
|
||||
while (depth < 16) : (depth += 1) {
|
||||
switch (current) {
|
||||
.slot_ptr => |slot| {
|
||||
const stored = frame.loadSlot(slot);
|
||||
switch (stored) {
|
||||
.aggregate => |ref_fields| {
|
||||
if (ref_fields.len >= 2) {
|
||||
// Field-pointer or index-gep reference: {parent_slot, index, [marker]}
|
||||
const parent_slot_val = ref_fields[0].asInt() orelse return stored;
|
||||
const parent_slot: u32 = @intCast(parent_slot_val);
|
||||
const parent = frame.loadSlot(parent_slot);
|
||||
return parent; // Return the parent array/struct
|
||||
}
|
||||
return stored;
|
||||
},
|
||||
.slot_ptr => {
|
||||
current = stored;
|
||||
continue;
|
||||
},
|
||||
else => return stored,
|
||||
}
|
||||
},
|
||||
else => return current,
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
// ── Constant → Value conversion ─────────────────────────────
|
||||
|
||||
fn constToValue(self: *Interpreter, cv: inst_mod.ConstantValue) Value {
|
||||
return switch (cv) {
|
||||
.int => |v| .{ .int = v },
|
||||
.float => |v| .{ .float = v },
|
||||
.boolean => |v| .{ .boolean = v },
|
||||
.string => |sid| .{ .string = self.module.types.getString(sid) },
|
||||
.null_val => .null_val,
|
||||
.undef, .zeroinit => .undef,
|
||||
.aggregate => |items| {
|
||||
const fields = self.alloc.alloc(Value, items.len) catch return .undef;
|
||||
for (items, 0..) |item, i| {
|
||||
fields[i] = self.constToValue(item);
|
||||
}
|
||||
return .{ .aggregate = fields };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ── Field pointer helpers (for struct_gep load/store) ─────────
|
||||
|
||||
/// Check if a slot value is a field pointer { parent_slot, field_index [, is_index_gep] }.
|
||||
/// If so, load the parent aggregate and return the field value.
|
||||
fn resolveFieldLoad(self: *Interpreter, frame: *Frame, slot_val: Value) ?Value {
|
||||
_ = self;
|
||||
switch (slot_val) {
|
||||
.aggregate => |fields| {
|
||||
if (fields.len >= 2) {
|
||||
const parent_slot_val = fields[0].asInt() orelse return null;
|
||||
const field_idx_val = fields[1].asInt() orelse return null;
|
||||
const parent_slot: u32 = @intCast(parent_slot_val);
|
||||
const field_idx: usize = @intCast(field_idx_val);
|
||||
const parent = frame.loadSlot(parent_slot);
|
||||
switch (parent) {
|
||||
.aggregate => |parent_fields| {
|
||||
if (field_idx < parent_fields.len) return parent_fields[field_idx];
|
||||
},
|
||||
.string => |s| {
|
||||
// String fat pointer: field 0 = ptr (as string), field 1 = len
|
||||
if (field_idx == 0) return .{ .string = s };
|
||||
if (field_idx == 1) return .{ .int = @intCast(s.len) };
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Check if a slot value is a field pointer. If so, modify the field
|
||||
/// in the parent aggregate. Returns true if handled.
|
||||
fn resolveFieldStore(self: *Interpreter, frame: *Frame, slot_val: Value, new_val: Value) bool {
|
||||
switch (slot_val) {
|
||||
.aggregate => |fields| {
|
||||
if (fields.len >= 2) {
|
||||
const parent_slot_val = fields[0].asInt() orelse return false;
|
||||
const field_idx_val = fields[1].asInt() orelse return false;
|
||||
const parent_slot: u32 = @intCast(parent_slot_val);
|
||||
const field_idx: usize = @intCast(field_idx_val);
|
||||
const parent = frame.loadSlot(parent_slot);
|
||||
switch (parent) {
|
||||
.aggregate => |parent_fields| {
|
||||
if (field_idx < parent_fields.len) {
|
||||
// Clone the aggregate and update the field
|
||||
const new_fields = self.alloc.alloc(Value, parent_fields.len) catch return false;
|
||||
@memcpy(new_fields, parent_fields);
|
||||
new_fields[field_idx] = new_val;
|
||||
frame.storeSlot(parent_slot, .{ .aggregate = new_fields });
|
||||
return true;
|
||||
}
|
||||
},
|
||||
.undef => {
|
||||
// Initialize a new aggregate from undef
|
||||
const num_fields: usize = @max(field_idx + 1, 2); // at least 2 for strings
|
||||
const new_fields = self.alloc.alloc(Value, num_fields) catch return false;
|
||||
for (new_fields) |*f| f.* = .undef;
|
||||
new_fields[field_idx] = new_val;
|
||||
frame.storeSlot(parent_slot, .{ .aggregate = new_fields });
|
||||
return true;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Builtin call dispatch ──────────────────────────────────────
|
||||
|
||||
fn execBuiltin(self: *Interpreter, bi: inst_mod.BuiltinCall, frame: *Frame, _: TypeId) InterpError!ExecResult {
|
||||
switch (bi.builtin) {
|
||||
.malloc => {
|
||||
const size_val = frame.getRef(bi.args[0]);
|
||||
const size: usize = @intCast(size_val.asInt() orelse return error.TypeError);
|
||||
const hp = self.heapAlloc(size);
|
||||
return .{ .value = .{ .heap_ptr = hp } };
|
||||
},
|
||||
.free => {
|
||||
const ptr = frame.getRef(bi.args[0]);
|
||||
switch (ptr) {
|
||||
.heap_ptr => |hp| self.heapFree(hp),
|
||||
else => {},
|
||||
}
|
||||
return .{ .value = .void_val };
|
||||
},
|
||||
.memcpy => {
|
||||
const dst = frame.getRef(bi.args[0]);
|
||||
const src = frame.getRef(bi.args[1]);
|
||||
const len_val = frame.getRef(bi.args[2]);
|
||||
const len: usize = @intCast(len_val.asInt() orelse return error.TypeError);
|
||||
const dst_hp = switch (dst) {
|
||||
.heap_ptr => |hp| hp,
|
||||
else => return error.CannotEvalComptime,
|
||||
};
|
||||
// Get source bytes
|
||||
const src_bytes: []const u8 = switch (src) {
|
||||
.heap_ptr => |hp| self.heapSlice(hp) orelse return error.CannotEvalComptime,
|
||||
.string => |s| s,
|
||||
else => return error.CannotEvalComptime,
|
||||
};
|
||||
self.heapMemcpy(dst_hp, src_bytes, len);
|
||||
return .{ .value = .{ .heap_ptr = dst_hp } };
|
||||
},
|
||||
.memset => {
|
||||
const dst = frame.getRef(bi.args[0]);
|
||||
const val = frame.getRef(bi.args[1]);
|
||||
const len_val = frame.getRef(bi.args[2]);
|
||||
const byte: u8 = @intCast(@as(u64, @bitCast(val.asInt() orelse return error.TypeError)) & 0xFF);
|
||||
const len: usize = @intCast(len_val.asInt() orelse return error.TypeError);
|
||||
switch (dst) {
|
||||
.heap_ptr => |hp| self.heapMemset(hp, byte, len),
|
||||
else => {},
|
||||
}
|
||||
return .{ .value = .void_val };
|
||||
},
|
||||
.out => {
|
||||
const str_val = frame.getRef(bi.args[0]);
|
||||
if (str_val.asString(self)) |s| {
|
||||
self.output.appendSlice(self.alloc, s) catch {};
|
||||
}
|
||||
return .{ .value = .void_val };
|
||||
},
|
||||
.size_of => {
|
||||
// Return a default size (8 bytes for most types)
|
||||
return .{ .value = .{ .int = 8 } };
|
||||
},
|
||||
.sqrt => {
|
||||
const val = frame.getRef(bi.args[0]);
|
||||
const f = val.asFloat() orelse return error.TypeError;
|
||||
return .{ .value = .{ .float = @sqrt(f) } };
|
||||
},
|
||||
.cast, .type_of, .alloc, .dealloc => {
|
||||
return error.CannotEvalComptime;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
// ── Frame ───────────────────────────────────────────────────────────────
|
||||
// Holds SSA values (by Ref index) and local mutable slots (for alloca).
|
||||
|
||||
const Frame = struct {
|
||||
refs: std.ArrayList(Value),
|
||||
refs: []Value,
|
||||
ref_alloc: Allocator,
|
||||
slots: std.ArrayList(Value),
|
||||
|
||||
fn init(alloc: Allocator) Frame {
|
||||
_ = alloc;
|
||||
/// Create a frame pre-allocated with `num_refs` slots (all undef).
|
||||
fn initSized(alloc: Allocator, num_refs: u32) Frame {
|
||||
const refs = alloc.alloc(Value, num_refs) catch unreachable;
|
||||
@memset(refs, .undef);
|
||||
return .{
|
||||
.refs = std.ArrayList(Value).empty,
|
||||
.refs = refs,
|
||||
.ref_alloc = alloc,
|
||||
.slots = std.ArrayList(Value).empty,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(self: *Frame) void {
|
||||
// We use the interpreter's allocator for everything — it's an arena-like pattern.
|
||||
// Actual cleanup handled by the test allocator.
|
||||
_ = self;
|
||||
self.ref_alloc.free(self.refs);
|
||||
}
|
||||
|
||||
fn pushRef(self: *Frame, alloc: Allocator, val: Value) void {
|
||||
self.refs.append(alloc, val) catch unreachable;
|
||||
fn setRef(self: *Frame, idx: u32, val: Value) void {
|
||||
if (idx < self.refs.len) {
|
||||
self.refs[idx] = val;
|
||||
}
|
||||
}
|
||||
|
||||
fn getRef(self: *const Frame, ref: Ref) Value {
|
||||
if (ref.isNone()) return .void_val;
|
||||
const idx = ref.index();
|
||||
if (idx >= self.refs.items.len) return .undef;
|
||||
return self.refs.items[idx];
|
||||
if (idx >= self.refs.len) return .undef;
|
||||
return self.refs[idx];
|
||||
}
|
||||
|
||||
fn allocSlot(self: *Frame, alloc: Allocator) u32 {
|
||||
|
||||
@@ -31,6 +31,9 @@ pub const Interpreter = interp.Interpreter;
|
||||
pub const Value = interp.Value;
|
||||
pub const Lowering = lower.Lowering;
|
||||
|
||||
pub const emit_llvm = @import("emit_llvm.zig");
|
||||
pub const LLVMEmitter = emit_llvm.LLVMEmitter;
|
||||
|
||||
pub const type_bridge = @import("type_bridge.zig");
|
||||
pub const resolveAstType = type_bridge.resolveAstType;
|
||||
pub const bridgeType = type_bridge.bridgeType;
|
||||
@@ -42,6 +45,7 @@ pub const print_tests = @import("print.test.zig");
|
||||
pub const interp_tests = @import("interp.test.zig");
|
||||
pub const lower_tests = @import("lower.test.zig");
|
||||
pub const type_bridge_tests = @import("type_bridge.test.zig");
|
||||
pub const emit_llvm_tests = @import("emit_llvm.test.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
2355
src/ir/lower.zig
2355
src/ir/lower.zig
File diff suppressed because it is too large
Load Diff
@@ -130,14 +130,38 @@ pub const Builder = struct {
|
||||
// ── Function setup ──────────────────────────────────────────────
|
||||
|
||||
pub fn beginFunction(self: *Builder, name: StringId, params: []const Function.Param, ret_ty: TypeId) FuncId {
|
||||
// Check if there's an existing extern stub with this name — upgrade it in-place
|
||||
for (self.module.functions.items, 0..) |*existing, i| {
|
||||
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.ret = ret_ty;
|
||||
const id = FuncId.fromIndex(@intCast(i));
|
||||
self.func = id;
|
||||
self.inst_counter = @intCast(params.len);
|
||||
self.current_block = null;
|
||||
return id;
|
||||
}
|
||||
}
|
||||
const func = Function.init(name, params, ret_ty);
|
||||
const id = self.module.addFunction(func);
|
||||
self.func = id;
|
||||
self.inst_counter = 0;
|
||||
// Reserve refs 0..N-1 for function parameters; instructions start at ref N.
|
||||
self.inst_counter = @intCast(params.len);
|
||||
self.current_block = null;
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Declare an extern function (no body, external linkage).
|
||||
pub fn declareExtern(self: *Builder, name: StringId, params: []const Function.Param, ret_ty: TypeId) FuncId {
|
||||
var func = Function.init(name, params, ret_ty);
|
||||
func.is_extern = true;
|
||||
func.linkage = .external;
|
||||
const id = self.module.addFunction(func);
|
||||
return id;
|
||||
}
|
||||
|
||||
pub fn finalize(self: *Builder) void {
|
||||
self.func = null;
|
||||
self.current_block = null;
|
||||
@@ -160,6 +184,24 @@ pub const Builder = struct {
|
||||
|
||||
pub fn switchToBlock(self: *Builder, block: BlockId) void {
|
||||
self.current_block = block;
|
||||
// Record the starting ref index for this block
|
||||
const func = self.currentFunc();
|
||||
const blk = &func.blocks.items[block.index()];
|
||||
blk.first_ref = self.inst_counter;
|
||||
}
|
||||
|
||||
/// Get the type of a previously emitted instruction Ref.
|
||||
pub fn getRefType(self: *Builder, ref: Ref) TypeId {
|
||||
if (self.func == null) return .s64;
|
||||
const func = self.currentFunc();
|
||||
const ref_idx = @intFromEnum(ref);
|
||||
for (func.blocks.items) |*block| {
|
||||
const first = block.first_ref;
|
||||
if (ref_idx >= first and ref_idx < first + @as(u32, @intCast(block.insts.items.len))) {
|
||||
return block.insts.items[ref_idx - first].ty;
|
||||
}
|
||||
}
|
||||
return .s64;
|
||||
}
|
||||
|
||||
// ── Emit helpers ────────────────────────────────────────────────
|
||||
@@ -405,7 +447,7 @@ pub const Builder = struct {
|
||||
|
||||
// ── Internal helpers ────────────────────────────────────────────
|
||||
|
||||
fn currentFunc(self: *Builder) *Function {
|
||||
pub fn currentFunc(self: *Builder) *Function {
|
||||
return self.module.getFunctionMut(self.func.?);
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +65,10 @@ pub fn printFunction(func: *const Function, func_idx: u32, module: *const Module
|
||||
|
||||
try writer.writeAll(" {\n");
|
||||
|
||||
// Blocks
|
||||
var ref_counter: u32 = 0;
|
||||
// Blocks — each block tracks its own first_ref from emission order
|
||||
_ = func_idx;
|
||||
for (func.blocks.items, 0..) |*block, bi| {
|
||||
var ref_counter: u32 = block.first_ref;
|
||||
try printBlock(block, @intCast(bi), tt, &ref_counter, writer);
|
||||
}
|
||||
|
||||
@@ -371,6 +371,10 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write
|
||||
.box_any => |ba| try writer.print("box_any %{d} : ", .{ba.operand.index()}),
|
||||
.unbox_any => |u| try writer.print("unbox_any %{d} : ", .{u.operand.index()}),
|
||||
|
||||
// ── Reflection ──────────────────────────────────────────
|
||||
.field_name_get => |fr| try writer.print("field_name_get T{d}[%{d}] : ", .{ fr.struct_type.index(), fr.index.index() }),
|
||||
.field_value_get => |fr| try writer.print("field_value_get %{d}, T{d}[%{d}] : ", .{ fr.base.index(), fr.struct_type.index(), fr.index.index() }),
|
||||
|
||||
// ── Terminators ─────────────────────────────────────────
|
||||
.br => |b| {
|
||||
try writer.print("br bb{d}", .{b.target.index()});
|
||||
|
||||
@@ -27,6 +27,10 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable) TypeId {
|
||||
.tuple_type_expr => |tt| resolveTupleType(&tt, table),
|
||||
.parameterized_type_expr => |pt| resolveParameterizedType(&pt, table),
|
||||
.inferred_type => .s64, // inferred — default until we have type inference
|
||||
// Inline type declarations (used as field types)
|
||||
.enum_decl => |ed| resolveInlineEnum(&ed, table),
|
||||
.struct_decl => |sd| resolveInlineStruct(&sd, table),
|
||||
.union_decl => |ud| resolveInlineUnion(&ud, table),
|
||||
else => .s64, // fallback for unknown nodes
|
||||
};
|
||||
}
|
||||
@@ -180,7 +184,7 @@ fn resolveTypeName(name: []const u8, table: *TypeTable) TypeId {
|
||||
return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
|
||||
}
|
||||
|
||||
fn resolveTypePrimitive(name: []const u8) ?TypeId {
|
||||
pub fn resolveTypePrimitive(name: []const u8) ?TypeId {
|
||||
if (name.len == 0) return null;
|
||||
// Fast path for common types
|
||||
if (std.mem.eql(u8, name, "s64")) return .s64;
|
||||
@@ -197,6 +201,7 @@ fn resolveTypePrimitive(name: []const u8) ?TypeId {
|
||||
if (std.mem.eql(u8, name, "string")) return .string;
|
||||
if (std.mem.eql(u8, name, "void")) return .void;
|
||||
if (std.mem.eql(u8, name, "Any")) return .any;
|
||||
if (std.mem.eql(u8, name, "Type")) return .s64; // Type tags are runtime integers
|
||||
if (std.mem.eql(u8, name, "noreturn")) return .noreturn;
|
||||
return null;
|
||||
}
|
||||
@@ -286,3 +291,97 @@ fn resolveParameterizedType(pt: *const ast.ParameterizedTypeExpr, table: *TypeTa
|
||||
const name_id = table.internString(pt.name);
|
||||
return table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
|
||||
}
|
||||
|
||||
// ── Inline type declarations ─────────────────────────────────────────
|
||||
|
||||
fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
|
||||
const alloc = table.alloc;
|
||||
const name_id = table.internString(ed.name);
|
||||
|
||||
// Check if already registered
|
||||
if (table.findByName(name_id)) |existing| return existing;
|
||||
|
||||
// Enum with payloads → tagged union
|
||||
const has_payloads = ed.variant_types.len > 0;
|
||||
if (has_payloads) {
|
||||
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
|
||||
for (ed.variant_names, 0..) |vn, i| {
|
||||
const field_ty = if (i < ed.variant_types.len)
|
||||
(if (ed.variant_types[i]) |vt| resolveAstType(vt, table) else .void)
|
||||
else
|
||||
.void;
|
||||
fields.append(alloc, .{
|
||||
.name = table.internString(vn),
|
||||
.ty = field_ty,
|
||||
}) catch unreachable;
|
||||
}
|
||||
const info: TypeInfo = .{ .@"union" = .{
|
||||
.name = name_id,
|
||||
.fields = fields.items,
|
||||
.tag_type = null,
|
||||
} };
|
||||
const id = table.intern(info);
|
||||
table.update(id, info);
|
||||
return id;
|
||||
}
|
||||
|
||||
// Plain enum (no payloads)
|
||||
var variants = std.ArrayList(StringId).empty;
|
||||
for (ed.variant_names) |vn| {
|
||||
variants.append(alloc, table.internString(vn)) catch unreachable;
|
||||
}
|
||||
const info: TypeInfo = .{ .@"enum" = .{
|
||||
.name = name_id,
|
||||
.variants = variants.items,
|
||||
} };
|
||||
const id = table.intern(info);
|
||||
table.update(id, info);
|
||||
return id;
|
||||
}
|
||||
|
||||
fn resolveInlineStruct(sd: *const ast.StructDecl, table: *TypeTable) TypeId {
|
||||
const alloc = table.alloc;
|
||||
const name_id = table.internString(sd.name);
|
||||
|
||||
if (table.findByName(name_id)) |existing| return existing;
|
||||
|
||||
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
|
||||
for (sd.field_names, sd.field_types) |fname, ftype_node| {
|
||||
const field_ty = resolveAstType(ftype_node, table);
|
||||
fields.append(alloc, .{
|
||||
.name = table.internString(fname),
|
||||
.ty = field_ty,
|
||||
}) catch unreachable;
|
||||
}
|
||||
const info: TypeInfo = .{ .@"struct" = .{
|
||||
.name = name_id,
|
||||
.fields = fields.items,
|
||||
} };
|
||||
const id = table.intern(info);
|
||||
table.update(id, info);
|
||||
return id;
|
||||
}
|
||||
|
||||
fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable) TypeId {
|
||||
const alloc = table.alloc;
|
||||
const name_id = table.internString(ud.name);
|
||||
|
||||
if (table.findByName(name_id)) |existing| return existing;
|
||||
|
||||
var fields = std.ArrayList(TypeInfo.StructInfo.Field).empty;
|
||||
for (ud.field_names, ud.field_types) |fname, ftype_node| {
|
||||
const field_ty = resolveAstType(ftype_node, table);
|
||||
fields.append(alloc, .{
|
||||
.name = table.internString(fname),
|
||||
.ty = field_ty,
|
||||
}) catch unreachable;
|
||||
}
|
||||
const info: TypeInfo = .{ .@"union" = .{
|
||||
.name = name_id,
|
||||
.fields = fields.items,
|
||||
.tag_type = null,
|
||||
} };
|
||||
const id = table.intern(info);
|
||||
table.update(id, info);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -272,6 +272,30 @@ pub const TypeTable = struct {
|
||||
return id;
|
||||
}
|
||||
|
||||
/// Update the TypeInfo for an existing TypeId. Used when a forward-declared
|
||||
/// type (e.g., struct with empty fields) gets its full definition later.
|
||||
pub fn update(self: *TypeTable, id: TypeId, info: TypeInfo) void {
|
||||
const idx = id.index();
|
||||
if (idx < self.infos.items.len) {
|
||||
self.infos.items[idx] = info;
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a named type (struct/union/enum) by its StringId name.
|
||||
/// Returns the TypeId if found, null otherwise.
|
||||
pub fn findByName(self: *const TypeTable, name: StringId) ?TypeId {
|
||||
for (self.infos.items, 0..) |info, i| {
|
||||
const n: ?StringId = switch (info) {
|
||||
.@"struct" => |s| s.name,
|
||||
.@"union" => |u| u.name,
|
||||
.@"enum" => |e| e.name,
|
||||
else => null,
|
||||
};
|
||||
if (n != null and n.? == name) return TypeId.fromIndex(@intCast(i));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Convenience constructors ────────────────────────────────────────
|
||||
|
||||
pub fn ptrTo(self: *TypeTable, pointee: TypeId) TypeId {
|
||||
|
||||
69
src/main.zig
69
src/main.zig
@@ -1,6 +1,9 @@
|
||||
const std = @import("std");
|
||||
const sx = @import("sx");
|
||||
|
||||
/// Feature flag: use the IR pipeline (parse → lower → IR → LLVM) instead of AST-based codegen.
|
||||
const USE_IR_PIPELINE = true;
|
||||
|
||||
pub fn main(init: std.process.Init) !void {
|
||||
const allocator = init.arena.allocator();
|
||||
const io = init.io;
|
||||
@@ -137,12 +140,25 @@ pub fn main(init: std.process.Init) !void {
|
||||
}
|
||||
|
||||
// Cache MISS — codegen + emit .o to memory (verify skipped: JIT catches errors)
|
||||
comp.generateCode() catch { comp.renderErrors(); return; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.generateCodeViaIR() catch { comp.renderErrors(); return; };
|
||||
} else {
|
||||
comp.generateCode() catch { comp.renderErrors(); return; };
|
||||
}
|
||||
timer.record("codegen");
|
||||
|
||||
timer.mark();
|
||||
var cg = &comp.cg.?;
|
||||
const buf = cg.emitObjectToMemory() catch { comp.renderErrors(); return; };
|
||||
const buf = if (USE_IR_PIPELINE) blk2: {
|
||||
comp.ir_emitter.?.verifyWithMessage() catch return;
|
||||
break :blk2 comp.ir_emitter.?.emitObjectToMemory() catch return;
|
||||
} else
|
||||
(emit_blk: {
|
||||
var cg = &comp.cg.?;
|
||||
break :emit_blk cg.emitObjectToMemory() catch {
|
||||
comp.renderErrors();
|
||||
return;
|
||||
};
|
||||
});
|
||||
timer.record("emit");
|
||||
|
||||
// Save .o to cache (extract data before JIT takes ownership)
|
||||
@@ -295,12 +311,20 @@ fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const
|
||||
timer.record("imports");
|
||||
|
||||
timer.mark();
|
||||
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.generateCodeViaIR() catch { comp.renderErrors(); return error.CompileError; };
|
||||
} else {
|
||||
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
||||
}
|
||||
timer.record("codegen");
|
||||
|
||||
timer.mark();
|
||||
var cg = &comp.cg.?;
|
||||
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
|
||||
} else {
|
||||
var cg = &comp.cg.?;
|
||||
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
||||
}
|
||||
timer.record("verify");
|
||||
|
||||
return comp;
|
||||
@@ -328,7 +352,11 @@ fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, targ
|
||||
var timer = Timing.init(false);
|
||||
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer);
|
||||
defer comp.deinit();
|
||||
comp.cg.?.printIR();
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.ir_emitter.?.printIR();
|
||||
} else {
|
||||
comp.cg.?.printIR();
|
||||
}
|
||||
}
|
||||
|
||||
fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !void {
|
||||
@@ -340,7 +368,11 @@ fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, tar
|
||||
break :blk try std.fmt.allocPrint(allocator, "{s}.s", .{name});
|
||||
};
|
||||
const asm_path_z = try allocator.dupeZ(u8, asm_path);
|
||||
comp.cg.?.emitAssembly(asm_path_z.ptr) catch { comp.renderErrors(); return error.CompileError; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.ir_emitter.?.emitAssembly(asm_path_z.ptr) catch return error.CompileError;
|
||||
} else {
|
||||
comp.cg.?.emitAssembly(asm_path_z.ptr) catch { comp.renderErrors(); return error.CompileError; };
|
||||
}
|
||||
std.debug.print("emitted: {s}\n", .{asm_path});
|
||||
}
|
||||
|
||||
@@ -398,16 +430,29 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
|
||||
} else {
|
||||
// Cache MISS — full codegen + emit
|
||||
timer.mark();
|
||||
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.generateCodeViaIR() catch { comp.renderErrors(); return error.CompileError; };
|
||||
} else {
|
||||
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
|
||||
}
|
||||
timer.record("codegen");
|
||||
|
||||
timer.mark();
|
||||
var cg = &comp.cg.?;
|
||||
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
|
||||
} else {
|
||||
var cg = &comp.cg.?;
|
||||
cg.verify() catch { comp.renderErrors(); return error.CompileError; };
|
||||
}
|
||||
timer.record("verify");
|
||||
|
||||
timer.mark();
|
||||
cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; };
|
||||
if (USE_IR_PIPELINE) {
|
||||
comp.ir_emitter.?.emitObject(obj_path.ptr) catch return error.CompileError;
|
||||
} else {
|
||||
var cg = &comp.cg.?;
|
||||
cg.emitObject(obj_path.ptr) catch { comp.renderErrors(); return error.CompileError; };
|
||||
}
|
||||
timer.record("emit");
|
||||
|
||||
// Save .o to cache
|
||||
|
||||
63
tests/ir_parity.sh
Executable file
63
tests/ir_parity.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# IR Parity Test — Compares output of the old pipeline vs the IR pipeline.
|
||||
#
|
||||
# For each example, this script:
|
||||
# 1. Compiles and runs with the old pipeline (sx run)
|
||||
# 2. Compiles and runs with the IR pipeline (sx ir-run, once wired)
|
||||
# 3. Compares stdout output
|
||||
#
|
||||
# For now (Step 3.9 initial), it tests that the IR pipeline can at least
|
||||
# produce LLVM IR without crashing (via sx ir-dump → lower → emit).
|
||||
#
|
||||
# Usage:
|
||||
# bash tests/ir_parity.sh # Test all examples
|
||||
# bash tests/ir_parity.sh --ir-dump # Just test lowering (no codegen)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SX="$ROOT_DIR/zig-out/bin/sx"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
skipped=0
|
||||
errors=""
|
||||
|
||||
# Get all example files
|
||||
examples=$(ls "$ROOT_DIR"/examples/*.sx 2>/dev/null | sort)
|
||||
|
||||
for example in $examples; do
|
||||
name=$(basename "$example" .sx)
|
||||
|
||||
# Test: ir-dump should not crash
|
||||
if output=$("$SX" ir-dump "$example" 2>&1); then
|
||||
# Check that output is non-empty (lowering produced something)
|
||||
if [ -n "$output" ]; then
|
||||
printf " %-30s ${GREEN}ok${NC}\n" "$name"
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
printf " %-30s ${YELLOW}empty${NC}\n" "$name"
|
||||
skipped=$((skipped + 1))
|
||||
fi
|
||||
else
|
||||
printf " %-30s ${RED}FAIL${NC}\n" "$name"
|
||||
failed=$((failed + 1))
|
||||
errors="$errors\n $name: ir-dump crashed"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "$passed passed, $failed failed, $skipped empty"
|
||||
|
||||
if [ -n "$errors" ]; then
|
||||
echo -e "\nErrors:$errors"
|
||||
fi
|
||||
|
||||
exit $failed
|
||||
Reference in New Issue
Block a user