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

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;
};