ir done'ish

This commit is contained in:
agra
2026-03-01 22:38:41 +02:00
parent 6a920dbd2c
commit f763765ea2
17 changed files with 1443 additions and 15017 deletions

View File

@@ -1782,6 +1782,24 @@ END;
print("opt param 7: {}\n", opt_process(7)); // opt param 7: 7
}
// Assignment to optional variable (f32 → ?f32)
{
iw: ?f32 = null;
w: f32 = 42.5;
iw = w;
print("opt reassign: {}\n", iw ?? 0.0); // opt reassign: 42.5
// Assignment of computed value to optional
iw2: ?f32 = null;
a: ?f32 = 10.0;
if v := a { iw2 = v + 5.0; }
print("opt compute assign: {}\n", iw2 ?? 0.0); // opt compute assign: 15.0
// Re-assign optional back to null
iw2 = null;
print("opt re-null: {}\n", iw2 ?? 99.0); // opt re-null: 99.0
}
// Generic function with ?T return
{
first_pos :: ($T: Type, a: T, b: T) -> ?T {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,13 +3,13 @@ const ast = @import("ast.zig");
const parser = @import("parser.zig");
const imports = @import("imports.zig");
const sema = @import("sema.zig");
const codegen = @import("codegen.zig");
const errors = @import("errors.zig");
const c_import = @import("c_import.zig");
const ir = @import("ir/ir.zig");
const target_mod = @import("target.zig");
const Node = ast.Node;
pub const TargetConfig = codegen.TargetConfig;
pub const TargetConfig = target_mod.TargetConfig;
pub const Compilation = struct {
allocator: std.mem.Allocator,
@@ -24,7 +24,6 @@ pub const Compilation = struct {
resolved_root: ?*Node = null,
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 {
@@ -41,7 +40,6 @@ 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();
}
@@ -95,21 +93,8 @@ pub const Compilation = struct {
}
}
pub fn generateCode(self: *Compilation) !void {
const root = self.resolved_root orelse self.root orelse return error.CompileError;
var cg = codegen.CodeGen.init(self.allocator, "sx_module", self.target_config);
cg.diagnostics = &self.diagnostics;
cg.import_sources = &self.import_sources;
if (self.sema_result) |*sr| {
cg.sema_result = sr;
}
cg.generate(root) catch return error.CompileError;
self.cg = cg;
}
/// Generate code via the IR pipeline: lower AST → IR → LLVM.
pub fn generateCodeViaIR(self: *Compilation) !void {
pub fn generateCode(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();
@@ -122,7 +107,6 @@ pub const Compilation = struct {
}
/// 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 {
const root = self.resolved_root orelse self.root orelse return &.{};
return c_import.collectCImportSources(self.allocator, root);

View File

@@ -437,7 +437,7 @@ test "emit: struct_gep (pointer to field)" {
b.switchToBlock(entry);
const p_ptr = b.alloca(point_ty);
const y_ptr = b.structGep(p_ptr, 1, ptr_s64);
const y_ptr = b.structGepTyped(p_ptr, 1, ptr_s64, point_ty);
const c42 = b.constInt(42, .s64);
b.store(y_ptr, c42);
const loaded = b.load(y_ptr, .s64);
@@ -482,7 +482,7 @@ test "emit: enum_init and enum_tag (plain enum)" {
b.switchToBlock(entry);
const green = b.enumInit(1, Ref.none, color_ty); // Green = tag 1
const tag = b.enumTag(green);
const tag = b.enumTag(green, .s32);
// Widen tag from s32 to s64 for the return
const wide = b.widen(tag, .s32, .s64);
b.ret(wide, .s64);
@@ -511,10 +511,10 @@ test "emit: tagged union (enum_init with payload, enum_tag, enum_payload)" {
};
const owned_ufields = alloc.dupe(types.TypeInfo.StructInfo.Field, ufields) catch unreachable;
defer alloc.free(owned_ufields);
const shape_ty = module.types.intern(.{ .@"union" = .{
const shape_ty = module.types.intern(.{ .tagged_union = .{
.name = str(&module, "Shape"),
.fields = owned_ufields,
.tag_type = null,
.tag_type = .s64,
} });
var b = Builder.init(&module);
@@ -558,7 +558,6 @@ test "emit: union_get (reinterpret union field)" {
const data_ty = module.types.intern(.{ .@"union" = .{
.name = str(&module, "Data"),
.fields = owned_ufields,
.tag_type = null,
} });
var b = Builder.init(&module);

View File

@@ -2,8 +2,8 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const llvm = @import("../llvm_api.zig");
const c = llvm.c;
const codegen = @import("../codegen.zig");
const TargetConfig = codegen.TargetConfig;
const target_mod = @import("../target.zig");
const TargetConfig = target_mod.TargetConfig;
const ir_types = @import("types.zig");
const TypeId = ir_types.TypeId;
const TypeInfo = ir_types.TypeInfo;
@@ -188,6 +188,29 @@ pub const LLVMEmitter = struct {
if (func.is_extern or func.blocks.items.len == 0) continue;
self.emitFunction(&func, @intCast(i));
}
// Pass 3: Verify typeSizeBytes matches LLVM's ABI sizes
self.verifySizes();
}
/// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types.
fn verifySizes(self: *LLVMEmitter) void {
const dl = c.LLVMGetModuleDataLayout(self.llvm_module);
if (dl == null) return;
const type_count = self.ir_mod.types.infos.items.len;
for (TypeId.first_user..type_count) |idx| {
const ty = TypeId.fromIndex(@intCast(idx));
const info = self.ir_mod.types.get(ty);
// Only verify aggregate types where sizing is non-trivial
switch (info) {
.@"struct", .@"union", .tagged_union, .tuple => {},
else => continue,
}
const llvm_ty = self.toLLVMType(ty);
const llvm_size = c.LLVMABISizeOfType(dl, llvm_ty);
const ir_size = self.ir_mod.types.typeSizeBytes(ty);
std.debug.assert(llvm_size == ir_size);
}
}
/// Run comptime side-effect functions (e.g., `#run main();` at top level).
@@ -241,6 +264,7 @@ pub const LLVMEmitter = struct {
.int => |v| c.LLVMConstInt(llvm_ty, @bitCast(v), 1),
.float => |v| c.LLVMConstReal(llvm_ty, v),
.boolean => |v| c.LLVMConstInt(llvm_ty, @intFromBool(v), 0),
.string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)),
else => c.LLVMConstNull(llvm_ty),
};
c.LLVMSetInitializer(llvm_global, init_val);
@@ -269,14 +293,16 @@ pub const LLVMEmitter = struct {
const is_main = std.mem.eql(u8, name, "main");
// main always returns i32 at the LLVM level (JIT expects it)
const ret_ty = if (is_main) self.cached_i32 else self.toLLVMType(func.ret);
const raw_ret_ty = self.toLLVMType(func.ret);
const ret_ty = if (is_main) self.cached_i32 else if (func.is_extern) self.abiCoerceParamType(func.ret, raw_ret_ty) else raw_ret_ty;
// Build parameter types
// Build parameter types — apply C ABI coercion for foreign (extern) functions
const param_count: c_uint = @intCast(func.params.len);
const param_types = self.alloc.alloc(c.LLVMTypeRef, func.params.len) catch unreachable;
defer self.alloc.free(param_types);
for (func.params, 0..) |param, j| {
param_types[j] = self.toLLVMType(param.ty);
const llvm_ty = self.toLLVMType(param.ty);
param_types[j] = if (func.is_extern) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty;
}
const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, 0);
@@ -356,7 +382,8 @@ pub const LLVMEmitter = struct {
// (blocks may not be in emission order due to nested control flow)
self.ref_counter = block.first_ref;
for (block.insts.items) |instruction| {
for (block.insts.items, 0..) |instruction, inst_i| {
_ = inst_i;
self.emitInst(&instruction, func_idx);
}
}
@@ -528,18 +555,21 @@ pub const LLVMEmitter = struct {
// ── Bitwise ────────────────────────────────────────────
.bit_and => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildAnd(self.builder, lhs, rhs, "and"));
},
.bit_or => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildOr(self.builder, lhs, rhs, "or"));
},
.bit_xor => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildXor(self.builder, lhs, rhs, "xor"));
},
.bit_not => |un| {
@@ -547,13 +577,15 @@ pub const LLVMEmitter = struct {
self.mapRef(c.LLVMBuildNot(self.builder, operand, "not"));
},
.shl => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
self.mapRef(c.LLVMBuildShl(self.builder, lhs, rhs, "shl"));
},
.shr => |bin| {
const lhs = self.resolveRef(bin.lhs);
const rhs = self.resolveRef(bin.rhs);
var lhs = self.resolveRef(bin.lhs);
var rhs = self.resolveRef(bin.rhs);
self.matchBinOpTypes(&lhs, &rhs, instruction.ty);
// Use arithmetic shift right for signed, logical for unsigned
const result = if (isSignedType(instruction.ty))
c.LLVMBuildAShr(self.builder, lhs, rhs, "ashr")
@@ -746,6 +778,15 @@ pub const LLVMEmitter = struct {
if (result.asInt()) |v| {
self.mapRef(c.LLVMConstInt(self.toLLVMType(instruction.ty), @bitCast(v), 0));
return;
} else if (result.asFloat()) |v| {
self.mapRef(c.LLVMConstReal(self.toLLVMType(instruction.ty), v));
return;
} else if (result.asBool()) |v| {
self.mapRef(c.LLVMConstInt(self.toLLVMType(instruction.ty), @intFromBool(v), 0));
return;
} else if (result == .string) {
self.mapRef(self.emitStringConstant(result.string));
return;
}
} else |_| {}
}
@@ -770,7 +811,12 @@ pub const LLVMEmitter = struct {
args[j] = self.coerceArg(args[j], param_types[j]);
}
}
const result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (instruction.ty == .void) "" else "call");
var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (instruction.ty == .void) "" else "call");
// Coerce ABI return value (e.g. i64) back to IR struct type if needed
if (instruction.ty != .void and callee_func.is_extern) {
const expected_ty = self.toLLVMType(instruction.ty);
result = self.coerceArg(result, expected_ty);
}
self.mapRef(result);
},
.call_indirect => |call_op| {
@@ -813,7 +859,11 @@ pub const LLVMEmitter = struct {
if (fn_params) |fp| {
for (0..call_op.args.len) |j| {
if (j < fp.len) {
const llvm_pty = self.toLLVMType(fp[j]);
var llvm_pty = self.toLLVMType(fp[j]);
// Array params in fn-ptr calls decay to pointers (C ABI)
if (c.LLVMGetTypeKind(llvm_pty) == c.LLVMArrayTypeKind) {
llvm_pty = self.cached_ptr;
}
param_tys[j] = llvm_pty;
args[j] = self.coerceArg(args[j], llvm_pty);
} else {
@@ -983,7 +1033,10 @@ pub const LLVMEmitter = struct {
// Safety: verify base is a pointer before GEP
const base_ty_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(base_ptr));
if (base_ty_kind == c.LLVMPointerTypeKind) {
const struct_llvm_ty = self.resolveGepStructType(fa.base, instruction);
const struct_llvm_ty = if (fa.base_type) |bt|
self.toLLVMType(self.resolveAggregate(bt))
else
self.resolveGepStructType(fa.base, instruction);
const st_kind = c.LLVMGetTypeKind(struct_llvm_ty);
if (st_kind == c.LLVMStructTypeKind or st_kind == c.LLVMArrayTypeKind) {
const result = c.LLVMBuildStructGEP2(self.builder, struct_llvm_ty, base_ptr, @intCast(fa.field_index), "gep");
@@ -1006,8 +1059,9 @@ pub const LLVMEmitter = struct {
// Plain enum or builtin integer → integer constant
self.mapRef(c.LLVMConstInt(ty, ei.tag, 0));
} else if (ty_kind == c.LLVMStructTypeKind) {
// Tagged union with no payload — store tag into union struct
const tag_val = c.LLVMConstInt(self.cached_i64, ei.tag, 0);
// Tagged union with no payload — header field 0 holds the tag
const header_ty = c.LLVMStructGetTypeAtIndex(ty, 0);
const tag_val = c.LLVMConstInt(header_ty, ei.tag, 0);
var result = c.LLVMGetUndef(ty);
result = c.LLVMBuildInsertValue(self.builder, result, tag_val, 0, "ei.tag");
self.mapRef(result);
@@ -1015,9 +1069,10 @@ pub const LLVMEmitter = struct {
self.mapRef(c.LLVMConstInt(self.cached_i64, ei.tag, 0));
}
} else {
// Tagged union with payload — { tag, payload_bytes }
// Tagged union with payload — { header, payload_bytes }
const union_ty = self.toLLVMType(instruction.ty);
const tag_val = c.LLVMConstInt(self.cached_i64, ei.tag, 0);
const header_ty = c.LLVMStructGetTypeAtIndex(union_ty, 0);
const tag_val = c.LLVMConstInt(header_ty, ei.tag, 0);
const payload_val = self.resolveRef(ei.payload);
// alloca union, store tag, bitcast payload area, store payload
@@ -1040,7 +1095,17 @@ pub const LLVMEmitter = struct {
const kind = c.LLVMGetTypeKind(val_ty);
if (kind == c.LLVMStructTypeKind) {
// Tagged union — extract field 0 (tag)
self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 0, "etag"));
var tag = c.LLVMBuildExtractValue(self.builder, val, 0, "etag");
// Truncate to declared tag width if needed (e.g. i64 → i32 for u32 tags)
// This is essential for FFI unions where the i64 tag slot contains
// a smaller tag + uninitialized padding (e.g. SDL_Event's u32 type + u32 reserved)
const target_ty = self.toLLVMType(instruction.ty);
const extracted_bits = c.LLVMGetIntTypeWidth(c.LLVMTypeOf(tag));
const target_bits = c.LLVMGetIntTypeWidth(target_ty);
if (target_bits < extracted_bits) {
tag = c.LLVMBuildTrunc(self.builder, tag, target_ty, "etag.trunc");
}
self.mapRef(tag);
} else {
// Plain enum — the value IS the tag
self.mapRef(val);
@@ -1071,28 +1136,34 @@ pub const LLVMEmitter = struct {
const base_ty = c.LLVMTypeOf(base);
const kind = c.LLVMGetTypeKind(base_ty);
if (kind == c.LLVMStructTypeKind) {
// { tag, payload_bytes } — extract payload then bitcast
// Tagged union { header, payload_bytes } — access payload at field 1
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ug.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, base_ty, tmp, 1, "ug.pp");
const typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "ug.cast");
self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, typed_ptr, "ug.val"));
self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, payload_ptr, "ug.val"));
} else {
// Plain reinterpret
self.mapRef(c.LLVMBuildBitCast(self.builder, base, result_ty, "ug.cast"));
// Untagged union [N x i8] — alloca, store, reinterpret-load
const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ug.tmp");
_ = c.LLVMBuildStore(self.builder, base, tmp);
self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, tmp, "ug.val"));
}
},
.union_gep => |fa| {
const base_ptr = self.resolveRef(fa.base);
const base_ty_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(base_ptr));
if (base_ty_kind == c.LLVMPointerTypeKind) {
const union_llvm_ty = self.resolveGepStructType(fa.base, instruction);
const union_llvm_ty = if (fa.base_type) |bt|
self.toLLVMType(self.resolveAggregate(bt))
else
self.resolveGepStructType(fa.base, instruction);
const st_kind = c.LLVMGetTypeKind(union_llvm_ty);
if (st_kind == c.LLVMStructTypeKind) {
// Tagged union — payload is at field 1
const payload_ptr = c.LLVMBuildStructGEP2(self.builder, union_llvm_ty, base_ptr, 1, "ugep.pp");
self.mapRef(c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "ugep.cast"));
self.mapRef(payload_ptr);
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
// Untagged union — data starts at offset 0
self.mapRef(base_ptr);
}
} else {
self.mapRef(c.LLVMGetUndef(self.cached_ptr));
@@ -1317,21 +1388,19 @@ pub const LLVMEmitter = struct {
_ = c.LLVMBuildCall2(self.builder, self.getMemsetType(), memset_fn, &args, 3, "");
self.advanceRefCounter();
},
.sqrt => {
.sqrt, .sin, .cos, .floor => {
const val = self.resolveRef(bi.args[0]);
const val_ty = c.LLVMTypeOf(val);
const val_kind = c.LLVMGetTypeKind(val_ty);
if (val_kind == c.LLVMFloatTypeKind) {
// f32 → sqrtf
const sqrtf_fn = self.getOrDeclareSqrtf();
const f = self.getOrDeclareMathF32(bi.builtin);
var args = [_]c.LLVMValueRef{val};
self.mapRef(c.LLVMBuildCall2(self.builder, self.getSqrtfType(), sqrtf_fn, &args, 1, "sqrtf"));
self.mapRef(c.LLVMBuildCall2(self.builder, self.getMathF32Type(), f, &args, 1, @tagName(bi.builtin)));
} else {
// f64 → sqrt (default)
const coerced = if (val_kind != c.LLVMDoubleTypeKind) self.coerceArg(val, self.cached_f64) else val;
const sqrt_fn = self.getOrDeclareSqrt();
const f = self.getOrDeclareMathF64(bi.builtin);
var args = [_]c.LLVMValueRef{coerced};
self.mapRef(c.LLVMBuildCall2(self.builder, self.getSqrtType(), sqrt_fn, &args, 1, "sqrt"));
self.mapRef(c.LLVMBuildCall2(self.builder, self.getMathF64Type(), f, &args, 1, @tagName(bi.builtin)));
}
},
.out => {
@@ -1563,6 +1632,7 @@ pub const LLVMEmitter = struct {
const field_count: u32 = switch (field_info) {
.@"struct" => |s| @intCast(s.fields.len),
.@"union" => |u| @intCast(u.fields.len),
.tagged_union => |u| @intCast(u.fields.len),
.@"enum" => |e| @intCast(e.variants.len),
else => 0,
};
@@ -1787,6 +1857,15 @@ pub const LLVMEmitter = struct {
return self.cached_i64;
}
/// Resolve through pointer types to get the underlying aggregate type.
fn resolveAggregate(self: *LLVMEmitter, ty: TypeId) TypeId {
if (!ty.isBuiltin()) {
const info = self.ir_mod.types.get(ty);
if (info == .pointer) return info.pointer.pointee;
}
return ty;
}
// ── Comparison helpers ────────────────────────────────────────────
fn emitCmp(self: *LLVMEmitter, bin: ir_inst.BinOp, _: TypeId, int_pred: c_uint, float_pred: c_uint) void {
@@ -1813,19 +1892,27 @@ pub const LLVMEmitter = struct {
}
}
// Struct types (strings, slices): compare fields individually
// Struct types (strings, slices, tagged unions): compare fields individually
if (kind == c.LLVMStructTypeKind and rhs_kind == c.LLVMStructTypeKind) {
const n_fields = c.LLVMCountStructElementTypes(lhs_ty);
if (n_fields >= 2) {
// For {ptr, i64} structs (string/slice): compare ptr and len
// eq: (f0_l == f0_r) && (f1_l == f1_r)
// ne: (f0_l != f0_r) || (f1_l != f1_r)
const is_eq = (int_pred == c.LLVMIntEQ);
const f0_l = c.LLVMBuildExtractValue(self.builder, lhs, 0, "sc.l0");
const f0_r = c.LLVMBuildExtractValue(self.builder, rhs, 0, "sc.r0");
const cmp0 = c.LLVMBuildICmp(self.builder, @intCast(int_pred), f0_l, f0_r, "sc.c0");
// Check if field 1 is an array (tagged union payload) — skip comparison
// For tagged unions {tag, [N x i8]}, the tag comparison alone is sufficient
const f1_ty = c.LLVMStructGetTypeAtIndex(lhs_ty, 1);
const f1_kind = c.LLVMGetTypeKind(f1_ty);
if (f1_kind == c.LLVMArrayTypeKind) {
// Tagged union: compare tag only
self.mapRef(cmp0);
return;
}
const f1_l = c.LLVMBuildExtractValue(self.builder, lhs, 1, "sc.l1");
const f1_r = c.LLVMBuildExtractValue(self.builder, rhs, 1, "sc.r1");
const cmp0 = c.LLVMBuildICmp(self.builder, @intCast(int_pred), f0_l, f0_r, "sc.c0");
const cmp1 = c.LLVMBuildICmp(self.builder, @intCast(int_pred), f1_l, f1_r, "sc.c1");
const result = if (is_eq)
c.LLVMBuildAnd(self.builder, cmp0, cmp1, "sc.and")
@@ -1862,6 +1949,8 @@ pub const LLVMEmitter = struct {
var rhs = self.resolveRef(bin.rhs);
const lhs_ty = c.LLVMTypeOf(lhs);
const kind = c.LLVMGetTypeKind(lhs_ty);
// Determine signedness from IR operand type
const is_unsigned = self.isRefUnsigned(bin.lhs) or self.isRefUnsigned(bin.rhs);
// Coerce operands to same type if needed
if (kind == c.LLVMIntegerTypeKind) {
const rhs_ty = c.LLVMTypeOf(rhs);
@@ -1869,16 +1958,21 @@ pub const LLVMEmitter = struct {
if (rhs_kind == c.LLVMIntegerTypeKind) {
const lw = c.LLVMGetIntTypeWidth(lhs_ty);
const rw = c.LLVMGetIntTypeWidth(rhs_ty);
if (lw < rw) lhs = c.LLVMBuildSExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildSExt(self.builder, rhs, lhs_ty, "cmp.ext");
if (is_unsigned) {
if (lw < rw) lhs = c.LLVMBuildZExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildZExt(self.builder, rhs, lhs_ty, "cmp.ext");
} else {
if (lw < rw) lhs = c.LLVMBuildSExt(self.builder, lhs, rhs_ty, "cmp.ext")
else if (rw < lw) rhs = c.LLVMBuildSExt(self.builder, rhs, lhs_ty, "cmp.ext");
}
}
}
const result = if (kind == c.LLVMFloatTypeKind or kind == c.LLVMDoubleTypeKind)
c.LLVMBuildFCmp(self.builder, @intCast(float_pred), lhs, rhs, "fcmp")
else if (is_unsigned)
c.LLVMBuildICmp(self.builder, @intCast(unsigned_pred), lhs, rhs, "icmp")
else
// Default to signed comparison (most common in sx)
c.LLVMBuildICmp(self.builder, @intCast(signed_pred), lhs, rhs, "icmp");
_ = unsigned_pred;
self.mapRef(result);
}
@@ -2030,24 +2124,36 @@ pub const LLVMEmitter = struct {
return c.LLVMFunctionType(self.cached_ptr, &param_types, 3, 0);
}
fn getOrDeclareSqrt(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "sqrt")) |f| return f;
return c.LLVMAddFunction(self.llvm_module, "sqrt", self.getSqrtType());
fn getOrDeclareMathF64(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef {
const name: [*:0]const u8 = switch (id) {
.sqrt => "sqrt",
.sin => "sin",
.cos => "cos",
.floor => "floor",
else => unreachable,
};
if (c.LLVMGetNamedFunction(self.llvm_module, name)) |f| return f;
return c.LLVMAddFunction(self.llvm_module, name, self.getMathF64Type());
}
fn getSqrtType(self: *LLVMEmitter) c.LLVMTypeRef {
// sqrt(f64) → f64
fn getMathF64Type(self: *LLVMEmitter) c.LLVMTypeRef {
var param_types = [_]c.LLVMTypeRef{self.cached_f64};
return c.LLVMFunctionType(self.cached_f64, &param_types, 1, 0);
}
fn getOrDeclareSqrtf(self: *LLVMEmitter) c.LLVMValueRef {
if (c.LLVMGetNamedFunction(self.llvm_module, "sqrtf")) |f| return f;
return c.LLVMAddFunction(self.llvm_module, "sqrtf", self.getSqrtfType());
fn getOrDeclareMathF32(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef {
const name: [*:0]const u8 = switch (id) {
.sqrt => "sqrtf",
.sin => "sinf",
.cos => "cosf",
.floor => "floorf",
else => unreachable,
};
if (c.LLVMGetNamedFunction(self.llvm_module, name)) |f| return f;
return c.LLVMAddFunction(self.llvm_module, name, self.getMathF32Type());
}
fn getSqrtfType(self: *LLVMEmitter) c.LLVMTypeRef {
// sqrtf(f32) → f32
fn getMathF32Type(self: *LLVMEmitter) c.LLVMTypeRef {
var param_types = [_]c.LLVMTypeRef{self.cached_f32};
return c.LLVMFunctionType(self.cached_f32, &param_types, 1, 0);
}
@@ -2279,6 +2385,31 @@ pub const LLVMEmitter = struct {
}
}
}
// Struct → Integer (C ABI coercion: store struct to memory, load as integer)
if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMIntegerTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, param_ty, "abi.tmp");
_ = c.LLVMBuildStore(self.builder, c.LLVMConstNull(param_ty), tmp);
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.coerce");
}
// Integer → Struct (C ABI return coercion: store integer to memory, load as struct)
if (val_kind == c.LLVMIntegerTypeKind and param_kind == c.LLVMStructTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "abi.ret.tmp");
_ = c.LLVMBuildStore(self.builder, val, tmp);
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.ret.coerce");
}
// Array → Ptr (array decay: alloca + GEP to first element)
if (val_kind == c.LLVMArrayTypeKind and param_kind == c.LLVMPointerTypeKind) {
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "ca.arr");
_ = c.LLVMBuildStore(self.builder, val, tmp);
const zero = c.LLVMConstInt(self.cached_i64, 0, 0);
var indices = [_]c.LLVMValueRef{ zero, zero };
return c.LLVMBuildGEP2(self.builder, val_ty, tmp, &indices, 2, "ca.decay");
}
// Int → Ptr (null literal: inttoptr)
if (val_kind == c.LLVMIntegerTypeKind and param_kind == c.LLVMPointerTypeKind) {
return c.LLVMBuildIntToPtr(self.builder, val, param_ty, "ca.itp");
}
return val;
}
@@ -2390,19 +2521,48 @@ pub const LLVMEmitter = struct {
}
return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0);
},
.@"enum" => self.cached_i64, // enums are i64 by default
.@"enum" => |e| {
// Use backing type if declared (e.g. enum u32 → i32), else i64
if (e.backing_type) |bt| return self.toLLVMType(bt);
return self.cached_i64;
},
.@"union" => |u| {
// Union: tag (i64) + largest-field payload
// For simplicity, use { i64, [N x i8] } where N = max field size
var max_size: u32 = 0;
// Untagged union — just [N x i8]
var max_size: usize = 0;
for (u.fields) |field| {
const sz = self.ir_mod.types.sizeOf(field.ty);
const sz = self.ir_mod.types.typeSizeBytes(field.ty);
if (sz > max_size) max_size = sz;
}
if (max_size == 0) max_size = 8;
return c.LLVMArrayType2(self.cached_i8, @intCast(max_size));
},
.tagged_union => |u| {
// Tagged union — { header, [N x i8] }
var max_size: usize = 0;
for (u.fields) |field| {
const sz = self.ir_mod.types.typeSizeBytes(field.ty);
if (sz > max_size) max_size = sz;
}
if (max_size == 0) max_size = 8;
var header_size: usize = self.ir_mod.types.typeSizeBytes(u.tag_type);
if (u.backing_type) |bt| {
const bi = self.ir_mod.types.get(bt);
if (bi == .@"struct" and bi.@"struct".fields.len > 1) {
header_size = 0;
const fields = bi.@"struct".fields;
for (fields[0 .. fields.len - 1]) |f| {
header_size += self.ir_mod.types.typeSizeBytes(f.ty);
}
const backing_payload = self.ir_mod.types.typeSizeBytes(fields[fields.len - 1].ty);
if (backing_payload > max_size) max_size = backing_payload;
}
}
const header_llvm = c.LLVMIntTypeInContext(self.context, @intCast(header_size * 8));
var field_types: [2]c.LLVMTypeRef = .{
self.cached_i64, // tag
c.LLVMArrayType2(self.cached_i8, max_size), // payload
header_llvm,
c.LLVMArrayType2(self.cached_i8, @intCast(max_size)),
};
return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0);
},
@@ -2423,6 +2583,56 @@ pub const LLVMEmitter = struct {
};
}
// ── C ABI coercion for foreign functions ──────────────────────────
//
// On ARM64 (and x86_64), the C calling convention coerces small struct
// arguments to integers for register passing:
// - String/slice {ptr, i64} → ptr (extract raw pointer)
// - Small integer struct (≤ 8 bytes, non-HFA) → i64
// - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it)
fn abiCoerceParamType(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef {
// String/slice → raw pointer
if (ir_ty == .string) return self.cached_ptr;
if (!ir_ty.isBuiltin()) {
const info = self.ir_mod.types.get(ir_ty);
if (info == .slice) return self.cached_ptr;
}
// Only coerce struct types
if (c.LLVMGetTypeKind(llvm_ty) != c.LLVMStructTypeKind) return llvm_ty;
// Check if it's an HFA (all float or all double fields) — leave as-is
const n_fields = c.LLVMCountStructElementTypes(llvm_ty);
if (n_fields >= 1 and n_fields <= 4) {
var all_float = true;
var all_double = true;
var fi: c_uint = 0;
while (fi < n_fields) : (fi += 1) {
const ft = c.LLVMStructGetTypeAtIndex(llvm_ty, fi);
const fk = c.LLVMGetTypeKind(ft);
if (fk != c.LLVMFloatTypeKind) all_float = false;
if (fk != c.LLVMDoubleTypeKind) all_double = false;
}
if (all_float or all_double) return llvm_ty;
}
// Small struct (≤ 8 bytes) → coerce to i64
const size = c.LLVMABISizeOfType(
c.LLVMGetModuleDataLayout(self.llvm_module),
llvm_ty,
);
if (size <= 8) return self.cached_i64;
// Medium struct (9-16 bytes) → coerce to [2 x i64]
if (size <= 16) {
return c.LLVMArrayType2(self.cached_i64, 2);
}
// Large struct (> 16 bytes) → leave as-is (should be indirect, but handle later)
return llvm_ty;
}
// ── Cached composite types ──────────────────────────────────────
fn getStringStructType(self: *LLVMEmitter) c.LLVMTypeRef {
@@ -2457,6 +2667,25 @@ pub const LLVMEmitter = struct {
// ── String constant emission ────────────────────────────────────
/// Build a constant string { ptr, i64 } value without using the builder
/// (safe to call during global initialization, before any function body is emitted).
fn emitConstStringGlobal(self: *LLVMEmitter, str: []const u8) c.LLVMValueRef {
const str_z = self.alloc.dupeZ(u8, str) catch unreachable;
defer self.alloc.free(str_z);
const len: c_uint = @intCast(str.len + 1); // include null terminator
const str_const = c.LLVMConstStringInContext(self.context, str_z.ptr, len - 1, 0);
const arr_ty = c.LLVMArrayType2(self.cached_i8, len);
const str_global_val = c.LLVMAddGlobal(self.llvm_module, arr_ty, "str.data");
c.LLVMSetInitializer(str_global_val, str_const);
c.LLVMSetGlobalConstant(str_global_val, 1);
c.LLVMSetLinkage(str_global_val, c.LLVMPrivateLinkage);
c.LLVMSetUnnamedAddress(str_global_val, c.LLVMGlobalUnnamedAddr);
// Build constant { ptr, i64 } aggregate
const len_val = c.LLVMConstInt(self.cached_i64, str.len, 0);
var fields = [_]c.LLVMValueRef{ str_global_val, len_val };
return c.LLVMConstStructInContext(self.context, &fields, 2, 0);
}
fn emitStringConstant(self: *LLVMEmitter, str: []const u8) c.LLVMValueRef {
// LLVMBuildGlobalStringPtr needs a null-terminated C string
const str_z = self.alloc.dupeZ(u8, str) catch unreachable;
@@ -2490,6 +2719,9 @@ pub const LLVMEmitter = struct {
.@"union" => |u| {
for (u.fields) |f| name_ids.append(self.alloc, f.name) catch unreachable;
},
.tagged_union => |u| {
for (u.fields) |f| name_ids.append(self.alloc, f.name) catch unreachable;
},
.@"enum" => |e| {
for (e.variants) |v| name_ids.append(self.alloc, v) catch unreachable;
},
@@ -2538,6 +2770,7 @@ pub const LLVMEmitter = struct {
const fields = switch (info) {
.@"struct" => |s| s.fields,
.@"union" => |u| u.fields,
.tagged_union => |u| u.fields,
else => &[_]TypeInfo.StructInfo.Field{},
};
@@ -2572,7 +2805,7 @@ pub const LLVMEmitter = struct {
var case_values = std.ArrayList(c.LLVMValueRef).empty;
defer case_values.deinit(self.alloc);
const is_union = info == .@"union";
const is_union = info == .@"union" or info == .tagged_union;
for (fields, 0..) |field, i| {
const case_bb = c.LLVMAppendBasicBlockInContext(self.context, current_func, "fv.case");
c.LLVMAddCase(switch_inst, c.LLVMConstInt(self.cached_i64, @intCast(i), 0), case_bb);
@@ -2651,6 +2884,8 @@ pub const LLVMEmitter = struct {
if (c.LLVMVerifyModule(self.llvm_module, c.LLVMReturnStatusAction, &err_msg) != 0) {
if (err_msg != null) {
const msg = std.mem.span(err_msg);
// Dump IR to /tmp for debugging
_ = c.LLVMPrintModuleToFile(self.llvm_module, "/tmp/sx_debug.ll", null);
std.debug.print("LLVM verification failed: {s}\n", .{msg});
c.LLVMDisposeMessage(err_msg);
}
@@ -2707,6 +2942,20 @@ pub const LLVMEmitter = struct {
return error.EmitFailed;
}
}
/// Check if an IR Ref's type is an unsigned integer (u8, u16, u32, u64).
fn isRefUnsigned(self: *LLVMEmitter, ref: Ref) bool {
if (ref.isNone()) return false;
const func = &self.ir_mod.functions.items[self.current_func_idx];
const ref_idx = ref.index();
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))) {
const ty = block.insts.items[ref_idx - first].ty;
return ty == .u8 or ty == .u16 or ty == .u32 or ty == .u64;
}
}
return false;
}
};
// ── Type classification helpers ─────────────────────────────────────

View File

@@ -251,6 +251,10 @@ pub const Conversion = struct {
pub const FieldAccess = struct {
base: Ref,
field_index: u32,
/// The IR type of the aggregate being accessed (struct, union, etc.).
/// Used by the LLVM emitter to resolve the correct type for GEP operations
/// without guessing from LLVM value chains.
base_type: ?TypeId = null,
};
pub const Aggregate = struct {
@@ -286,6 +290,9 @@ pub const BuiltinCall = struct {
pub const BuiltinId = enum(u16) {
out,
sqrt,
sin,
cos,
floor,
size_of,
cast,
malloc,

View File

@@ -800,6 +800,7 @@ pub const Interpreter = struct {
const fields = switch (info) {
.@"struct" => |s| s.fields,
.@"union" => |u| u.fields,
.tagged_union => |u| u.fields,
else => return error.CannotEvalComptime,
};
if (idx >= fields.len) return error.OutOfBounds;
@@ -821,6 +822,7 @@ pub const Interpreter = struct {
const fields = switch (info) {
.@"struct" => |s| s.fields,
.@"union" => |u| u.fields,
.tagged_union => |u| u.fields,
else => return error.CannotEvalComptime,
};
const field_ty_tag: i64 = if (idx < fields.len) @intFromEnum(fields[idx].ty) else 0;
@@ -1225,6 +1227,21 @@ pub const Interpreter = struct {
const f = val.asFloat() orelse return error.TypeError;
return .{ .value = .{ .float = @sqrt(f) } };
},
.sin => {
const val = frame.getRef(bi.args[0]);
const f = val.asFloat() orelse return error.TypeError;
return .{ .value = .{ .float = @sin(f) } };
},
.cos => {
const val = frame.getRef(bi.args[0]);
const f = val.asFloat() orelse return error.TypeError;
return .{ .value = .{ .float = @cos(f) } };
},
.floor => {
const val = frame.getRef(bi.args[0]);
const f = val.asFloat() orelse return error.TypeError;
return .{ .value = .{ .float = @floor(f) } };
},
.cast, .type_of, .alloc, .dealloc => {
return error.CannotEvalComptime;
},

File diff suppressed because it is too large Load Diff

View File

@@ -313,14 +313,18 @@ pub const Builder = struct {
return self.emit(.{ .struct_gep = .{ .base = base, .field_index = field_index } }, ty);
}
pub fn structGepTyped(self: *Builder, base: Ref, field_index: u32, ty: TypeId, base_type: TypeId) Ref {
return self.emit(.{ .struct_gep = .{ .base = base, .field_index = field_index, .base_type = base_type } }, ty);
}
// ── Enum ops ────────────────────────────────────────────────────
pub fn enumInit(self: *Builder, tag: u32, payload: Ref, ty: TypeId) Ref {
return self.emit(.{ .enum_init = .{ .tag = tag, .payload = payload } }, ty);
}
pub fn enumTag(self: *Builder, val: Ref) Ref {
return self.emit(.{ .enum_tag = .{ .operand = val } }, .s32);
pub fn enumTag(self: *Builder, val: Ref, tag_ty: TypeId) Ref {
return self.emit(.{ .enum_tag = .{ .operand = val } }, tag_ty);
}
// ── Optional ops ────────────────────────────────────────────────

View File

@@ -454,6 +454,7 @@ fn writeType(id: TypeId, tt: *const TypeTable, writer: Writer) !void {
.@"struct" => |s| try writer.writeAll(tt.getString(s.name)),
.@"enum" => |e| try writer.writeAll(tt.getString(e.name)),
.@"union" => |u| try writer.writeAll(tt.getString(u.name)),
.tagged_union => |u| try writer.writeAll(tt.getString(u.name)),
.protocol => |p| try writer.writeAll(tt.getString(p.name)),
.pointer => |p| {
try writer.writeByte('*');

View File

@@ -146,7 +146,7 @@ fn resolveNamedType(name: []const u8, kind: NamedKind, table: *TypeTable) TypeId
return switch (kind) {
.@"struct" => table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }),
.@"enum" => table.intern(.{ .@"enum" = .{ .name = name_id, .variants = &.{} } }),
.@"union" => table.intern(.{ .@"union" = .{ .name = name_id, .fields = &.{}, .tag_type = null } }),
.@"union" => table.intern(.{ .@"union" = .{ .name = name_id, .fields = &.{} } }),
};
}
@@ -367,10 +367,46 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
.ty = field_ty,
}) catch unreachable;
}
const info: TypeInfo = .{ .@"union" = .{
// Resolve backing type and tag type from enum struct
// e.g. enum struct { tag: u32; _: u32; payload: [30]u32; } { ... }
var backing_type: ?TypeId = null;
var tag_type: ?TypeId = null;
if (ed.backing_type) |bt| {
const backing_ty = resolveAstType(bt, table);
backing_type = backing_ty;
// Extract tag type from first field of backing struct
const backing_info = table.get(backing_ty);
if (backing_info == .@"struct") {
if (backing_info.@"struct".fields.len > 0) {
tag_type = backing_info.@"struct".fields[0].ty;
}
}
}
// Build explicit tag values from variant_values (e.g., quit :: 0x100)
var explicit_tag_vals: ?[]const i64 = null;
if (ed.variant_values.len > 0) {
var vals = std.ArrayList(i64).empty;
for (0..ed.variant_names.len) |i| {
if (i < ed.variant_values.len) {
if (ed.variant_values[i]) |vv| {
if (vv.data == .int_literal) {
vals.append(alloc, vv.data.int_literal.value) catch unreachable;
continue;
}
}
}
vals.append(alloc, @intCast(i)) catch unreachable;
}
explicit_tag_vals = vals.items;
}
const info: TypeInfo = .{ .tagged_union = .{
.name = name_id,
.fields = fields.items,
.tag_type = null,
.tag_type = tag_type orelse .s64, // enum unions are always tagged (default i64)
.backing_type = backing_type,
.explicit_tag_values = explicit_tag_vals,
} };
const id = table.intern(info);
table.update(id, info);
@@ -414,11 +450,21 @@ fn resolveInlineEnum(ed: *const ast.EnumDecl, table: *TypeTable) TypeId {
}
explicit_vals = vals.items;
}
// Resolve backing type for sized enums (e.g. enum u32 { ... })
var enum_backing: ?TypeId = null;
if (ed.backing_type) |bt| {
// Only use simple backing types (u8, u16, u32, etc.), not struct backing (enum struct)
if (bt.data != .struct_decl) {
enum_backing = resolveAstType(bt, table);
}
}
const info: TypeInfo = .{ .@"enum" = .{
.name = name_id,
.variants = variants.items,
.is_flags = ed.is_flags,
.explicit_values = explicit_vals,
.backing_type = enum_backing,
} };
const id = table.intern(info);
table.update(id, info);
@@ -465,7 +511,6 @@ fn resolveInlineUnion(ud: *const ast.UnionDecl, table: *TypeTable) TypeId {
const info: TypeInfo = .{ .@"union" = .{
.name = name_id,
.fields = fields.items,
.tag_type = null,
} };
const id = table.intern(info);
table.update(id, info);

View File

@@ -56,6 +56,7 @@ pub const TypeInfo = union(enum) {
@"struct": StructInfo,
@"enum": EnumInfo,
@"union": UnionInfo,
tagged_union: TaggedUnionInfo,
array: ArrayInfo,
slice: SliceInfo,
pointer: PointerInfo,
@@ -84,12 +85,20 @@ pub const TypeInfo = union(enum) {
variants: []const StringId,
is_flags: bool = false,
explicit_values: ?[]const i64 = null, // for flags (power-of-2) or custom values
backing_type: ?TypeId = null, // e.g. u32 for `enum u32 { ... }`
};
pub const UnionInfo = struct {
name: StringId,
fields: []const StructInfo.Field,
tag_type: ?TypeId, // tagged union enum type, null if untagged
};
pub const TaggedUnionInfo = struct {
name: StringId,
fields: []const StructInfo.Field,
tag_type: TypeId, // tag integer type (e.g. .u32, .s64)
backing_type: ?TypeId = null, // enum struct backing (e.g. { tag: u32; _: u32; payload: [30]u32; })
explicit_tag_values: ?[]const i64 = null, // explicit variant values (e.g., quit :: 0x100)
};
pub const ArrayInfo = struct {
@@ -290,6 +299,7 @@ pub const TypeTable = struct {
const n: ?StringId = switch (info) {
.@"struct" => |s| s.name,
.@"union" => |u| u.name,
.tagged_union => |u| u.name,
.@"enum" => |e| e.name,
else => null,
};
@@ -358,15 +368,27 @@ pub const TypeTable = struct {
return if (total == 0) 8 else total;
},
.@"union" => |u| {
// Size of union = tag + max(field sizes)
var max_field: u32 = 0;
for (u.fields) |f| {
const sz = self.sizeOf(f.ty);
if (sz > max_field) max_field = sz;
}
return 8 + @max(max_field, 8);
return @max(max_field, 8);
},
.tagged_union => |u| {
if (u.backing_type) |bt| return self.sizeOf(bt);
var max_field: u32 = 0;
for (u.fields) |f| {
const sz = self.sizeOf(f.ty);
if (sz > max_field) max_field = sz;
}
const tag_sz = @as(u32, @intCast(self.typeSizeBytes(u.tag_type)));
return tag_sz + @max(max_field, 8);
},
.@"enum" => |e| {
if (e.backing_type) |bt| return self.sizeOf(bt);
return 8;
},
.@"enum" => 8, // plain enums are just integer tags
.tuple => |t| {
var total: u32 = 0;
for (t.fields) |f| total += @max(self.sizeOf(f), 8);
@@ -376,6 +398,145 @@ pub const TypeTable = struct {
};
}
/// Compute the ABI size in bytes for a type, matching LLVM's struct layout rules.
/// This is the authoritative size computation used for closure env sizing and
/// verified against LLVMABISizeOfType.
pub fn typeSizeBytes(self: *const TypeTable, ty: TypeId) usize {
if (ty == .void) return 0;
if (ty == .bool) return 1;
if (ty == .u8 or ty == .s8) return 1;
if (ty == .u16 or ty == .s16) return 2;
if (ty == .s32 or ty == .u32 or ty == .f32) return 4;
if (ty == .s64 or ty == .u64 or ty == .f64) return 8;
if (ty == .string) return 16; // {ptr, i64}
if (ty.isBuiltin()) return 8; // default for unknown builtins
const info = self.get(ty);
return switch (info) {
.pointer, .many_pointer, .function => 8,
.slice => 16, // {ptr, i64}
.closure => 16, // {fn_ptr, env_ptr}
.optional => |o| blk: {
const child_info = self.get(o.child);
if (child_info == .pointer or child_info == .many_pointer or child_info == .function)
break :blk 8;
if (child_info == .closure)
break :blk 16; // {fn_ptr, env_ptr}
const cs = self.typeSizeBytes(o.child);
const ca = self.typeAlignBytes(o.child);
// { T, i1 } — i1 goes right after T, then pad to struct alignment
const unpadded = cs + 1;
break :blk (unpadded + ca - 1) & ~(ca - 1);
},
.@"struct" => |s| blk: {
var offset: usize = 0;
var max_a: usize = 1;
for (s.fields) |f| {
const fs = self.typeSizeBytes(f.ty);
const fa = self.typeAlignBytes(f.ty);
if (fa > max_a) max_a = fa;
offset = (offset + fa - 1) & ~(fa - 1);
offset += fs;
}
break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1);
},
.@"union" => |u| blk: {
var max_payload: usize = 0;
for (u.fields) |f| {
const fs = self.typeSizeBytes(f.ty);
if (fs > max_payload) max_payload = fs;
}
break :blk if (max_payload == 0) 8 else max_payload;
},
.tagged_union => |u| blk: {
if (u.backing_type) |bt| break :blk self.typeSizeBytes(bt);
var max_payload: usize = 0;
for (u.fields) |f| {
const fs = self.typeSizeBytes(f.ty);
if (fs > max_payload) max_payload = fs;
}
const tag_size = self.typeSizeBytes(u.tag_type);
const raw = max_payload + tag_size;
break :blk (raw + 7) & ~@as(usize, 7);
},
.array => |a| blk: {
const elem_size = self.typeSizeBytes(a.element);
break :blk elem_size * @as(usize, @intCast(a.length));
},
.vector => |v| blk: {
const elem_size = self.typeSizeBytes(v.element);
const raw = elem_size * @as(usize, @intCast(v.length));
// LLVM vectors round ABI size up to next power of 2
break :blk std.math.ceilPowerOfTwo(usize, raw) catch raw;
},
.tuple => |t| blk: {
var offset: usize = 0;
var max_a: usize = 1;
for (t.fields) |f| {
const fs = self.typeSizeBytes(f);
const fa = self.typeAlignBytes(f);
if (fa > max_a) max_a = fa;
offset = (offset + fa - 1) & ~(fa - 1);
offset += fs;
}
break :blk if (offset == 0) 0 else (offset + max_a - 1) & ~(max_a - 1);
},
.any => 16,
.protocol => 16,
.@"enum" => |e| {
if (e.backing_type) |bt| return self.typeSizeBytes(bt);
return 8;
},
else => 8,
};
}
/// Compute the ABI alignment in bytes for a type, matching LLVM's rules.
pub fn typeAlignBytes(self: *const TypeTable, ty: TypeId) usize {
if (ty == .void) return 1;
if (ty == .bool) return 1;
if (ty == .u8 or ty == .s8) return 1;
if (ty == .u16 or ty == .s16) return 2;
if (ty == .s32 or ty == .u32 or ty == .f32) return 4;
if (ty == .s64 or ty == .u64 or ty == .f64) return 8;
if (ty == .string) return 8;
if (ty.isBuiltin()) return 8;
const info = self.get(ty);
return switch (info) {
.pointer, .many_pointer, .function => 8,
.slice, .closure => 8,
.optional => |o| blk: {
const child_info = self.get(o.child);
if (child_info == .pointer or child_info == .many_pointer or child_info == .function or child_info == .closure)
break :blk 8;
break :blk self.typeAlignBytes(o.child);
},
.@"struct" => |s| blk: {
var max_a: usize = 1;
for (s.fields) |f| {
const fa = self.typeAlignBytes(f.ty);
if (fa > max_a) max_a = fa;
}
break :blk max_a;
},
.@"union", .tagged_union => 8,
.@"enum" => |e| {
if (e.backing_type) |bt| return self.typeAlignBytes(bt);
return 8;
},
.array => |a| self.typeAlignBytes(a.element),
.vector => |v| self.typeAlignBytes(v.element),
.tuple => |t| blk: {
var max_a: usize = 1;
for (t.fields) |f| {
const fa = self.typeAlignBytes(f);
if (fa > max_a) max_a = fa;
}
break :blk max_a;
},
else => 8,
};
}
/// Intern a string into the pool.
pub fn internString(self: *TypeTable, str: []const u8) StringId {
return self.strings.intern(self.alloc, str);
@@ -412,6 +573,7 @@ pub const TypeTable = struct {
.@"struct" => |s| self.getString(s.name),
.@"enum" => |e| self.getString(e.name),
.@"union" => |u| self.getString(u.name),
.tagged_union => |u| self.getString(u.name),
.protocol => |p| self.getString(p.name),
else => "?",
};
@@ -471,6 +633,7 @@ fn hashTypeInfo(h: *std.hash.Wyhash, info: TypeInfo) void {
.@"struct" => |s| h.update(std.mem.asBytes(&s.name)),
.@"enum" => |e| h.update(std.mem.asBytes(&e.name)),
.@"union" => |u| h.update(std.mem.asBytes(&u.name)),
.tagged_union => |u| h.update(std.mem.asBytes(&u.name)),
.protocol => |p| h.update(std.mem.asBytes(&p.name)),
.tuple => |t| {
for (t.fields) |f| h.update(std.mem.asBytes(&f));
@@ -513,6 +676,7 @@ fn typeInfoEql(a: TypeInfo, b: TypeInfo) bool {
.@"struct" => |s| s.name == b.@"struct".name,
.@"enum" => |e| e.name == b.@"enum".name,
.@"union" => |u| u.name == b.@"union".name,
.tagged_union => |u| u.name == b.tagged_union.name,
.protocol => |p| p.name == b.protocol.name,
.tuple => |t| {
const u = b.tuple;

View File

@@ -1,9 +1,6 @@
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;
@@ -24,7 +21,7 @@ pub fn main(init: std.process.Init) !void {
// Parse flags and positional arguments
var input_path: ?[]const u8 = null;
var target_config = sx.codegen.TargetConfig{};
var target_config = sx.target.TargetConfig{};
var lib_paths = std.ArrayList([]const u8).empty;
var show_timing: bool = false;
var explicit_opt: bool = false;
@@ -140,25 +137,12 @@ pub fn main(init: std.process.Init) !void {
}
// Cache MISS — codegen + emit .o to memory (verify skipped: JIT catches errors)
if (USE_IR_PIPELINE) {
comp.generateCodeViaIR() catch { comp.renderErrors(); return; };
} else {
comp.generateCode() catch { comp.renderErrors(); return; };
}
comp.generateCode() catch { comp.renderErrors(); return; };
timer.record("codegen");
timer.mark();
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;
};
});
comp.ir_emitter.?.verifyWithMessage() catch return;
const buf = comp.ir_emitter.?.emitObjectToMemory() catch return;
timer.record("emit");
// Save .o to cache (extract data before JIT takes ownership)
@@ -175,10 +159,25 @@ pub fn main(init: std.process.Init) !void {
defer c_handle.unload(io);
timer.record("c-import");
// dlopen #library dependencies so JIT can resolve foreign symbols
const libs = extractLibraries(allocator, root) catch return;
var lib_handles = std.ArrayList(*anyopaque).empty;
defer {
for (lib_handles.items) |h| _ = std.c.dlclose(h);
}
for (libs) |lib_name| {
if (loadLibrary(allocator, lib_name, target_config.lib_paths)) |handle| {
lib_handles.append(allocator, handle) catch {};
} else {
const e = std.c.dlerror();
if (e) |msg| std.debug.print("warning: could not load library '{s}': {s}\n", .{ lib_name, std.mem.span(msg) });
}
}
// JIT from precompiled object (relocation only, no IR compilation)
sx.llvm_api.initNativeTarget();
timer.mark();
const exit_code = sx.codegen.CodeGen.runJITFromObject(obj_buf) catch {
const exit_code = sx.target.runJITFromObject(obj_buf) catch {
// JIT failed — fall back to AOT
timer.record("jit-fail");
runAOT(allocator, io, path, target_config, &timer, enable_cache) catch return;
@@ -212,7 +211,7 @@ fn compileCForBuild(allocator: std.mem.Allocator, io: std.Io, comp: *sx.core.Com
return try sx.c_import.writeCObjectFiles(allocator, io, obj_bufs);
}
fn parseOptLevel(s: []const u8) ?sx.codegen.TargetConfig.OptLevel {
fn parseOptLevel(s: []const u8) ?sx.target.TargetConfig.OptLevel {
if (std.mem.eql(u8, s, "none") or std.mem.eql(u8, s, "0")) return .none;
if (std.mem.eql(u8, s, "less") or std.mem.eql(u8, s, "1")) return .less;
if (std.mem.eql(u8, s, "default") or std.mem.eql(u8, s, "2")) return .default;
@@ -294,7 +293,7 @@ fn readSource(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8)
return try allocator.dupeZ(u8, source_bytes);
}
fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing) !sx.core.Compilation {
fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing) !sx.core.Compilation {
timer.mark();
const source = try readSource(allocator, io, input_path);
timer.record("read");
@@ -311,20 +310,11 @@ fn compilePipeline(allocator: std.mem.Allocator, io: std.Io, input_path: []const
timer.record("imports");
timer.mark();
if (USE_IR_PIPELINE) {
comp.generateCodeViaIR() catch { comp.renderErrors(); return error.CompileError; };
} else {
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
}
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
timer.record("codegen");
timer.mark();
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; };
}
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
timer.record("verify");
return comp;
@@ -348,18 +338,14 @@ fn dumpSxIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8) !v
std.debug.print("{s}", .{result.items});
}
fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !void {
fn emitIR(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig) !void {
var timer = Timing.init(false);
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer);
defer comp.deinit();
if (USE_IR_PIPELINE) {
comp.ir_emitter.?.printIR();
} else {
comp.cg.?.printIR();
}
comp.ir_emitter.?.printIR();
}
fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig) !void {
fn emitAsm(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig) !void {
var timer = Timing.init(false);
var comp = try compilePipeline(allocator, io, input_path, target_config, &timer);
defer comp.deinit();
@@ -368,21 +354,17 @@ 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);
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; };
}
comp.ir_emitter.?.emitAssembly(asm_path_z.ptr) catch return error.CompileError;
std.debug.print("emitted: {s}\n", .{asm_path});
}
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, show_timing: bool, enable_cache: bool) !void {
fn compile(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, show_timing: bool, enable_cache: bool) !void {
var timer = Timing.init(show_timing);
try compileWithTimer(allocator, io, input_path, output_path, target_config, &timer, enable_cache);
timer.printAll();
}
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, enable_cache: bool) !void {
fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, output_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool) !void {
// Phase A: read + parse + resolveImports (fast: ~0.5ms)
timer.mark();
const source = try readSource(allocator, io, input_path);
@@ -430,29 +412,15 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
} else {
// Cache MISS — full codegen + emit
timer.mark();
if (USE_IR_PIPELINE) {
comp.generateCodeViaIR() catch { comp.renderErrors(); return error.CompileError; };
} else {
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
}
comp.generateCode() catch { comp.renderErrors(); return error.CompileError; };
timer.record("codegen");
timer.mark();
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; };
}
comp.ir_emitter.?.verifyWithMessage() catch return error.CompileError;
timer.record("verify");
timer.mark();
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; };
}
comp.ir_emitter.?.emitObject(obj_path.ptr) catch return error.CompileError;
timer.record("emit");
// Save .o to cache
@@ -471,7 +439,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
// Link (sx .o + C .o files)
timer.mark();
sx.codegen.CodeGen.link(allocator, io, obj_path, c_obj_paths, output_path, libs, target_config) catch {
sx.target.link(allocator, io, obj_path, c_obj_paths, output_path, libs, target_config) catch {
std.debug.print("error: linking failed\n", .{});
return error.CompileError;
};
@@ -489,7 +457,7 @@ fn compileWithTimer(allocator: std.mem.Allocator, io: std.Io, input_path: []cons
}
}
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.codegen.TargetConfig, timer: *Timing, enable_cache: bool) !void {
fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, target_config: sx.target.TargetConfig, timer: *Timing, enable_cache: bool) !void {
const tmp_bin = if (comptime @import("builtin").os.tag == .windows) "sx_run_tmp.exe" else "/tmp/sx_run_tmp";
try compileWithTimer(allocator, io, input_path, tmp_bin, target_config, timer, enable_cache);
defer {
@@ -518,7 +486,7 @@ fn runAOT(allocator: std.mem.Allocator, io: std.Io, input_path: []const u8, targ
// --- Cache helpers ---
fn computeCacheKey(source: [:0]const u8, import_sources: *const std.StringHashMap([:0]const u8), target_config: sx.codegen.TargetConfig) u64 {
fn computeCacheKey(source: [:0]const u8, import_sources: *const std.StringHashMap([:0]const u8), target_config: sx.target.TargetConfig) u64 {
const Wyhash = std.hash.Wyhash;
var key = Wyhash.hash(0, source);
@@ -583,6 +551,40 @@ fn extractLibraries(allocator: std.mem.Allocator, root: *const sx.ast.Node) ![]c
return try libs.toOwnedSlice(allocator);
}
/// Try to dlopen a library by name, searching user paths, host paths, and common naming conventions.
fn loadLibrary(allocator: std.mem.Allocator, lib_name: []const u8, user_lib_paths: []const []const u8) ?*anyopaque {
const is_macos = comptime @import("builtin").os.tag == .macos;
const suffixes: []const []const u8 = if (is_macos) &.{ ".dylib", ".so" } else &.{ ".so", ".dylib" };
// Search paths: user-supplied first, then host defaults
const search_paths = comptime blk: {
var paths: []const []const u8 = &.{};
for (sx.target.host_lib_paths) |p| {
paths = paths ++ .{p};
}
break :blk paths;
};
// Try each path with each suffix
const all_paths = [_][]const []const u8{ user_lib_paths, search_paths };
for (&all_paths) |paths| {
for (paths) |dir| {
for (suffixes) |sfx| {
const full = std.fmt.allocPrintSentinel(allocator, "{s}/lib{s}{s}", .{ dir, lib_name, sfx }, 0) catch continue;
if (std.c.dlopen(full.ptr, .{ .NOW = true })) |h| return h;
}
}
}
// Fallback: bare name (let dlopen search its default paths)
for (suffixes) |sfx| {
const bare = std.fmt.allocPrintSentinel(allocator, "lib{s}{s}", .{ lib_name, sfx }, 0) catch continue;
if (std.c.dlopen(bare.ptr, .{ .NOW = true })) |h| return h;
}
return null;
}
// Simple timing helper — records stage durations and prints a summary table.
const Timing = struct {
const max_entries = 16;

View File

@@ -4,7 +4,7 @@ pub const lexer = @import("lexer.zig");
pub const ast = @import("ast.zig");
pub const parser = @import("parser.zig");
pub const types = @import("types.zig");
pub const codegen = @import("codegen.zig");
pub const target = @import("target.zig");
pub const builtins = @import("builtins.zig");
pub const errors = @import("errors.zig");
pub const sema = @import("sema.zig");

206
src/target.zig Normal file
View File

@@ -0,0 +1,206 @@
const std = @import("std");
const llvm = @import("llvm_api.zig");
const c = llvm.c;
pub const TargetConfig = struct {
/// Target triple (e.g. "aarch64-apple-darwin"). Null = host default.
triple: ?[*:0]const u8 = null,
/// CPU name (e.g. "generic", "apple-m1"). Null = "generic".
cpu: ?[*:0]const u8 = null,
/// CPU features string (e.g. "+avx2"). Null = "".
features: ?[*:0]const u8 = null,
/// Optimization level.
opt_level: OptLevel = .default,
/// Library search paths (-L flags).
lib_paths: []const []const u8 = &.{},
/// Output path override.
output_path: ?[]const u8 = null,
/// Linker command (null = "cc" on Unix, "link.exe" on Windows).
linker: ?[]const u8 = null,
/// Sysroot for cross-compilation (passed as --sysroot to linker).
sysroot: ?[]const u8 = null,
pub const OptLevel = enum {
none,
less,
default,
aggressive,
pub fn toLLVM(self: OptLevel) c.LLVMCodeGenOptLevel {
return switch (self) {
.none => c.LLVMCodeGenLevelNone,
.less => c.LLVMCodeGenLevelLess,
.default => c.LLVMCodeGenLevelDefault,
.aggressive => c.LLVMCodeGenLevelAggressive,
};
}
};
/// Check if target triple indicates aarch64/arm64 (runtime check, not comptime).
pub fn isAarch64(self: TargetConfig) bool {
return self.tripleHasPrefix("aarch64", "arm64");
}
/// Check if target triple indicates x86_64/x86-64.
pub fn isX86_64(self: TargetConfig) bool {
return self.tripleHasPrefix("x86_64", "x86-64");
}
/// Check if target triple indicates Windows (contains "windows" or "win32").
pub fn isWindows(self: TargetConfig) bool {
return self.tripleContains("windows") or self.tripleContains("win32");
}
fn tripleHasPrefix(self: TargetConfig, prefix1: []const u8, prefix2: []const u8) bool {
if (self.triple) |t| {
const span = std.mem.span(t);
return std.mem.startsWith(u8, span, prefix1) or std.mem.startsWith(u8, span, prefix2);
}
const dt = c.LLVMGetDefaultTargetTriple();
defer c.LLVMDisposeMessage(dt);
const span = std.mem.span(dt);
return std.mem.startsWith(u8, span, prefix1) or std.mem.startsWith(u8, span, prefix2);
}
fn tripleContains(self: TargetConfig, needle: []const u8) bool {
if (self.triple) |t| {
return std.mem.indexOf(u8, std.mem.span(t), needle) != null;
}
const dt = c.LLVMGetDefaultTargetTriple();
defer c.LLVMDisposeMessage(dt);
return std.mem.indexOf(u8, std.mem.span(dt), needle) != null;
}
pub fn getCpu(self: TargetConfig) [*:0]const u8 {
return self.cpu orelse "generic";
}
pub fn getFeatures(self: TargetConfig) [*:0]const u8 {
return self.features orelse "";
}
pub fn getLinker(self: TargetConfig) []const u8 {
return self.linker orelse "cc";
}
};
/// Execute a precompiled object file in-process using LLVM's ORC JIT.
/// Takes ownership of obj_buf. Returns the exit code from main().
pub fn runJITFromObject(obj_buf: c.LLVMMemoryBufferRef) !u8 {
// Create LLJIT with default builder (no custom TM needed — .o is precompiled)
var jit: c.LLVMOrcLLJITRef = null;
var err = c.LLVMOrcCreateLLJIT(&jit, null);
if (err != null) {
const msg = c.LLVMGetErrorMessage(err);
defer c.LLVMDisposeErrorMessage(msg);
std.debug.print("JIT error: {s}\n", .{std.mem.span(msg)});
return error.CompileError;
}
defer _ = c.LLVMOrcDisposeLLJIT(jit);
// Add process symbols so JIT can find libc (printf, etc.)
const jd = c.LLVMOrcLLJITGetMainJITDylib(jit);
const prefix = c.LLVMOrcLLJITGetGlobalPrefix(jit);
var gen: c.LLVMOrcDefinitionGeneratorRef = null;
err = c.LLVMOrcCreateDynamicLibrarySearchGeneratorForProcess(&gen, prefix, null, null);
if (err != null) {
const msg = c.LLVMGetErrorMessage(err);
defer c.LLVMDisposeErrorMessage(msg);
std.debug.print("JIT symbol gen error: {s}\n", .{std.mem.span(msg)});
return error.CompileError;
}
c.LLVMOrcJITDylibAddGenerator(jd, gen);
// Add precompiled object file (transfers ownership of obj_buf)
err = c.LLVMOrcLLJITAddObjectFile(jit, jd, obj_buf);
if (err != null) {
const msg = c.LLVMGetErrorMessage(err);
defer c.LLVMDisposeErrorMessage(msg);
std.debug.print("JIT add object error: {s}\n", .{std.mem.span(msg)});
return error.CompileError;
}
// Look up the "main" function
var main_addr: c.LLVMOrcExecutorAddress = 0;
err = c.LLVMOrcLLJITLookup(jit, &main_addr, "main");
if (err != null) {
const msg = c.LLVMGetErrorMessage(err);
defer c.LLVMDisposeErrorMessage(msg);
std.debug.print("JIT lookup error: {s}\n", .{std.mem.span(msg)});
return error.CompileError;
}
// Cast to function pointer and call
const main_fn: *const fn () callconv(.c) i32 = @ptrFromInt(main_addr);
const result = main_fn();
return if (result >= 0 and result <= 255) @intCast(result) else 1;
}
pub fn link(allocator: std.mem.Allocator, io: std.Io, output_obj: []const u8, extra_objects: []const []const u8, output_bin: []const u8, libraries: []const []const u8, target_config: TargetConfig) !void {
var argv = std.ArrayList([]const u8).empty;
if (target_config.isWindows()) {
// Windows: MSVC-style linker flags
const linker = target_config.linker orelse "link.exe";
try argv.appendSlice(allocator, &.{ linker, output_obj });
for (extra_objects) |eo| try argv.append(allocator, eo);
try argv.append(allocator, try std.fmt.allocPrint(allocator, "/OUT:{s}", .{output_bin}));
for (target_config.lib_paths) |lp| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "/LIBPATH:{s}", .{lp}));
}
for (libraries) |lib| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "{s}.lib", .{lib}));
}
} else {
// Unix: cc-style linker flags
try argv.appendSlice(allocator, &.{ target_config.getLinker(), output_obj, "-o", output_bin });
for (extra_objects) |eo| try argv.append(allocator, eo);
if (target_config.sysroot) |sr| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "--sysroot={s}", .{sr}));
}
// User-supplied library paths first
for (target_config.lib_paths) |lp| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{lp}));
}
// Auto-detect host OS library paths when linking foreign libraries
if (libraries.len > 0 and target_config.triple == null) {
for (host_lib_paths) |path| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-L{s}", .{path}));
}
}
for (libraries) |lib| {
try argv.append(allocator, try std.fmt.allocPrint(allocator, "-l{s}", .{lib}));
}
}
const argv_slice = try argv.toOwnedSlice(allocator);
var child = std.process.spawn(io, .{
.argv = argv_slice,
}) catch return error.LinkError;
const result = child.wait(io) catch return error.LinkError;
if (result != .exited) return error.LinkError;
if (result.exited != 0) return error.LinkError;
}
/// Common library paths for the host OS, computed at comptime.
pub const host_lib_paths = blk: {
const builtin = @import("builtin");
var paths: []const []const u8 = &.{};
if (builtin.os.tag == .macos) {
if (builtin.cpu.arch == .aarch64) {
// Apple Silicon Homebrew
paths = &.{ "/opt/homebrew/lib", "/usr/local/lib" };
} else {
// Intel Mac Homebrew
paths = &.{"/usr/local/lib"};
}
} else if (builtin.os.tag == .linux) {
paths = &.{ "/usr/local/lib", "/usr/lib" };
}
break :blk paths;
};

View File

@@ -410,6 +410,9 @@ opt field set: 42
opt param a: 42
opt param b: 0
opt param 7: 7
opt reassign: 42.500000
opt compute assign: 15.000000
opt re-null: 99.000000
generic opt 1: 5
generic opt 2: 7
generic opt 3: null