ir done'ish
This commit is contained in:
@@ -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 {
|
||||
|
||||
11905
src/codegen.zig
11905
src/codegen.zig
File diff suppressed because it is too large
Load Diff
2752
src/comptime.zig
2752
src/comptime.zig
File diff suppressed because it is too large
Load Diff
22
src/core.zig
22
src/core.zig
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, ¶m_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, ¶m_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, ¶m_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 ─────────────────────────────────────
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
766
src/ir/lower.zig
766
src/ir/lower.zig
File diff suppressed because it is too large
Load Diff
@@ -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 ────────────────────────────────────────────────
|
||||
|
||||
@@ -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('*');
|
||||
|
||||
@@ -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);
|
||||
|
||||
172
src/ir/types.zig
172
src/ir/types.zig
@@ -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;
|
||||
|
||||
138
src/main.zig
138
src/main.zig
@@ -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;
|
||||
|
||||
@@ -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
206
src/target.zig
Normal 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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user