const std = @import("std"); const Allocator = std.mem.Allocator; const llvm = @import("../llvm_api.zig"); const c = llvm.c; 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; const TypeTable = ir_types.TypeTable; const StringId = ir_types.StringId; const ir_inst = @import("inst.zig"); const Ref = ir_inst.Ref; const BlockId = ir_inst.BlockId; const FuncId = ir_inst.FuncId; const GlobalId = ir_inst.GlobalId; const Inst = ir_inst.Inst; const Op = ir_inst.Op; const Block = ir_inst.Block; const Function = ir_inst.Function; const Global = ir_inst.Global; const ir_module = @import("module.zig"); const Module = ir_module.Module; const interp_mod = @import("interp.zig"); const Interpreter = interp_mod.Interpreter; const Value = interp_mod.Value; // ── LLVMEmitter ───────────────────────────────────────────────────────── // Emits LLVM IR from an IR Module. This is the Phase 3 replacement for // the AST-based codegen. pub const LLVMEmitter = struct { // LLVM handles context: c.LLVMContextRef, llvm_module: c.LLVMModuleRef, builder: c.LLVMBuilderRef, target_machine: ?c.LLVMTargetMachineRef, // IR Module being emitted ir_mod: *const Module, // Allocator for temporary bookkeeping alloc: Allocator, // Maps IR Ref → LLVM Value for the current function ref_map: std.AutoHashMap(u32, c.LLVMValueRef), // Maps IR FuncId → LLVM function value func_map: std.AutoHashMap(u32, c.LLVMValueRef), // Maps IR GlobalId → LLVM global value global_map: std.AutoHashMap(u32, c.LLVMValueRef), // Maps (func_idx, block_idx) → LLVM BasicBlock block_map: std.AutoHashMap(u64, c.LLVMBasicBlockRef), // Cached LLVM types cached_i1: c.LLVMTypeRef, cached_i8: c.LLVMTypeRef, cached_i16: c.LLVMTypeRef, cached_i32: c.LLVMTypeRef, cached_i64: c.LLVMTypeRef, cached_f32: c.LLVMTypeRef, cached_f64: c.LLVMTypeRef, cached_ptr: c.LLVMTypeRef, cached_void: c.LLVMTypeRef, // Current ref counter — tracks which Ref index we're emitting within a function ref_counter: u32 = 0, // Pending PHI nodes to fixup after all blocks in a function are emitted pending_phis: std.ArrayList(PendingPhi), // Whether the current function being emitted is "main" (needs i32 return for JIT) current_func_is_main: bool = false, current_func_idx: u32 = 0, // Cached composite types string_struct_type: ?c.LLVMTypeRef, any_struct_type: ?c.LLVMTypeRef, closure_struct_type: ?c.LLVMTypeRef, // Cached field name arrays for reflection (TypeId → LLVM global) field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef), // Target configuration (stored for ABI decisions during emission) target_config: TargetConfig, // Build configuration accumulated from #run blocks build_config: interp_mod.BuildConfig, const PendingPhi = struct { phi: c.LLVMValueRef, block_id: BlockId, // the block this phi belongs to param_index: u32, }; pub fn init(alloc: Allocator, ir_mod: *const Module, module_name: [*:0]const u8, target_config: TargetConfig) LLVMEmitter { // Initialize LLVM targets if (target_config.triple == null) { llvm.initNativeTarget(); } else { llvm.initAllTargets(); } const ctx = c.LLVMContextCreate(); const llvm_module = c.LLVMModuleCreateWithNameInContext(module_name, ctx); const builder = c.LLVMCreateBuilderInContext(ctx); // Set target triple const triple_owned = target_config.triple == null; const triple = target_config.triple orelse c.LLVMGetDefaultTargetTriple(); defer if (triple_owned) c.LLVMDisposeMessage(@constCast(triple)); c.LLVMSetTarget(llvm_module, triple); // Create target machine and set data layout var target: c.LLVMTargetRef = null; var err_msg: [*c]u8 = null; var tm: c.LLVMTargetMachineRef = null; if (c.LLVMGetTargetFromTriple(triple, &target, &err_msg) == 0) { tm = c.LLVMCreateTargetMachine( target, triple, target_config.getCpu(), target_config.getFeatures(), target_config.opt_level.toLLVM(), c.LLVMRelocPIC, c.LLVMCodeModelDefault, ); const dl = c.LLVMCreateTargetDataLayout(tm); c.LLVMSetModuleDataLayout(llvm_module, dl); c.LLVMDisposeTargetData(dl); } else { if (err_msg != null) c.LLVMDisposeMessage(err_msg); } return .{ .context = ctx, .llvm_module = llvm_module, .builder = builder, .target_machine = tm, .ir_mod = ir_mod, .alloc = alloc, .ref_map = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), .func_map = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), .global_map = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), .block_map = std.AutoHashMap(u64, c.LLVMBasicBlockRef).init(alloc), .pending_phis = std.ArrayList(PendingPhi).empty, .cached_i1 = c.LLVMInt1TypeInContext(ctx), .cached_i8 = c.LLVMInt8TypeInContext(ctx), .cached_i16 = c.LLVMInt16TypeInContext(ctx), .cached_i32 = c.LLVMInt32TypeInContext(ctx), .cached_i64 = c.LLVMInt64TypeInContext(ctx), .cached_f32 = c.LLVMFloatTypeInContext(ctx), .cached_f64 = c.LLVMDoubleTypeInContext(ctx), .cached_ptr = c.LLVMPointerTypeInContext(ctx, 0), .cached_void = c.LLVMVoidTypeInContext(ctx), .string_struct_type = null, .any_struct_type = null, .closure_struct_type = null, .field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), .target_config = target_config, .build_config = .{}, }; } pub fn deinit(self: *LLVMEmitter) void { self.build_config.deinit(self.alloc); self.ref_map.deinit(); self.func_map.deinit(); self.field_name_arrays.deinit(); self.global_map.deinit(); self.block_map.deinit(); if (self.target_machine) |tm| c.LLVMDisposeTargetMachine(tm); c.LLVMDisposeBuilder(self.builder); c.LLVMDisposeModule(self.llvm_module); c.LLVMContextDispose(self.context); } // ── Top-level emit ────────────────────────────────────────────── pub fn emit(self: *LLVMEmitter) void { // Pass 0: Declare and initialize globals self.emitGlobals(); // Pass 0.5: Run comptime side-effect functions (#run expr; at top level) self.runComptimeSideEffects(); // Pass 1: Declare all functions (so calls can reference them) for (self.ir_mod.functions.items, 0..) |func, i| { self.declareFunction(&func, @intCast(i)); } // Pass 1.5: Initialize vtable globals (needs function declarations from Pass 1) self.initVtableGlobals(); // Pass 2: Emit function bodies for (self.ir_mod.functions.items, 0..) |func, i| { 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 { // Skip for wasm32: 4-byte pointers vs IR's assumed 8-byte, // so struct sizes will differ. LLVM handles emission correctly. if (self.target_config.isWasm32()) return; 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). /// These are functions marked `is_comptime = true` with void return that /// aren't associated with any global. They produce compile-time output. fn runComptimeSideEffects(self: *LLVMEmitter) void { for (self.ir_mod.functions.items, 0..) |func, i| { if (!func.is_comptime or func.ret != .void) continue; // Skip functions that are global initializers (already run by emitGlobals) var is_global_init = false; for (self.ir_mod.globals.items) |global| { if (global.comptime_func) |gf| { if (@intFromEnum(gf) == i) { is_global_init = true; break; } } } if (is_global_init) continue; // Run the side-effect function via interpreter const func_id = ir_inst.FuncId.fromIndex(@intCast(i)); var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; _ = interp_inst.call(func_id, &.{}) catch {}; // Write comptime output to stderr (same as old comptime VM) if (interp_inst.output.items.len > 0) { std.debug.print("{s}", .{interp_inst.output.items}); } interp_inst.deinit(); } } fn emitGlobals(self: *LLVMEmitter) void { for (self.ir_mod.globals.items, 0..) |global, i| { const name = self.ir_mod.types.getString(global.name); const llvm_ty = self.toLLVMType(global.ty); const name_z = self.alloc.dupeZ(u8, name) catch continue; defer self.alloc.free(name_z); const llvm_global = c.LLVMAddGlobal(self.llvm_module, llvm_ty, name_z.ptr); // Extern globals (` : #foreign;`) resolve at link time // to a libSystem / framework symbol — no initializer, default linkage. if (global.is_extern) { c.LLVMSetLinkage(llvm_global, c.LLVMExternalLinkage); self.global_map.put(@intCast(i), llvm_global) catch {}; continue; } c.LLVMSetLinkage(llvm_global, c.LLVMInternalLinkage); // Evaluate comptime initializer if present if (global.comptime_func) |func_id| { var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; const result = interp_inst.call(func_id, &.{}) catch .void_val; const init_val = self.valueToLLVMConst(result, llvm_ty); c.LLVMSetInitializer(llvm_global, init_val); } else if (global.init_val) |iv| { const init_val = switch (iv) { .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)), .aggregate => |agg| self.emitConstAggregate(agg, llvm_ty), .vtable => c.LLVMConstNull(llvm_ty), // placeholder — initialized in initVtableGlobals after function declarations else => c.LLVMConstNull(llvm_ty), }; c.LLVMSetInitializer(llvm_global, init_val); } else { c.LLVMSetInitializer(llvm_global, c.LLVMConstNull(llvm_ty)); } self.global_map.put(@intCast(i), llvm_global) catch {}; } } /// Initialize vtable globals with function pointer constants. /// Must run after Pass 1 (function declarations) so func_map is populated. fn initVtableGlobals(self: *LLVMEmitter) void { for (self.ir_mod.globals.items, 0..) |global, i| { const iv = global.init_val orelse continue; const func_ids = switch (iv) { .vtable => |ids| ids, else => continue, }; const llvm_global = self.global_map.get(@intCast(i)) orelse continue; const llvm_ty = self.toLLVMType(global.ty); // Build constant struct of function pointers var field_vals = std.ArrayList(c.LLVMValueRef).empty; defer field_vals.deinit(self.alloc); for (func_ids) |fid| { const llvm_func = self.func_map.get(fid.index()) orelse { field_vals.append(self.alloc, c.LLVMConstNull(self.cached_ptr)) catch unreachable; continue; }; field_vals.append(self.alloc, llvm_func) catch unreachable; } const init_val = c.LLVMConstNamedStruct(llvm_ty, field_vals.items.ptr, @intCast(field_vals.items.len)); c.LLVMSetInitializer(llvm_global, init_val); c.LLVMSetGlobalConstant(llvm_global, 1); } } fn valueToLLVMConst(self: *LLVMEmitter, val: Value, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { _ = self; return switch (val) { .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), else => c.LLVMConstNull(llvm_ty), }; } // ── Function declaration ──────────────────────────────────────── fn declareFunction(self: *LLVMEmitter, func: *const Function, func_idx: u32) void { const name = self.ir_mod.types.getString(func.name); // Skip builtins that are declared via getOrDeclare* with correct C-compatible types. // The IR lowering creates extern stubs with IR types (e.g. memset → void return), // but the C ABI may differ (memset returns ptr). Let getOrDeclare* handle these. if (func.is_extern and isBuiltinLibcName(name)) { // Still register in func_map so call resolution works const builtin_fn = self.getOrDeclareBuiltinByName(name); if (builtin_fn) |bf| { self.func_map.put(func_idx, bf) catch unreachable; return; } } const is_main = std.mem.eql(u8, name, "main"); // main always returns i32 at the LLVM level (JIT expects it) const raw_ret_ty = self.toLLVMType(func.ret); const needs_c_abi = func.is_extern or func.call_conv == .c; const ret_ty = if (is_main) self.cached_i32 else if (needs_c_abi) self.abiCoerceParamType(func.ret, raw_ret_ty) else raw_ret_ty; // Build parameter types — apply C ABI coercion for foreign/callconv(.c) 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| { const llvm_ty = self.toLLVMType(param.ty); param_types[j] = if (needs_c_abi) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty; } const fn_type = c.LLVMFunctionType(ret_ty, param_types.ptr, param_count, 0); const name_z = self.alloc.dupeZ(u8, name) catch unreachable; defer self.alloc.free(name_z); const llvm_func = c.LLVMAddFunction(self.llvm_module, name_z.ptr, fn_type); // Set linkage switch (func.linkage) { .external => c.LLVMSetLinkage(llvm_func, c.LLVMExternalLinkage), .internal => c.LLVMSetLinkage(llvm_func, c.LLVMInternalLinkage), .private => c.LLVMSetLinkage(llvm_func, c.LLVMPrivateLinkage), } // Set calling convention if (func.call_conv == .c) { c.LLVMSetFunctionCallConv(llvm_func, c.LLVMCCallConv); } // Add frame-pointer and nounwind attributes for correct ARM64 codegen { const fp_kind = "frame-pointer"; const fp_val = "all"; const fp_attr = c.LLVMCreateStringAttribute( self.context, fp_kind.ptr, @intCast(fp_kind.len), fp_val.ptr, @intCast(fp_val.len), ); const func_idx_attr: c.LLVMAttributeIndex = @bitCast(@as(i32, -1)); c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, fp_attr); // Add nounwind const nounwind_id = c.LLVMGetEnumAttributeKindForName("nounwind", 8); const nounwind_attr = c.LLVMCreateEnumAttribute(self.context, nounwind_id, 0); c.LLVMAddAttributeAtIndex(llvm_func, func_idx_attr, nounwind_attr); } // Apple ARM64 ABI for >16B non-HFA composites: pass by reference // via a pointer in the next int register (NOT via LLVM's `byval` // attribute, which lowers the struct on the stack — incompatible // with what `clang` emits and what foreign C callees expect). // abiCoerceParamType returned `ptr` for these slots, so the formal // param IS a plain pointer; the prologue loads the struct back. self.func_map.put(func_idx, llvm_func) catch unreachable; } // ── Function body emission ────────────────────────────────────── fn emitFunction(self: *LLVMEmitter, func: *const Function, func_idx: u32) void { const llvm_func = self.func_map.get(func_idx) orelse unreachable; const name = self.ir_mod.types.getString(func.name); self.current_func_is_main = std.mem.eql(u8, name, "main"); self.current_func_idx = func_idx; // Clear ref_map and pre-map parameter refs self.ref_map.clearRetainingCapacity(); self.ref_counter = 0; // Refs 0..N-1 are function parameters (matching the IR convention) for (0..func.params.len) |pi| { const param_val = c.LLVMGetParam(llvm_func, @intCast(pi)); self.mapRef(param_val); } // Create all basic blocks first (so branches can reference them) for (func.blocks.items, 0..) |block, bi| { const block_name = self.ir_mod.types.getString(block.name); const block_name_z = self.alloc.dupeZ(u8, block_name) catch unreachable; defer self.alloc.free(block_name_z); const bb = c.LLVMAppendBasicBlockInContext(self.context, llvm_func, block_name_z.ptr); const block_key = makeBlockKey(func_idx, @intCast(bi)); self.block_map.put(block_key, bb) catch unreachable; } // byval params arrive as `ptr` in LLVM but the IR body expects struct values. // At entry, load each byval param into a struct SSA value and re-map its ref. const needs_c_abi = func.is_extern or func.call_conv == .c; if (needs_c_abi and func.blocks.items.len > 0) { const entry_key = makeBlockKey(func_idx, 0); const entry_bb = self.block_map.get(entry_key) orelse unreachable; c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); for (func.params, 0..) |param, pi| { const raw_llvm_ty = self.toLLVMType(param.ty); if (self.needsByval(param.ty, raw_llvm_ty)) { const ptr_val = c.LLVMGetParam(llvm_func, @intCast(pi)); const loaded = c.LLVMBuildLoad2(self.builder, raw_llvm_ty, ptr_val, "byval.load"); self.ref_map.put(@intCast(pi), loaded) catch unreachable; } } } // Clear pending phis for this function self.pending_phis.clearRetainingCapacity(); // Emit instructions for each block — use first_ref to sync ref numbering for (func.blocks.items, 0..) |block, bi| { const block_key = makeBlockKey(func_idx, @intCast(bi)); const bb = self.block_map.get(block_key) orelse unreachable; c.LLVMPositionBuilderAtEnd(self.builder, bb); // Reset ref_counter to this block's actual starting ref // (blocks may not be in emission order due to nested control flow) self.ref_counter = block.first_ref; for (block.insts.items, 0..) |instruction, inst_i| { _ = inst_i; self.emitInst(&instruction, func_idx); } } // Fixup PHI nodes: scan all blocks for branches that pass args self.fixupPhiNodes(func, func_idx); } /// After emitting all blocks, fill in PHI incoming values from branch args. fn fixupPhiNodes(self: *LLVMEmitter, func: *const Function, func_idx: u32) void { if (self.pending_phis.items.len == 0) return; for (func.blocks.items, 0..) |block, bi| { const src_key = makeBlockKey(func_idx, @intCast(bi)); const src_bb = self.block_map.get(src_key) orelse continue; for (block.insts.items) |instruction| { switch (instruction.op) { .br => |branch| { self.addPhiIncoming(branch.target, branch.args, src_bb); }, .cond_br => |cb| { self.addPhiIncoming(cb.then_target, cb.then_args, src_bb); self.addPhiIncoming(cb.else_target, cb.else_args, src_bb); }, .switch_br => |sw| { for (sw.cases) |case| { self.addPhiIncoming(case.target, case.args, src_bb); } self.addPhiIncoming(sw.default, sw.default_args, src_bb); }, else => {}, } } } } fn addPhiIncoming(self: *LLVMEmitter, target: BlockId, args: []const Ref, src_bb: c.LLVMBasicBlockRef) void { for (args, 0..) |arg, pi| { const val = self.resolveRef(arg) orelse continue; // Find the matching pending phi for (self.pending_phis.items) |pp| { if (pp.block_id.index() == target.index() and pp.param_index == pi) { var incoming_vals = [1]c.LLVMValueRef{val}; var incoming_bbs = [1]c.LLVMBasicBlockRef{src_bb}; c.LLVMAddIncoming(pp.phi, &incoming_vals, &incoming_bbs, 1); break; } } } } // ── Instruction emission ──────────────────────────────────────── fn emitInst(self: *LLVMEmitter, instruction: *const Inst, func_idx: u32) void { switch (instruction.op) { // ── Constants ─────────────────────────────────────────── .const_int => |val| { const ty = self.toLLVMType(instruction.ty); const kind = c.LLVMGetTypeKind(ty); const llvm_val = if (kind == c.LLVMIntegerTypeKind) c.LLVMConstInt(ty, @bitCast(val), 1) else if (kind == c.LLVMPointerTypeKind) c.LLVMConstNull(ty) else // void or other non-integer type: emit i64 0 as unused placeholder c.LLVMConstInt(c.LLVMInt64TypeInContext(self.context), 0, 0); self.mapRef(llvm_val); }, .const_float => |val| { const ty = self.toLLVMType(instruction.ty); const llvm_val = c.LLVMConstReal(ty, val); self.mapRef(llvm_val); }, .const_bool => |val| { const llvm_val = c.LLVMConstInt(self.cached_i1, @intFromBool(val), 0); self.mapRef(llvm_val); }, .const_string => |str_id| { const str = self.ir_mod.types.getString(str_id); const llvm_val = self.emitStringConstant(str); self.mapRef(llvm_val); }, .const_null => { const ty = if (instruction.ty == .void) self.cached_ptr else self.toLLVMType(instruction.ty); const llvm_val = c.LLVMConstNull(ty); self.mapRef(llvm_val); }, .const_undef => { if (instruction.ty == .void) { // void has no value — map to undef i64 as placeholder self.mapRef(c.LLVMGetUndef(self.cached_i64)); } else { const ty = self.toLLVMType(instruction.ty); const llvm_val = c.LLVMGetUndef(ty); self.mapRef(llvm_val); } }, // ── Arithmetic ───────────────────────────────────────── .add => |bin| { var lhs = self.resolveRef(bin.lhs); var rhs = self.resolveRef(bin.rhs); self.matchBinOpTypes(&lhs, &rhs, instruction.ty); const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types); const result = if (is_float) c.LLVMBuildFAdd(self.builder, lhs, rhs, "fadd") else c.LLVMBuildAdd(self.builder, lhs, rhs, "add"); self.mapRef(result); }, .sub => |bin| { var lhs = self.resolveRef(bin.lhs); var rhs = self.resolveRef(bin.rhs); self.matchBinOpTypes(&lhs, &rhs, instruction.ty); const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types); const result = if (is_float) c.LLVMBuildFSub(self.builder, lhs, rhs, "fsub") else c.LLVMBuildSub(self.builder, lhs, rhs, "sub"); self.mapRef(result); }, .mul => |bin| { var lhs = self.resolveRef(bin.lhs); var rhs = self.resolveRef(bin.rhs); self.matchBinOpTypes(&lhs, &rhs, instruction.ty); const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types); const result = if (is_float) c.LLVMBuildFMul(self.builder, lhs, rhs, "fmul") else c.LLVMBuildMul(self.builder, lhs, rhs, "mul"); self.mapRef(result); }, .div => |bin| { var lhs = self.resolveRef(bin.lhs); var rhs = self.resolveRef(bin.rhs); self.matchBinOpTypes(&lhs, &rhs, instruction.ty); const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types); const result = if (is_float) c.LLVMBuildFDiv(self.builder, lhs, rhs, "fdiv") else if (isSignedType(instruction.ty)) c.LLVMBuildSDiv(self.builder, lhs, rhs, "sdiv") else c.LLVMBuildUDiv(self.builder, lhs, rhs, "udiv"); self.mapRef(result); }, .mod => |bin| { var lhs = self.resolveRef(bin.lhs); var rhs = self.resolveRef(bin.rhs); self.matchBinOpTypes(&lhs, &rhs, instruction.ty); const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types); const result = if (is_float) c.LLVMBuildFRem(self.builder, lhs, rhs, "fmod") else if (isSignedType(instruction.ty)) c.LLVMBuildSRem(self.builder, lhs, rhs, "srem") else c.LLVMBuildURem(self.builder, lhs, rhs, "urem"); self.mapRef(result); }, .neg => |un| { const operand = self.resolveRef(un.operand); const is_float = isFloatOrVecFloat(instruction.ty, &self.ir_mod.types); const result = if (is_float) c.LLVMBuildFNeg(self.builder, operand, "fneg") else c.LLVMBuildNeg(self.builder, operand, "neg"); self.mapRef(result); }, // ── Bitwise ──────────────────────────────────────────── .bit_and => |bin| { 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| { 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| { 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| { const operand = self.resolveRef(un.operand); self.mapRef(c.LLVMBuildNot(self.builder, operand, "not")); }, .shl => |bin| { 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| { 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") else c.LLVMBuildLShr(self.builder, lhs, rhs, "lshr"); self.mapRef(result); }, // ── Comparisons ─────────────────────────────────────── .cmp_eq => |bin| self.emitCmp(bin, instruction.ty, c.LLVMIntEQ, c.LLVMRealOEQ), .cmp_ne => |bin| self.emitCmp(bin, instruction.ty, c.LLVMIntNE, c.LLVMRealONE), .cmp_lt => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSLT, c.LLVMIntULT, c.LLVMRealOLT), .cmp_le => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSLE, c.LLVMIntULE, c.LLVMRealOLE), .cmp_gt => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSGT, c.LLVMIntUGT, c.LLVMRealOGT), .cmp_ge => |bin| self.emitCmpOrdered(bin, instruction.ty, c.LLVMIntSGE, c.LLVMIntUGE, c.LLVMRealOGE), .str_eq => |bin| self.emitStrCmp(bin, true), .str_ne => |bin| self.emitStrCmp(bin, false), // ── Logical ─────────────────────────────────────────── .bool_and => |bin| { const lhs = self.resolveRef(bin.lhs); const rhs = self.resolveRef(bin.rhs); self.mapRef(c.LLVMBuildAnd(self.builder, lhs, rhs, "land")); }, .bool_or => |bin| { const lhs = self.resolveRef(bin.lhs); const rhs = self.resolveRef(bin.rhs); self.mapRef(c.LLVMBuildOr(self.builder, lhs, rhs, "lor")); }, .bool_not => |un| { const operand = self.resolveRef(un.operand); self.mapRef(c.LLVMBuildNot(self.builder, operand, "lnot")); }, // ── Memory ──────────────────────────────────────────── .alloca => |elem_ty| { const llvm_ty = self.toLLVMType(elem_ty); const result = c.LLVMBuildAlloca(self.builder, llvm_ty, "alloca"); self.mapRef(result); }, .load => |un| { const ptr = self.resolveRef(un.operand); const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr)); if (ptr_kind == c.LLVMPointerTypeKind and instruction.ty != .void) { const llvm_ty = self.toLLVMType(instruction.ty); const result = c.LLVMBuildLoad2(self.builder, llvm_ty, ptr, "load"); self.mapRef(result); } else { self.mapRef(c.LLVMGetUndef(self.toLLVMType(if (instruction.ty == .void) .s64 else instruction.ty))); } }, .store => |st| { const ptr = self.resolveRef(st.ptr); var val = self.resolveRef(st.val); // Guard: don't store void types or store to non-pointer const ptr_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(ptr)); const val_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(val)); if (ptr_kind == c.LLVMPointerTypeKind and val_kind != c.LLVMVoidTypeKind) { // Coerce value to match the IR-declared pointer target type. // E.g. storing i64 to *i8 (from index_gep on string) needs truncation. // // Only unwrap .pointer (from index_gep/alloca: *element → element). // Never unwrap .many_pointer — it only appears as struct_gep field // value types (e.g., [*]BigNode), where unwrapping to the element // type gives a wrong store size (stores BigNode-sized instead of ptr). if (self.getRefIRType(st.ptr)) |ptr_ir_ty| { const pointee_info = self.ir_mod.types.get(ptr_ir_ty); const target_ty: ?c.LLVMTypeRef = switch (pointee_info) { .pointer => |p| self.toLLVMType(p.pointee), else => null, }; if (target_ty) |tt| { val = self.coerceArg(val, tt); } } _ = c.LLVMBuildStore(self.builder, val, ptr); } self.advanceRefCounter(); }, .heap_alloc => |un| { // malloc(size) → *void const size = self.coerceArg(self.resolveRef(un.operand), self.sizeType()); const malloc_fn = self.getOrDeclareMalloc(); var args = [_]c.LLVMValueRef{size}; const result = c.LLVMBuildCall2( self.builder, self.getMallocType(), malloc_fn, &args, 1, "heap", ); self.mapRef(result); }, .heap_free => |un| { // free(ptr) const ptr = self.resolveRef(un.operand); const free_fn = self.getOrDeclareFree(); var args = [_]c.LLVMValueRef{ptr}; _ = c.LLVMBuildCall2( self.builder, self.getFreeType(), free_fn, &args, 1, "", ); self.advanceRefCounter(); }, // ── Globals ─────────────────────────────────────────── .global_get => |gid| { const llvm_global = self.global_map.get(gid.index()) orelse { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); return; }; const llvm_ty = self.toLLVMType(instruction.ty); self.mapRef(c.LLVMBuildLoad2(self.builder, llvm_ty, llvm_global, "gload")); }, .global_addr => |gid| { const llvm_global = self.global_map.get(gid.index()) orelse { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); return; }; // Return the global's address directly (no load) self.mapRef(llvm_global); }, .func_ref => |fid| { // Produce a reference to the function as a function pointer value if (self.func_map.get(@intFromEnum(fid))) |llvm_func| { self.mapRef(llvm_func); } else { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); } }, .global_set => |gs| { const llvm_global = self.global_map.get(gs.global.index()) orelse { self.advanceRefCounter(); return; }; const val = self.resolveRef(gs.value); _ = c.LLVMBuildStore(self.builder, val, llvm_global); self.advanceRefCounter(); }, // ── Conversions ─────────────────────────────────────── .widen => |conv| { const operand = self.resolveRef(conv.operand); const to_ty = self.toLLVMType(conv.to); const result = self.emitConversion(operand, conv.from, conv.to, to_ty); self.mapRef(result); }, .narrow => |conv| { const operand = self.resolveRef(conv.operand); const to_ty = self.toLLVMType(conv.to); const result = self.emitConversion(operand, conv.from, conv.to, to_ty); self.mapRef(result); }, .bitcast => |conv| { const operand = self.resolveRef(conv.operand); const to_ty = self.toLLVMType(conv.to); self.mapRef(c.LLVMBuildBitCast(self.builder, operand, to_ty, "bitcast")); }, .int_to_float => |conv| { const operand = self.resolveRef(conv.operand); const to_ty = self.toLLVMType(conv.to); const result = if (isSignedType(conv.from)) c.LLVMBuildSIToFP(self.builder, operand, to_ty, "sitofp") else c.LLVMBuildUIToFP(self.builder, operand, to_ty, "uitofp"); self.mapRef(result); }, .float_to_int => |conv| { const operand = self.resolveRef(conv.operand); const to_ty = self.toLLVMType(conv.to); const result = if (isSignedType(conv.to)) c.LLVMBuildFPToSI(self.builder, operand, to_ty, "fptosi") else c.LLVMBuildFPToUI(self.builder, operand, to_ty, "fptoui"); self.mapRef(result); }, // ── Pointer ops ─────────────────────────────────────── .addr_of => |un| { // addr_of returns the pointer directly (the operand is already a ptr from alloca) self.mapRef(self.resolveRef(un.operand)); }, .deref => |un| { const ptr = self.resolveRef(un.operand); const llvm_ty = self.toLLVMType(instruction.ty); self.mapRef(c.LLVMBuildLoad2(self.builder, llvm_ty, ptr, "deref")); }, // ── Calls ───────────────────────────────────────────── .call => |call_op| { // Evaluate comptime functions at compile time const callee_func = &self.ir_mod.functions.items[call_op.callee.index()]; if (callee_func.is_comptime and call_op.args.len == 0) { var interp_inst = Interpreter.init(self.ir_mod, self.alloc); interp_inst.build_config = &self.build_config; defer interp_inst.deinit(); if (interp_inst.call(call_op.callee, &.{})) |result| { 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 |_| {} } const callee = self.func_map.get(call_op.callee.index()) orelse { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); return; }; const arg_count: c_uint = @intCast(call_op.args.len); const args = self.alloc.alloc(c.LLVMValueRef, call_op.args.len) catch unreachable; defer self.alloc.free(args); for (call_op.args, 0..) |arg_ref, j| { args[j] = self.resolveRef(arg_ref); } // Get the function type from LLVM and coerce arguments const fn_ty = c.LLVMGlobalGetValueType(callee); const param_count = c.LLVMCountParamTypes(fn_ty); const callee_needs_c_abi = callee_func.is_extern or callee_func.call_conv == .c; if (param_count > 0) { const param_types = self.alloc.alloc(c.LLVMTypeRef, param_count) catch unreachable; defer self.alloc.free(param_types); c.LLVMGetParamTypes(fn_ty, param_types.ptr); for (0..@min(args.len, param_count)) |j| { // Materialize byval args before coercion so we pass a ptr instead of the struct value. if (callee_needs_c_abi and j < callee_func.params.len) { const ir_ty = callee_func.params[j].ty; const raw_struct = self.toLLVMType(ir_ty); if (self.needsByval(ir_ty, raw_struct)) { args[j] = self.materializeByvalArg(args[j], raw_struct); continue; } } args[j] = self.coerceArg(args[j], param_types[j]); } } 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| { const callee = self.resolveRef(call_op.callee); const arg_count: c_uint = @intCast(call_op.args.len); const args = self.alloc.alloc(c.LLVMValueRef, call_op.args.len) catch unreachable; defer self.alloc.free(args); for (call_op.args, 0..) |arg_ref, j| { args[j] = self.resolveRef(arg_ref); } // Get callee's IR type to resolve parameter types accurately const callee_ir_ty = self.getRefIRType(call_op.callee); const fn_params: ?[]const @import("types.zig").TypeId = if (callee_ir_ty) |cty| blk: { if (!cty.isBuiltin()) { const ci = self.ir_mod.types.get(cty); switch (ci) { .function => |f| break :blk f.params, .closure => |cl| break :blk cl.params, else => {}, } } break :blk null; } else null; // Read the fn-pointer type's calling convention. Only `.c` opts // into the C-ABI byval coercion for >16B aggregate params. const fp_is_c_abi: bool = if (callee_ir_ty) |cty| blk: { if (!cty.isBuiltin()) { const ci = self.ir_mod.types.get(cty); if (ci == .function and ci.function.call_conv == .c) break :blk true; } break :blk false; } else false; const ret_ty = if (callee_ir_ty) |cty| blk: { if (!cty.isBuiltin()) { const ci = self.ir_mod.types.get(cty); switch (ci) { .function => |f| break :blk self.toLLVMType(f.ret), .closure => |cl| break :blk self.toLLVMType(cl.ret), else => {}, } } break :blk self.toLLVMType(instruction.ty); } else self.toLLVMType(instruction.ty); const param_tys = self.alloc.alloc(c.LLVMTypeRef, call_op.args.len) catch unreachable; defer self.alloc.free(param_tys); if (fn_params) |fp| { for (0..call_op.args.len) |j| { if (j < fp.len) { const raw_struct = self.toLLVMType(fp[j]); if (fp_is_c_abi and self.needsByval(fp[j], raw_struct)) { args[j] = self.materializeByvalArg(args[j], raw_struct); param_tys[j] = self.cached_ptr; continue; } var llvm_pty = raw_struct; // 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 { param_tys[j] = c.LLVMTypeOf(args[j]); } } } else { for (args, 0..) |arg, j| { param_tys[j] = c.LLVMTypeOf(arg); } } const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, arg_count, 0); var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (instruction.ty == .void) "" else "icall"); // Coerce call result to instruction's expected type const expected_ty = self.toLLVMType(instruction.ty); if (instruction.ty != .void and c.LLVMTypeOf(result) != expected_ty) { result = self.coerceArg(result, expected_ty); } self.mapRef(result); }, // ── Terminators ──────────────────────────────────────── .ret => |un| { var val = self.resolveRef(un.operand); // Coerce return value to match the function's LLVM return type const llvm_func = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder)); const fn_ty = c.LLVMGlobalGetValueType(llvm_func); const expected_ret = c.LLVMGetReturnType(fn_ty); val = self.coerceArg(val, expected_ret); // If coercion didn't fix the type (e.g. dead comptime function), // emit undef of the correct type to avoid LLVM verification error if (c.LLVMTypeOf(val) != expected_ret) { val = c.LLVMGetUndef(expected_ret); } _ = c.LLVMBuildRet(self.builder, val); self.advanceRefCounter(); }, .ret_void => { if (self.current_func_is_main) { // main must return i32 0 for JIT _ = c.LLVMBuildRet(self.builder, c.LLVMConstInt(self.cached_i32, 0, 0)); } else { _ = c.LLVMBuildRetVoid(self.builder); } self.advanceRefCounter(); }, .@"unreachable" => { _ = c.LLVMBuildUnreachable(self.builder); self.advanceRefCounter(); }, .br => |branch| { const target = self.getBlock(func_idx, branch.target); _ = c.LLVMBuildBr(self.builder, target); self.advanceRefCounter(); }, .cond_br => |cbr| { var cond = self.resolveRef(cbr.cond); const then_bb = self.getBlock(func_idx, cbr.then_target); const else_bb = self.getBlock(func_idx, cbr.else_target); // Coerce condition to i1 if needed (e.g., loaded bool stored as i64) const cond_ty = c.LLVMTypeOf(cond); const cond_kind = c.LLVMGetTypeKind(cond_ty); if (cond_ty != self.cached_i1) { if (cond_kind == c.LLVMPointerTypeKind) { cond = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, cond, c.LLVMConstNull(cond_ty), "tobool"); } else if (cond_kind == c.LLVMIntegerTypeKind) { cond = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, cond, c.LLVMConstInt(cond_ty, 0, 0), "tobool"); } else if (cond_kind == c.LLVMStructTypeKind) { // Struct values are always truthy cond = c.LLVMConstInt(self.cached_i1, 1, 0); } else { cond = c.LLVMConstInt(self.cached_i1, 1, 0); // default truthy } } _ = c.LLVMBuildCondBr(self.builder, cond, then_bb, else_bb); self.advanceRefCounter(); }, // ── Struct ops ──────────────────────────────────────────── .struct_init => |agg| { const struct_ty = self.toLLVMType(instruction.ty); const type_kind = c.LLVMGetTypeKind(struct_ty); // For vector types, use InsertElement instead of InsertValue const is_vector = type_kind == c.LLVMVectorTypeKind or type_kind == c.LLVMScalableVectorTypeKind; // For array types, get expected element type for coercion const is_array = type_kind == c.LLVMArrayTypeKind; const elem_llvm_ty = if (is_array) c.LLVMGetElementType(struct_ty) else null; var result = c.LLVMGetUndef(struct_ty); for (agg.fields, 0..) |field_ref, i| { var field_val = self.resolveRef(field_ref); if (is_vector) { // Coerce element to match vector element type const vec_elem_ty = c.LLVMGetElementType(struct_ty); const val_ty = c.LLVMTypeOf(field_val); if (val_ty != vec_elem_ty) { field_val = self.coerceArg(field_val, vec_elem_ty); } const idx = c.LLVMConstInt(self.cached_i32, @intCast(i), 0); result = c.LLVMBuildInsertElement(self.builder, result, field_val, idx, "vi"); } else { // Coerce element to match array element type if needed if (elem_llvm_ty) |elt| { const val_ty = c.LLVMTypeOf(field_val); if (val_ty != elt) { const val_kind = c.LLVMGetTypeKind(val_ty); const elt_kind = c.LLVMGetTypeKind(elt); if (val_kind == c.LLVMIntegerTypeKind and elt_kind == c.LLVMIntegerTypeKind) { const val_w = c.LLVMGetIntTypeWidth(val_ty); const elt_w = c.LLVMGetIntTypeWidth(elt); if (val_w > elt_w) { field_val = c.LLVMBuildTrunc(self.builder, field_val, elt, "atrunc"); } else if (val_w < elt_w) { field_val = c.LLVMBuildZExt(self.builder, field_val, elt, "aext"); } } } } else if (type_kind == c.LLVMStructTypeKind) { // Coerce struct field value to match declared field type const n_elts = c.LLVMCountStructElementTypes(struct_ty); if (n_elts > 0 and i < n_elts) { const field_ty = c.LLVMStructGetTypeAtIndex(struct_ty, @intCast(i)); const val_ty = c.LLVMTypeOf(field_val); if (val_ty != field_ty) { field_val = self.coerceArg(field_val, field_ty); } } } result = c.LLVMBuildInsertValue(self.builder, result, field_val, @intCast(i), "si"); } } self.mapRef(result); }, .struct_get => |fa| { const base = self.resolveRef(fa.base); // Safety: null base means unresolved reference — emit undef if (base == null) { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); } else { // Safety: check that base is an aggregate type (struct/array/vector), not scalar const base_ty = c.LLVMTypeOf(base); const base_ty_kind = c.LLVMGetTypeKind(base_ty); if (base_ty_kind == c.LLVMVectorTypeKind or base_ty_kind == c.LLVMScalableVectorTypeKind) { // Vector: use ExtractElement with an index const idx = c.LLVMConstInt(self.cached_i32, @intCast(fa.field_index), 0); const result = c.LLVMBuildExtractElement(self.builder, base, idx, "ve"); self.mapRef(result); } else if (base_ty_kind == c.LLVMStructTypeKind or base_ty_kind == c.LLVMArrayTypeKind) { // Validate field index is in bounds const n_fields = if (base_ty_kind == c.LLVMStructTypeKind) c.LLVMCountStructElementTypes(base_ty) else 0; // Check builder has valid insert point const insert_bb = c.LLVMGetInsertBlock(self.builder); if (insert_bb == null or (n_fields == 0 and base_ty_kind == c.LLVMStructTypeKind) or (n_fields > 0 and fa.field_index >= n_fields)) { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); } else { const result = c.LLVMBuildExtractValue(self.builder, base, @intCast(fa.field_index), "sg"); self.mapRef(result); } } else { // Base is not an aggregate (e.g., placeholder undef of scalar type) self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); } } }, .struct_gep => |fa| { const base_ptr = self.resolveRef(fa.base); // 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 = 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"); self.mapRef(result); } else { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); } } else { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); } }, // ── Enum ops ───────────────────────────────────────────── .enum_init => |ei| { if (ei.payload.isNone()) { // Simple enum (no payload) — just a tag integer const ty = self.toLLVMType(instruction.ty); const ty_kind = c.LLVMGetTypeKind(ty); if (ty_kind == c.LLVMIntegerTypeKind) { // 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 — 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); } else { self.mapRef(c.LLVMConstInt(self.cached_i64, ei.tag, 0)); } } else { // Tagged union with payload — { header, payload_bytes } const union_ty = self.toLLVMType(instruction.ty); 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 const tmp = c.LLVMBuildAlloca(self.builder, union_ty, "ei.tmp"); // Store tag at field 0 const tag_ptr = c.LLVMBuildStructGEP2(self.builder, union_ty, tmp, 0, "ei.tagp"); _ = c.LLVMBuildStore(self.builder, tag_val, tag_ptr); // Store payload at field 1 (bitcast the byte array to payload type) const payload_ptr = c.LLVMBuildStructGEP2(self.builder, union_ty, tmp, 1, "ei.pp"); const payload_typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "ei.pcast"); _ = c.LLVMBuildStore(self.builder, payload_val, payload_typed_ptr); // Load the whole union value self.mapRef(c.LLVMBuildLoad2(self.builder, union_ty, tmp, "ei.val")); } }, .enum_tag => |un| { const val = self.resolveRef(un.operand); // Check if this is a plain enum (integer) or tagged union (struct with tag at 0) const val_ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(val_ty); if (kind == c.LLVMStructTypeKind) { // Tagged union — extract field 0 (tag) 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); } }, .enum_payload => |fa| { const base = self.resolveRef(fa.base); const result_ty = self.toLLVMType(instruction.ty); const base_ty = c.LLVMTypeOf(base); const base_kind = c.LLVMGetTypeKind(base_ty); if (base_kind == c.LLVMStructTypeKind) { // Tagged union: alloca, store, GEP field 1 (payload area), bitcast, load const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ep.tmp"); _ = c.LLVMBuildStore(self.builder, base, tmp); const payload_ptr = c.LLVMBuildStructGEP2(self.builder, base_ty, tmp, 1, "ep.pp"); const typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "ep.cast"); self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, typed_ptr, "ep.val")); } else { self.mapRef(c.LLVMGetUndef(result_ty)); } }, // ── Union ops ──────────────────────────────────────────── .union_get => |fa| { const base = self.resolveRef(fa.base); const result_ty = self.toLLVMType(instruction.ty); // Union field access: reinterpret the union's data area as the target type const base_ty = c.LLVMTypeOf(base); const kind = c.LLVMGetTypeKind(base_ty); if (kind == c.LLVMStructTypeKind) { // 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"); self.mapRef(c.LLVMBuildLoad2(self.builder, result_ty, payload_ptr, "ug.val")); } else { // 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 = 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(payload_ptr); } else { // Untagged union — data starts at offset 0 self.mapRef(base_ptr); } } else { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); } }, // ── Array/Slice ops ─────────────────────────────────────── .index_get => |bin| { const base = self.resolveRef(bin.lhs); const idx = self.resolveRef(bin.rhs); const base_ty = c.LLVMTypeOf(base); const kind = c.LLVMGetTypeKind(base_ty); if (kind == c.LLVMVectorTypeKind or kind == c.LLVMScalableVectorTypeKind) { // Vector — use extractelement // Coerce index to i32 if needed const idx32 = self.coerceArg(idx, self.cached_i32); self.mapRef(c.LLVMBuildExtractElement(self.builder, base, idx32, "ve")); } else if (kind == c.LLVMArrayTypeKind) { // Fixed-size array value — alloca, store, GEP, load const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ig.tmp"); _ = c.LLVMBuildStore(self.builder, base, tmp); const elem_ty = self.toLLVMType(instruction.ty); var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), idx }; const ptr = c.LLVMBuildGEP2(self.builder, base_ty, tmp, &indices, 2, "ig.ptr"); self.mapRef(c.LLVMBuildLoad2(self.builder, elem_ty, ptr, "ig.val")); } else if (kind == c.LLVMPointerTypeKind) { // Pointer (many-pointer or raw ptr) — GEP + load const elem_ty = self.toLLVMType(instruction.ty); var indices = [_]c.LLVMValueRef{idx}; const ptr = c.LLVMBuildGEP2(self.builder, elem_ty, base, &indices, 1, "ig.ptr"); self.mapRef(c.LLVMBuildLoad2(self.builder, elem_ty, ptr, "ig.val")); } else if (kind == c.LLVMStructTypeKind) { // Slice/string {ptr, len} — extract ptr, GEP, load const data = c.LLVMBuildExtractValue(self.builder, base, 0, "ig.data"); const elem_ty = self.toLLVMType(instruction.ty); var indices = [_]c.LLVMValueRef{idx}; const ptr = c.LLVMBuildGEP2(self.builder, elem_ty, data, &indices, 1, "ig.ptr"); self.mapRef(c.LLVMBuildLoad2(self.builder, elem_ty, ptr, "ig.val")); } else { // Non-aggregate base (lowering error) — emit undef self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); } }, .index_gep => |bin| { const base = self.resolveRef(bin.lhs); const idx = self.resolveRef(bin.rhs); const base_ty = c.LLVMTypeOf(base); const kind = c.LLVMGetTypeKind(base_ty); if (kind == c.LLVMArrayTypeKind) { // Fixed-size array value — alloca, store, GEP const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "igp.tmp"); _ = c.LLVMBuildStore(self.builder, base, tmp); var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), idx }; self.mapRef(c.LLVMBuildGEP2(self.builder, base_ty, tmp, &indices, 2, "igp.ptr")); } else if (kind == c.LLVMPointerTypeKind) { // Pointer — GEP with proper element type const gep_elem = blk: { // instruction.ty is the result type (ptr to element) // Resolve the pointee type for the GEP element size const info = self.ir_mod.types.get(instruction.ty); break :blk switch (info) { .pointer => |p| self.toLLVMType(p.pointee), .many_pointer => |p| self.toLLVMType(p.element), else => self.cached_i8, // fallback }; }; var indices = [_]c.LLVMValueRef{idx}; self.mapRef(c.LLVMBuildGEP2(self.builder, gep_elem, base, &indices, 1, "igp.ptr")); } else if (kind == c.LLVMStructTypeKind) { // Slice/string {ptr, len} — extract ptr, GEP with proper element type const data = c.LLVMBuildExtractValue(self.builder, base, 0, "igp.data"); const gep_elem = blk: { const info = self.ir_mod.types.get(instruction.ty); break :blk switch (info) { .pointer => |p| self.toLLVMType(p.pointee), .many_pointer => |p| self.toLLVMType(p.element), else => self.cached_i8, }; }; var indices = [_]c.LLVMValueRef{idx}; self.mapRef(c.LLVMBuildGEP2(self.builder, gep_elem, data, &indices, 1, "igp.ptr")); } else { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); } }, .length => |un| { const val = self.resolveRef(un.operand); const val_ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(val_ty); if (kind == c.LLVMArrayTypeKind) { const len = c.LLVMGetArrayLength2(val_ty); self.mapRef(c.LLVMConstInt(self.cached_i64, len, 0)); } else if (kind == c.LLVMStructTypeKind) { // Slice/string {ptr, len} — extract field 1 (len) self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 1, "len")); } else { self.mapRef(c.LLVMGetUndef(self.cached_i64)); } }, .data_ptr => |un| { const val = self.resolveRef(un.operand); const val_ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(val_ty); if (kind == c.LLVMStructTypeKind) { self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 0, "dptr")); } else { self.mapRef(c.LLVMGetUndef(self.cached_ptr)); } }, .subslice => |ss| { const base = self.resolveRef(ss.base); var lo = self.resolveRef(ss.lo); var hi = self.resolveRef(ss.hi); // Normalize lo/hi to i64 for consistent arithmetic (indices are unsigned) if (c.LLVMTypeOf(lo) != self.cached_i64) { lo = c.LLVMBuildZExt(self.builder, lo, self.cached_i64, "ss.lo64"); } if (c.LLVMTypeOf(hi) != self.cached_i64) { hi = c.LLVMBuildZExt(self.builder, hi, self.cached_i64, "ss.hi64"); } const base_ty = c.LLVMTypeOf(base); const base_kind = c.LLVMGetTypeKind(base_ty); const slice_ty = self.toLLVMType(instruction.ty); // Resolve element type from the result slice type for correct GEP stride const elem_ty = blk: { const info = self.ir_mod.types.get(instruction.ty); break :blk switch (info) { .slice => |s| self.toLLVMType(s.element), else => self.cached_i8, }; }; if (base_kind == c.LLVMStructTypeKind) { // Slice/string: extract data ptr, GEP by lo const data = c.LLVMBuildExtractValue(self.builder, base, 0, "ss.data"); var lo_indices = [_]c.LLVMValueRef{lo}; const new_ptr = c.LLVMBuildGEP2(self.builder, elem_ty, data, &lo_indices, 1, "ss.ptr"); var new_len = c.LLVMBuildSub(self.builder, hi, lo, "ss.len"); // Ensure length is i64 for slice struct {ptr, i64} if (c.LLVMTypeOf(new_len) != self.cached_i64) { new_len = c.LLVMBuildSExt(self.builder, new_len, self.cached_i64, "ss.ext"); } var result = c.LLVMGetUndef(slice_ty); result = c.LLVMBuildInsertValue(self.builder, result, new_ptr, 0, "ss.wptr"); result = c.LLVMBuildInsertValue(self.builder, result, new_len, 1, "ss.wlen"); self.mapRef(result); } else if (base_kind == c.LLVMArrayTypeKind) { // Array: alloca, GEP to element at lo, compute len const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "ss.arr"); _ = c.LLVMBuildStore(self.builder, base, tmp); var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), lo }; const new_ptr = c.LLVMBuildGEP2(self.builder, base_ty, tmp, &indices, 2, "ss.ptr"); var new_len = c.LLVMBuildSub(self.builder, hi, lo, "ss.len"); // Ensure length is i64 for slice struct {ptr, i64} if (c.LLVMTypeOf(new_len) != self.cached_i64) { new_len = c.LLVMBuildSExt(self.builder, new_len, self.cached_i64, "ss.ext"); } var result = c.LLVMGetUndef(slice_ty); result = c.LLVMBuildInsertValue(self.builder, result, new_ptr, 0, "ss.wptr"); result = c.LLVMBuildInsertValue(self.builder, result, new_len, 1, "ss.wlen"); self.mapRef(result); } else { self.mapRef(c.LLVMGetUndef(slice_ty)); } }, .array_to_slice => |un| { const arr = self.resolveRef(un.operand); const arr_ty = c.LLVMTypeOf(arr); const arr_kind = c.LLVMGetTypeKind(arr_ty); if (arr_kind == c.LLVMArrayTypeKind) { const len = c.LLVMGetArrayLength2(arr_ty); const tmp = c.LLVMBuildAlloca(self.builder, arr_ty, "a2s.tmp"); _ = c.LLVMBuildStore(self.builder, arr, tmp); var indices = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i64, 0, 0), c.LLVMConstInt(self.cached_i64, 0, 0) }; const elem_ptr = c.LLVMBuildGEP2(self.builder, arr_ty, tmp, &indices, 2, "a2s.ptr"); const slice_ty = self.toLLVMType(instruction.ty); var result = c.LLVMGetUndef(slice_ty); result = c.LLVMBuildInsertValue(self.builder, result, elem_ptr, 0, "a2s.wptr"); const len_val = c.LLVMConstInt(self.cached_i64, len, 0); result = c.LLVMBuildInsertValue(self.builder, result, len_val, 1, "a2s.wlen"); self.mapRef(result); } else { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); } }, // ── Call extensions ─────────────────────────────────────── .call_builtin => |bi| { // Builtins that map to libc functions or LLVM intrinsics switch (bi.builtin) { .malloc => { const size = self.coerceArg(self.resolveRef(bi.args[0]), self.sizeType()); const malloc_fn = self.getOrDeclareMalloc(); var args = [_]c.LLVMValueRef{size}; self.mapRef(c.LLVMBuildCall2(self.builder, self.getMallocType(), malloc_fn, &args, 1, "malloc")); }, .free => { const ptr = self.resolveRef(bi.args[0]); const free_fn = self.getOrDeclareFree(); var args = [_]c.LLVMValueRef{ptr}; _ = c.LLVMBuildCall2(self.builder, self.getFreeType(), free_fn, &args, 1, ""); self.advanceRefCounter(); }, .memcpy => { const dst = self.resolveRef(bi.args[0]); const src = self.resolveRef(bi.args[1]); const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType()); const memcpy_fn = self.getOrDeclareMemcpy(); var args = [_]c.LLVMValueRef{ dst, src, len }; _ = c.LLVMBuildCall2(self.builder, self.getMemcpyType(), memcpy_fn, &args, 3, ""); self.advanceRefCounter(); }, .memset => { const dst = self.resolveRef(bi.args[0]); var val = self.resolveRef(bi.args[1]); const len = self.coerceArg(self.resolveRef(bi.args[2]), self.sizeType()); // memset expects i32 for byte value — coerce width val = self.coerceArg(val, self.cached_i32); const memset_fn = self.getOrDeclareMemset(); var args = [_]c.LLVMValueRef{ dst, val, len }; _ = c.LLVMBuildCall2(self.builder, self.getMemsetType(), memset_fn, &args, 3, ""); self.advanceRefCounter(); }, .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) { const f = self.getOrDeclareMathF32(bi.builtin); var args = [_]c.LLVMValueRef{val}; self.mapRef(c.LLVMBuildCall2(self.builder, self.getMathF32Type(), f, &args, 1, @tagName(bi.builtin))); } else { const coerced = if (val_kind != c.LLVMDoubleTypeKind) self.coerceArg(val, self.cached_f64) else val; const f = self.getOrDeclareMathF64(bi.builtin); var args = [_]c.LLVMValueRef{coerced}; self.mapRef(c.LLVMBuildCall2(self.builder, self.getMathF64Type(), f, &args, 1, @tagName(bi.builtin))); } }, .out => { // out(str): extract ptr and len from string fat pointer, call write(1, ptr, len) const str_val = self.resolveRef(bi.args[0]); const raw_ptr = c.LLVMBuildExtractValue(self.builder, str_val, 0, "str.ptr"); const str_len = c.LLVMBuildExtractValue(self.builder, str_val, 1, "str.len"); // On wasm32, count param is i32 (size_t) const count = if (self.target_config.isWasm32()) c.LLVMBuildTrunc(self.builder, str_len, self.cached_i32, "len.tr") else str_len; const write_fn = self.getOrDeclareWrite(); var write_args = [_]c.LLVMValueRef{ c.LLVMConstInt(self.cached_i32, 1, 0), // fd = stdout raw_ptr, count, }; _ = c.LLVMBuildCall2(self.builder, self.getWriteType(), write_fn, &write_args, 3, ""); self.advanceRefCounter(); }, else => { // size_of, cast — handled by lowering or codegen glue self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, } }, .compiler_call => { // Compiler hooks are comptime-only; if one reaches emission, produce undef self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, .call_closure => |call_op| { // Closure: { fn_ptr, env } — extract fn_ptr, prepend env as first arg const closure = self.resolveRef(call_op.callee); const cl_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(closure)); if (cl_kind != c.LLVMStructTypeKind) { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); return; } const fn_ptr = c.LLVMBuildExtractValue(self.builder, closure, 0, "cl.fn"); const env_ptr = c.LLVMBuildExtractValue(self.builder, closure, 1, "cl.env"); // Get the closure's declared parameter types from the IR type system const callee_ir_ty = self.getRefIRType(call_op.callee); const closure_params: ?[]const @import("types.zig").TypeId = if (callee_ir_ty) |cty| blk: { if (!cty.isBuiltin()) { const ci = self.ir_mod.types.get(cty); if (ci == .closure) break :blk ci.closure.params; } break :blk null; } else null; // Build args: env_ptr + call args const total_args = call_op.args.len + 1; const args = self.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable; defer self.alloc.free(args); args[0] = env_ptr; for (call_op.args, 0..) |arg_ref, j| { args[j + 1] = self.resolveRef(arg_ref); } // Build function type using declared param types (not arg types) const ret_ty = self.toLLVMType(instruction.ty); const param_tys = self.alloc.alloc(c.LLVMTypeRef, total_args) catch unreachable; defer self.alloc.free(param_tys); param_tys[0] = self.cached_ptr; // env if (closure_params) |cp| { // Use declared closure param types and coerce args to match // cp contains user-visible params only (no env) for (0..call_op.args.len) |j| { const param_ir_ty = if (j < cp.len) cp[j] else null; if (param_ir_ty) |pty| { const llvm_pty = self.toLLVMType(pty); param_tys[j + 1] = llvm_pty; args[j + 1] = self.coerceArg(args[j + 1], llvm_pty); } else { param_tys[j + 1] = c.LLVMTypeOf(args[j + 1]); } } } else { for (args[1..], 0..) |arg, j| { param_tys[j + 1] = c.LLVMTypeOf(arg); } } const fn_ty = c.LLVMFunctionType(ret_ty, param_tys.ptr, @intCast(total_args), 0); const is_void = instruction.ty == .void; const result = c.LLVMBuildCall2(self.builder, fn_ty, fn_ptr, args.ptr, @intCast(total_args), if (is_void) "" else "ccall"); if (!is_void) { self.mapRef(result); } else { self.advanceRefCounter(); } }, // ── Tuple ops ──────────────────────────────────────────── .tuple_init => |agg| { const tuple_ty = self.toLLVMType(instruction.ty); var result = c.LLVMGetUndef(tuple_ty); for (agg.fields, 0..) |field_ref, i| { const field_val = self.resolveRef(field_ref); result = c.LLVMBuildInsertValue(self.builder, result, field_val, @intCast(i), "ti"); } self.mapRef(result); }, .tuple_get => |fa| { const base = self.resolveRef(fa.base); self.mapRef(c.LLVMBuildExtractValue(self.builder, base, @intCast(fa.field_index), "tg")); }, // ── Optional ops ───────────────────────────────────────── .optional_wrap => |un| { var val = self.resolveRef(un.operand); const opt_ty = self.toLLVMType(instruction.ty); const opt_kind = c.LLVMGetTypeKind(opt_ty); if (opt_kind == c.LLVMPointerTypeKind) { // ?*T — pointer is the optional itself (null = none) self.mapRef(val); } else if (opt_kind == c.LLVMStructTypeKind) { // Distinguish {T, i1} (real optional) from {ptr, ptr} (?Closure) const num_fields = c.LLVMCountStructElementTypes(opt_ty); const last_field_ty = if (num_fields > 0) c.LLVMStructGetTypeAtIndex(opt_ty, num_fields - 1) else self.cached_i1; if (last_field_ty == self.cached_i1) { // ?T → { T, i1 } — wrap value + true flag const inner_ty = c.LLVMStructGetTypeAtIndex(opt_ty, 0); val = self.coerceArg(val, inner_ty); var result = c.LLVMGetUndef(opt_ty); result = c.LLVMBuildInsertValue(self.builder, result, val, 0, "ow.val"); result = c.LLVMBuildInsertValue(self.builder, result, c.LLVMConstInt(self.cached_i1, 1, 0), 1, "ow.has"); self.mapRef(result); } else { // ?Closure → closure struct IS the optional, just pass through self.mapRef(val); } } else { self.mapRef(val); } }, .optional_unwrap => |un| { const val = self.resolveRef(un.operand); const val_ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(val_ty); if (kind == c.LLVMStructTypeKind) { // Distinguish {T, i1} (real optional) from {ptr, ptr} (?Closure) const num_fields = c.LLVMCountStructElementTypes(val_ty); const last_field_ty = if (num_fields > 0) c.LLVMStructGetTypeAtIndex(val_ty, num_fields - 1) else self.cached_i1; if (last_field_ty == self.cached_i1) { // { T, i1 } → extract field 0 self.mapRef(c.LLVMBuildExtractValue(self.builder, val, 0, "ou.val")); } else { // ?Closure → the struct itself is the value self.mapRef(val); } } else { // ?*T → pointer is the value itself self.mapRef(val); } }, .optional_has_value => |un| { const val = self.resolveRef(un.operand); const val_ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(val_ty); if (kind == c.LLVMStructTypeKind) { // Distinguish {T, i1} (real optional) from {ptr, ptr} (?Closure) const num_fields = c.LLVMCountStructElementTypes(val_ty); const last_field_ty = if (num_fields > 0) c.LLVMStructGetTypeAtIndex(val_ty, num_fields - 1) else self.cached_i1; if (last_field_ty == self.cached_i1) { // { T, i1 } → extract has_value flag self.mapRef(c.LLVMBuildExtractValue(self.builder, val, num_fields - 1, "oh.has")); } else { // ?Closure {fn_ptr, env} → check if fn_ptr is null const fn_ptr = c.LLVMBuildExtractValue(self.builder, val, 0, "oh.fn"); self.mapRef(c.LLVMBuildICmp(self.builder, c.LLVMIntNE, fn_ptr, c.LLVMConstNull(c.LLVMTypeOf(fn_ptr)), "oh.nn")); } } else { // ?*T → compare with null const is_nonnull = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, val, c.LLVMConstNull(val_ty), "oh.nn"); self.mapRef(is_nonnull); } }, .optional_coalesce => |bin| { // a ?? b — if a has value, use a's value; otherwise use b const a = self.resolveRef(bin.lhs); var b_val = self.resolveRef(bin.rhs); const a_ty = c.LLVMTypeOf(a); const kind = c.LLVMGetTypeKind(a_ty); if (kind == c.LLVMStructTypeKind) { const n_fields = c.LLVMCountStructElementTypes(a_ty); const f1_ty = if (n_fields >= 2) c.LLVMStructGetTypeAtIndex(a_ty, 1) else null; const is_ti1 = if (f1_ty) |ft| c.LLVMGetTypeKind(ft) == c.LLVMIntegerTypeKind and c.LLVMGetIntTypeWidth(ft) == 1 else false; if (is_ti1) { // Standard optional {T, i1}: extract has_value and unwrap const has = c.LLVMBuildExtractValue(self.builder, a, 1, "oc.has"); const unwrapped = c.LLVMBuildExtractValue(self.builder, a, 0, "oc.val"); const uw_ty = c.LLVMTypeOf(unwrapped); const b_ty = c.LLVMTypeOf(b_val); if (uw_ty != b_ty) { b_val = self.coerceArg(b_val, uw_ty); } self.mapRef(c.LLVMBuildSelect(self.builder, has, unwrapped, b_val, "oc.sel")); } else { // ?Closure {fn_ptr, env}: check if fn_ptr is null const fn_ptr = c.LLVMBuildExtractValue(self.builder, a, 0, "oc.fn"); const is_nonnull = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, fn_ptr, c.LLVMConstNull(c.LLVMTypeOf(fn_ptr)), "oc.nn"); // Select the full closure struct, not just the fn_ptr self.mapRef(c.LLVMBuildSelect(self.builder, is_nonnull, a, b_val, "oc.sel")); } } else { // ?*T — select on null const is_nonnull = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, a, c.LLVMConstNull(a_ty), "oc.nn"); self.mapRef(c.LLVMBuildSelect(self.builder, is_nonnull, a, b_val, "oc.sel")); } }, // ── Box/Unbox Any ──────────────────────────────────────── .box_any => |ba| { const val = self.resolveRef(ba.operand); const any_ty = self.getAnyStructType(); // Any = { type_tag: i64, value: i64 } const tag = c.LLVMConstInt(self.cached_i64, self.anyTag(ba.source_type), 0); // Bitcast value to i64, using SExt for signed types, ZExt otherwise const is_signed = self.isSignedTypeEx(ba.source_type); const val_as_i64 = if (is_signed) self.coerceToI64Signed(val) else self.coerceToI64(val); var result = c.LLVMGetUndef(any_ty); result = c.LLVMBuildInsertValue(self.builder, result, tag, 0, "ba.tag"); result = c.LLVMBuildInsertValue(self.builder, result, val_as_i64, 1, "ba.val"); self.mapRef(result); }, .unbox_any => |un| { const any_val = self.resolveRef(un.operand); const any_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(any_val)); if (any_kind == c.LLVMStructTypeKind) { const raw = c.LLVMBuildExtractValue(self.builder, any_val, 1, "ua.raw"); const target_ty = self.toLLVMType(instruction.ty); self.mapRef(self.coerceFromI64(raw, target_ty)); } else { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); } }, // ── Reflection ops ────────────────────────────────────── .field_name_get => |fr| { // Build global string array for this struct's field names, then GEP at runtime index const global = self.getOrBuildFieldNameArray(fr.struct_type); const idx = self.resolveRef(fr.index); const string_ty = self.getStringStructType(); // Get struct field count for array type const field_info = self.ir_mod.types.get(fr.struct_type); 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, }; const array_ty = c.LLVMArrayType(string_ty, field_count); const zero = c.LLVMConstInt(self.cached_i64, 0, 0); var indices = [2]c.LLVMValueRef{ zero, idx }; const gep = c.LLVMBuildInBoundsGEP2(self.builder, array_ty, global, &indices, 2, "fn.gep"); const result = c.LLVMBuildLoad2(self.builder, string_ty, gep, "fn.load"); self.mapRef(result); }, .field_value_get => |fr| { // Switch on index, each case: extractvalue field k → box as Any self.emitFieldValueGet(fr, func_idx); }, // ── Switch branch ──────────────────────────────────────── .switch_br => |sw| { const operand = self.resolveRef(sw.operand); const default_bb = self.getBlock(func_idx, sw.default); const switch_inst = c.LLVMBuildSwitch(self.builder, operand, default_bb, @intCast(sw.cases.len)); for (sw.cases) |case| { const case_val = c.LLVMConstInt(c.LLVMTypeOf(operand), @bitCast(case.value), 0); const case_bb = self.getBlock(func_idx, case.target); c.LLVMAddCase(switch_inst, case_val, case_bb); } self.advanceRefCounter(); }, // ── Closure creation ───────────────────────────────────── .closure_create => |cc| { const fn_val = self.func_map.get(cc.func.index()) orelse c.LLVMGetUndef(self.cached_ptr); const env_val = if (cc.env.isNone()) c.LLVMConstNull(self.cached_ptr) else self.resolveRef(cc.env); const closure_ty = self.getClosureStructType(); var result = c.LLVMGetUndef(closure_ty); result = c.LLVMBuildInsertValue(self.builder, result, fn_val, 0, "cc.fn"); result = c.LLVMBuildInsertValue(self.builder, result, env_val, 1, "cc.env"); self.mapRef(result); }, // ── Context ops ────────────────────────────────────────── .context_load, .context_store, .context_save => { // Context ops are handled by the existing codegen glue // In the IR pipeline, these become load/store on a known global self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, .context_restore => { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, // ── Protocol ops ───────────────────────────────────────── .protocol_call_dynamic => |pc| { // Dynamic dispatch through vtable — extract method ptr from protocol value const receiver = self.resolveRef(pc.receiver); _ = receiver; // Stub: full protocol dispatch needs vtable layout info self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, .protocol_erase => |pe| { const concrete = self.resolveRef(pe.concrete); _ = concrete; // Stub: needs protocol struct construction self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, // ── Vector ops ─────────────────────────────────────────── .vec_splat => |un| { const scalar = self.resolveRef(un.operand); const vec_ty = self.toLLVMType(instruction.ty); const vec_len = c.LLVMGetVectorSize(vec_ty); // Build a splat: insertelement into undef for each lane var result = c.LLVMGetUndef(vec_ty); var i: c_uint = 0; while (i < vec_len) : (i += 1) { const idx_val = c.LLVMConstInt(self.cached_i32, i, 0); result = c.LLVMBuildInsertElement(self.builder, result, scalar, idx_val, "splat"); } self.mapRef(result); }, .vec_extract => |bin| { const vec = self.resolveRef(bin.lhs); const idx = self.resolveRef(bin.rhs); self.mapRef(c.LLVMBuildExtractElement(self.builder, vec, idx, "vext")); }, .vec_insert => |tri| { const vec = self.resolveRef(tri.a); const idx = self.resolveRef(tri.b); const val = self.resolveRef(tri.c); self.mapRef(c.LLVMBuildInsertElement(self.builder, vec, val, idx, "vins")); }, // ── Block params ───────────────────────────────────────── .block_param => |bp| { // Create a PHI node — incoming values are filled in by fixupPhiNodes const ty = self.toLLVMType(instruction.ty); const phi = c.LLVMBuildPhi(self.builder, ty, "bp"); self.pending_phis.append(self.alloc, .{ .phi = phi, .block_id = bp.block, .param_index = bp.param_index, }) catch unreachable; self.mapRef(phi); }, // ── Misc ───────────────────────────────────────────────── .placeholder => { self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); }, } } // ── Ref tracking ──────────────────────────────────────────────── fn mapRef(self: *LLVMEmitter, val: c.LLVMValueRef) void { self.ref_map.put(self.ref_counter, val) catch unreachable; self.ref_counter += 1; } fn advanceRefCounter(self: *LLVMEmitter) void { self.ref_counter += 1; } fn resolveRef(self: *LLVMEmitter, ref: Ref) c.LLVMValueRef { if (ref.isNone()) { return c.LLVMGetUndef(self.cached_i64); } return self.ref_map.get(ref.index()) orelse c.LLVMGetUndef(self.cached_i64); } fn getBlock(self: *LLVMEmitter, func_idx: u32, block_id: BlockId) c.LLVMBasicBlockRef { const key = makeBlockKey(func_idx, block_id.index()); return self.block_map.get(key) orelse { std.debug.print("getBlock: missing block func={d} block={d}\n", .{ func_idx, block_id.index() }); unreachable; }; } // ── Struct/union GEP helper ──────────────────────────────────────── /// For struct_gep/union_gep: we need the LLVM type of the aggregate being pointed to. /// The instruction's type is the *result* (pointer to field), so we need to look at /// the IR instruction that produced the base pointer to find the aggregate type. /// As a fallback, we scan back through the ref_map to find the alloca type. fn getStructTypeForGep(self: *LLVMEmitter, instruction: *const Inst) c.LLVMTypeRef { // For GEP, the base ref points to an alloca or another pointer. // The instruction type is a pointer type (result of GEP), but we need the // aggregate type. We get it from the base pointer's allocated type. const fa = switch (instruction.op) { .struct_gep => |f| f, .union_gep => |f| f, else => unreachable, }; const base_val = self.resolveRef(fa.base); // LLVMGetAllocatedType only works on alloca instructions if (c.LLVMIsAAllocaInst(base_val) != null) { const alloc_ty = c.LLVMGetAllocatedType(base_val); if (alloc_ty != null) return alloc_ty; } // Fallback: trace LLVM value chain — if base came from a load, // check the load's source pointer for an alloca if (c.LLVMIsALoadInst(base_val) != null) { const load_ptr = c.LLVMGetOperand(base_val, 0); if (load_ptr != null and c.LLVMIsAAllocaInst(load_ptr) != null) { const inner_alloc = c.LLVMGetAllocatedType(load_ptr); if (inner_alloc != null) return inner_alloc; } } // Fallback: look up the IR type of the base ref to find the pointee type const base_ir_ty = self.getRefIRType(fa.base); if (base_ir_ty) |ir_ty| { if (!ir_ty.isBuiltin()) { const info = self.ir_mod.types.get(ir_ty); switch (info) { .pointer => |p| return self.toLLVMType(p.pointee), else => return self.toLLVMType(ir_ty), } } } return self.cached_i64; } /// Resolve the struct LLVM type for GEP operations. /// Uses LLVM alloca type when available, falls back to IR type system. fn resolveGepStructType(self: *LLVMEmitter, base_ref: Ref, instruction: *const Inst) c.LLVMTypeRef { const base_val = self.resolveRef(base_ref); // Strategy 1: base is an alloca — get allocated type directly if (c.LLVMIsAAllocaInst(base_val) != null) { const alloc_ty = c.LLVMGetAllocatedType(base_val); if (alloc_ty != null) { const kind = c.LLVMGetTypeKind(alloc_ty); if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind) return alloc_ty; } } // Strategy 2: Use IR type system — most accurate for chained GEPs (e.g. union_gep + struct_gep) const base_ir_ty = self.getRefIRType(base_ref); if (base_ir_ty) |ir_ty| { // Resolve through pointer types to find the pointee struct var resolved = ir_ty; if (!resolved.isBuiltin()) { const info = self.ir_mod.types.get(resolved); if (info == .pointer) { resolved = info.pointer.pointee; } } if (!resolved.isBuiltin()) { return self.toLLVMType(resolved); } } // Strategy 3: base is a GEP result — get the source element type if (c.LLVMIsAGetElementPtrInst(base_val) != null) { const src_ty = c.LLVMGetGEPSourceElementType(base_val); if (src_ty != null) { const kind = c.LLVMGetTypeKind(src_ty); if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind) return src_ty; } } // Strategy 4: old fallback _ = instruction; 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 { var lhs = self.resolveRef(bin.lhs); var rhs = self.resolveRef(bin.rhs); // Determine if float by inspecting operand LLVM type var lhs_ty = c.LLVMTypeOf(lhs); var kind = c.LLVMGetTypeKind(lhs_ty); var rhs_ty = c.LLVMTypeOf(rhs); var rhs_kind = c.LLVMGetTypeKind(rhs_ty); // Unwrap single-element struct (1-tuple) to scalar for comparison if (kind == c.LLVMStructTypeKind and rhs_kind != c.LLVMStructTypeKind) { if (c.LLVMCountStructElementTypes(lhs_ty) == 1) { lhs = c.LLVMBuildExtractValue(self.builder, lhs, 0, "tup.unwrap"); lhs_ty = c.LLVMTypeOf(lhs); kind = c.LLVMGetTypeKind(lhs_ty); } } else if (rhs_kind == c.LLVMStructTypeKind and kind != c.LLVMStructTypeKind) { if (c.LLVMCountStructElementTypes(rhs_ty) == 1) { rhs = c.LLVMBuildExtractValue(self.builder, rhs, 0, "tup.unwrap"); rhs_ty = c.LLVMTypeOf(rhs); rhs_kind = c.LLVMGetTypeKind(rhs_ty); } } // 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) { 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 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") else c.LLVMBuildOr(self.builder, cmp0, cmp1, "sc.or"); self.mapRef(result); return; } } // Coerce operands to same type if needed if (kind == c.LLVMIntegerTypeKind and rhs_kind == c.LLVMIntegerTypeKind) { const lw = c.LLVMGetIntTypeWidth(lhs_ty); const rw = c.LLVMGetIntTypeWidth(rhs_ty); const is_unsigned = self.isRefUnsigned(bin.lhs) or self.isRefUnsigned(bin.rhs); 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"); } } // Pointer vs integer: coerce int to null pointer if (kind == c.LLVMPointerTypeKind and rhs_kind == c.LLVMIntegerTypeKind) { rhs = c.LLVMConstNull(lhs_ty); } else if (kind == c.LLVMIntegerTypeKind and rhs_kind == c.LLVMPointerTypeKind) { lhs = c.LLVMConstNull(rhs_ty); } const result_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(lhs)); const result = if (result_kind == c.LLVMFloatTypeKind or result_kind == c.LLVMDoubleTypeKind) c.LLVMBuildFCmp(self.builder, @intCast(float_pred), lhs, rhs, "fcmp") else c.LLVMBuildICmp(self.builder, @intCast(int_pred), lhs, rhs, "icmp"); self.mapRef(result); } fn emitCmpOrdered(self: *LLVMEmitter, bin: ir_inst.BinOp, _: TypeId, signed_pred: c_uint, unsigned_pred: c_uint, float_pred: c_uint) void { var lhs = self.resolveRef(bin.lhs); 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); const rhs_kind = c.LLVMGetTypeKind(rhs_ty); if (rhs_kind == c.LLVMIntegerTypeKind) { const lw = c.LLVMGetIntTypeWidth(lhs_ty); const rw = c.LLVMGetIntTypeWidth(rhs_ty); 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 c.LLVMBuildICmp(self.builder, @intCast(signed_pred), lhs, rhs, "icmp"); self.mapRef(result); } /// String comparison via memcmp: compare length first, then content. fn emitStrCmp(self: *LLVMEmitter, bin: ir_inst.BinOp, is_eq: bool) void { const lhs = self.resolveRef(bin.lhs); const rhs = self.resolveRef(bin.rhs); const b = self.builder; const i32_ty = c.LLVMInt32TypeInContext(self.context); const i1_ty = c.LLVMInt1TypeInContext(self.context); const ptr_ty = c.LLVMPointerTypeInContext(self.context, 0); // Extract ptr and len from both fat pointers const lhs_ptr = c.LLVMBuildExtractValue(b, lhs, 0, "str.lp"); const lhs_len = c.LLVMBuildExtractValue(b, lhs, 1, "str.ll"); const rhs_ptr = c.LLVMBuildExtractValue(b, rhs, 0, "str.rp"); const rhs_len = c.LLVMBuildExtractValue(b, rhs, 1, "str.rl"); // Compare lengths first const len_eq = c.LLVMBuildICmp(b, c.LLVMIntEQ, lhs_len, rhs_len, "str.len_eq"); // Set up basic blocks const cur_fn = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(b)); const memcmp_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "str.memcmp"); const merge_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "str.merge"); const cur_bb = c.LLVMGetInsertBlock(b); _ = c.LLVMBuildCondBr(b, len_eq, memcmp_bb, merge_bb); // memcmp block c.LLVMPositionBuilderAtEnd(b, memcmp_bb); const size_ty = self.sizeType(); const memcmp_fn = c.LLVMGetNamedFunction(self.llvm_module, "memcmp") orelse blk: { var params = [_]c.LLVMTypeRef{ ptr_ty, ptr_ty, size_ty }; const fn_type = c.LLVMFunctionType(i32_ty, ¶ms, 3, 0); break :blk c.LLVMAddFunction(self.llvm_module, "memcmp", fn_type); }; const cmp_len = self.coerceArg(lhs_len, size_ty); var args = [_]c.LLVMValueRef{ lhs_ptr, rhs_ptr, cmp_len }; const fn_ty = c.LLVMGlobalGetValueType(memcmp_fn); const cmp_result = c.LLVMBuildCall2(b, fn_ty, memcmp_fn, &args, 3, "memcmp"); const content_eq = c.LLVMBuildICmp(b, c.LLVMIntEQ, cmp_result, c.LLVMConstInt(i32_ty, 0, 0), "str.ceq"); _ = c.LLVMBuildBr(b, merge_bb); // Merge block: phi(len_mismatch=false, memcmp_result) c.LLVMPositionBuilderAtEnd(b, merge_bb); const phi = c.LLVMBuildPhi(b, i1_ty, "str.eq"); const false_val = c.LLVMConstInt(i1_ty, 0, 0); var phi_vals = [_]c.LLVMValueRef{ false_val, content_eq }; var phi_bbs = [_]c.LLVMBasicBlockRef{ cur_bb, memcmp_bb }; c.LLVMAddIncoming(phi, &phi_vals, &phi_bbs, 2); const result = if (is_eq) phi else c.LLVMBuildNot(b, phi, "str.ne"); self.mapRef(result); } // ── Conversion helpers ────────────────────────────────────────── fn emitConversion(self: *LLVMEmitter, operand: c.LLVMValueRef, from: TypeId, to: TypeId, to_ty: c.LLVMTypeRef) c.LLVMValueRef { const from_float = isFloatOrVecFloat(from, &self.ir_mod.types); const to_float = isFloatOrVecFloat(to, &self.ir_mod.types); if (from_float and to_float) { // float→float: FPExt or FPTrunc const from_bits = floatBits(from); const to_bits = floatBits(to); return if (to_bits > from_bits) c.LLVMBuildFPExt(self.builder, operand, to_ty, "fpext") else c.LLVMBuildFPTrunc(self.builder, operand, to_ty, "fptrunc"); } if (from_float and !to_float) { return if (isSignedType(to)) c.LLVMBuildFPToSI(self.builder, operand, to_ty, "fptosi") else c.LLVMBuildFPToUI(self.builder, operand, to_ty, "fptoui"); } if (!from_float and to_float) { return if (isSignedType(from)) c.LLVMBuildSIToFP(self.builder, operand, to_ty, "sitofp") else c.LLVMBuildUIToFP(self.builder, operand, to_ty, "uitofp"); } // int→int: SExt, ZExt, or Trunc const ptr_bits: u32 = @as(u32, self.ir_mod.types.pointer_size) * 8; const from_bits = if (intBits(from) == 0) ptr_bits else intBits(from); const to_bits = if (intBits(to) == 0) ptr_bits else intBits(to); if (to_bits > from_bits) { return if (isSignedType(from)) c.LLVMBuildSExt(self.builder, operand, to_ty, "sext") else c.LLVMBuildZExt(self.builder, operand, to_ty, "zext"); } else if (to_bits < from_bits) { return c.LLVMBuildTrunc(self.builder, operand, to_ty, "trunc"); } // Same width — no-op (bitcast or just return) return operand; } // ── Malloc/Free declarations ──────────────────────────────────── fn getOrDeclareMalloc(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "malloc")) |f| return f; const fn_ty = self.getMallocType(); return c.LLVMAddFunction(self.llvm_module, "malloc", fn_ty); } fn getOrDeclareFree(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "free")) |f| return f; const fn_ty = self.getFreeType(); return c.LLVMAddFunction(self.llvm_module, "free", fn_ty); } /// Returns the LLVM type for C `size_t`: i32 on wasm32, i64 on 64-bit targets (including wasm64). fn sizeType(self: *LLVMEmitter) c.LLVMTypeRef { return if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64; } fn getMallocType(self: *LLVMEmitter) c.LLVMTypeRef { // malloc(size_t) → ptr var param_types = [_]c.LLVMTypeRef{self.sizeType()}; return c.LLVMFunctionType(self.cached_ptr, ¶m_types, 1, 0); } fn getFreeType(self: *LLVMEmitter) c.LLVMTypeRef { // free(ptr) → void var param_types = [_]c.LLVMTypeRef{self.cached_ptr}; return c.LLVMFunctionType(self.cached_void, ¶m_types, 1, 0); } fn getOrDeclareMemcpy(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "memcpy")) |f| return f; return c.LLVMAddFunction(self.llvm_module, "memcpy", self.getMemcpyType()); } fn getMemcpyType(self: *LLVMEmitter) c.LLVMTypeRef { // memcpy(ptr, ptr, size_t) → ptr var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.sizeType() }; return c.LLVMFunctionType(self.cached_ptr, ¶m_types, 3, 0); } fn getOrDeclareMemset(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "memset")) |f| return f; return c.LLVMAddFunction(self.llvm_module, "memset", self.getMemsetType()); } fn getMemsetType(self: *LLVMEmitter) c.LLVMTypeRef { // memset(ptr, i32, size_t) → ptr var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_i32, self.sizeType() }; return c.LLVMFunctionType(self.cached_ptr, ¶m_types, 3, 0); } 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 getMathF64Type(self: *LLVMEmitter) c.LLVMTypeRef { var param_types = [_]c.LLVMTypeRef{self.cached_f64}; return c.LLVMFunctionType(self.cached_f64, ¶m_types, 1, 0); } 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 getMathF32Type(self: *LLVMEmitter) c.LLVMTypeRef { var param_types = [_]c.LLVMTypeRef{self.cached_f32}; return c.LLVMFunctionType(self.cached_f32, ¶m_types, 1, 0); } fn getOrDeclareMemcmp(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "memcmp")) |f| return f; // memcmp(ptr, ptr, size_t) → i32 var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.sizeType() }; const fn_ty = c.LLVMFunctionType(self.cached_i32, ¶m_types, 3, 0); return c.LLVMAddFunction(self.llvm_module, "memcmp", fn_ty); } fn getOrDeclareWrite(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "write")) |f| return f; return c.LLVMAddFunction(self.llvm_module, "write", self.getWriteType()); } fn getWriteType(self: *LLVMEmitter) c.LLVMTypeRef { // write(fd: i32, buf: ptr, count: size_t) → ssize_t const st = self.sizeType(); var param_types = [_]c.LLVMTypeRef{ self.cached_i32, self.cached_ptr, st }; return c.LLVMFunctionType(st, ¶m_types, 3, 0); } fn getOrDeclareSnprintf(self: *LLVMEmitter) c.LLVMValueRef { if (c.LLVMGetNamedFunction(self.llvm_module, "snprintf")) |f| return f; return c.LLVMAddFunction(self.llvm_module, "snprintf", self.getSnprintfType()); } fn getSnprintfType(self: *LLVMEmitter) c.LLVMTypeRef { // snprintf(buf: ptr, size: i32, fmt: ptr, ...) → i32 (variadic) var param_types = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_i32, self.cached_ptr }; return c.LLVMFunctionType(self.cached_i32, ¶m_types, 3, 1); // 1 = variadic } /// Check if a function name is a known libc builtin that has a dedicated /// getOrDeclare* helper with correct C-compatible types. fn isBuiltinLibcName(name: []const u8) bool { const builtins = [_][]const u8{ "malloc", "free", "memcpy", "memset", "memcmp", "write", "snprintf" }; for (builtins) |b| { if (std.mem.eql(u8, name, b)) return true; } return false; } /// Get or declare a builtin libc function by name, using the correct C-compatible type. fn getOrDeclareBuiltinByName(self: *LLVMEmitter, name: []const u8) ?c.LLVMValueRef { if (std.mem.eql(u8, name, "malloc")) return self.getOrDeclareMalloc(); if (std.mem.eql(u8, name, "free")) return self.getOrDeclareFree(); if (std.mem.eql(u8, name, "memcpy")) return self.getOrDeclareMemcpy(); if (std.mem.eql(u8, name, "memset")) return self.getOrDeclareMemset(); if (std.mem.eql(u8, name, "memcmp")) return self.getOrDeclareMemcmp(); if (std.mem.eql(u8, name, "write")) return self.getOrDeclareWrite(); if (std.mem.eql(u8, name, "snprintf")) return self.getOrDeclareSnprintf(); return null; } /// Build a string fat pointer {ptr, len} from raw pointer and length. fn buildStringValue(self: *LLVMEmitter, ptr: c.LLVMValueRef, len: c.LLVMValueRef) c.LLVMValueRef { const str_ty = self.getStringStructType(); const undef = c.LLVMGetUndef(str_ty); const with_ptr = c.LLVMBuildInsertValue(self.builder, undef, ptr, 0, "s.ptr"); return c.LLVMBuildInsertValue(self.builder, with_ptr, len, 1, "s.len"); } // ── Value coercion helpers ────────────────────────────────────── /// Coerce any scalar value to i64 for boxing into Any. fn coerceToI64(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef { const ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(ty); if (kind == c.LLVMVoidTypeKind) { return c.LLVMConstInt(self.cached_i64, 0, 0); } if (kind == c.LLVMPointerTypeKind) { return c.LLVMBuildPtrToInt(self.builder, val, self.cached_i64, "p2i"); } if (kind == c.LLVMFloatTypeKind) { // f32 → bitcast to i32 → zext to i64 const as_i32 = c.LLVMBuildBitCast(self.builder, val, self.cached_i32, "f2i32"); return c.LLVMBuildZExt(self.builder, as_i32, self.cached_i64, "z64"); } if (kind == c.LLVMDoubleTypeKind) { return c.LLVMBuildBitCast(self.builder, val, self.cached_i64, "d2i"); } if (kind == c.LLVMIntegerTypeKind) { const width = c.LLVMGetIntTypeWidth(ty); if (width < 64) { return c.LLVMBuildZExt(self.builder, val, self.cached_i64, "z64"); } return val; // already i64 } // Struct/Array/Vector types: store to alloca, ptrtoint for the pointer if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind or kind == c.LLVMVectorTypeKind or kind == c.LLVMScalableVectorTypeKind) { const tmp = c.LLVMBuildAlloca(self.builder, ty, "ba.tmp"); _ = c.LLVMBuildStore(self.builder, val, tmp); return c.LLVMBuildPtrToInt(self.builder, tmp, self.cached_i64, "ba.p2i"); } return val; } /// Coerce signed integer to i64 using sign-extension. fn coerceToI64Signed(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef { const ty = c.LLVMTypeOf(val); const kind = c.LLVMGetTypeKind(ty); if (kind == c.LLVMIntegerTypeKind) { const width = c.LLVMGetIntTypeWidth(ty); if (width < 64) { return c.LLVMBuildSExt(self.builder, val, self.cached_i64, "s64"); } return val; } // Fallback for non-integer types return self.coerceToI64(val); } /// Check if a TypeId represents a signed integer type (including arbitrary-width). fn isSignedTypeEx(self: *LLVMEmitter, ty: TypeId) bool { if (isSignedType(ty)) return true; if (!ty.isBuiltin()) { const info = self.ir_mod.types.get(ty); return info == .signed; } return false; } /// Map a TypeId to its Any tag value. /// Uses TypeId.index() directly — this matches resolveTypeCategoryTags in lower.zig /// which also uses TypeId indices for type-switch comparisons. /// For arbitrary-width ints (user-defined signed/unsigned), map to the closest /// builtin TypeId so the "case int:" branch matches correctly. /// Map a TypeId to its Any tag value. /// Uses TypeId.index() directly — this matches resolveTypeCategoryTags in lower.zig /// which also uses TypeId indices for type-switch comparisons. /// For arbitrary-width ints (user-defined signed/unsigned), map to the closest /// builtin TypeId so the "case int:" branch matches correctly. fn anyTag(self: *LLVMEmitter, ty: TypeId) u64 { if (ty.isBuiltin()) return ty.index(); // For user-defined types, check if they're arbitrary-width ints const info = self.ir_mod.types.get(ty); return switch (info) { .signed => |w| switch (w) { 8 => TypeId.s8.index(), 16 => TypeId.s16.index(), 32 => TypeId.s32.index(), 64 => TypeId.s64.index(), else => if (w <= 32) TypeId.s32.index() else TypeId.s64.index(), }, .unsigned => |w| switch (w) { 8 => TypeId.u8.index(), 16 => TypeId.u16.index(), 32 => TypeId.u32.index(), 64 => TypeId.u64.index(), else => if (w <= 32) TypeId.u32.index() else TypeId.u64.index(), }, else => ty.index(), }; } /// Coerce i64 back to the target type for unboxing from Any. fn coerceFromI64(self: *LLVMEmitter, val: c.LLVMValueRef, target: c.LLVMTypeRef) c.LLVMValueRef { const kind = c.LLVMGetTypeKind(target); if (kind == c.LLVMPointerTypeKind) { return c.LLVMBuildIntToPtr(self.builder, val, target, "i2p"); } if (kind == c.LLVMFloatTypeKind) { const as_i32 = c.LLVMBuildTrunc(self.builder, val, self.cached_i32, "tr32"); return c.LLVMBuildBitCast(self.builder, as_i32, target, "i2f"); } if (kind == c.LLVMDoubleTypeKind) { return c.LLVMBuildBitCast(self.builder, val, target, "i2d"); } if (kind == c.LLVMIntegerTypeKind) { const width = c.LLVMGetIntTypeWidth(target); if (width < 64) { return c.LLVMBuildTrunc(self.builder, val, target, "tr"); } } // Struct/Array/Vector types: interpret i64 as pointer, load the value if (kind == c.LLVMStructTypeKind or kind == c.LLVMArrayTypeKind or kind == c.LLVMVectorTypeKind or kind == c.LLVMScalableVectorTypeKind) { const ptr = c.LLVMBuildIntToPtr(self.builder, val, self.cached_ptr, "ua.ptr"); return c.LLVMBuildLoad2(self.builder, target, ptr, "ua.load"); } return val; } /// Coerce a call argument to match the expected parameter type. /// Handles int width mismatches (trunc/ext), float width, and int↔float. fn coerceArg(self: *LLVMEmitter, val: c.LLVMValueRef, param_ty: c.LLVMTypeRef) c.LLVMValueRef { const val_ty = c.LLVMTypeOf(val); if (val_ty == param_ty) return val; const val_kind = c.LLVMGetTypeKind(val_ty); const param_kind = c.LLVMGetTypeKind(param_ty); // Int → Int (width mismatch) if (val_kind == c.LLVMIntegerTypeKind and param_kind == c.LLVMIntegerTypeKind) { const val_w = c.LLVMGetIntTypeWidth(val_ty); const param_w = c.LLVMGetIntTypeWidth(param_ty); if (val_w > param_w) { return c.LLVMBuildTrunc(self.builder, val, param_ty, "ca.tr"); } else { // Use ZExt by default — preserves bit pattern for unsigned types. // Signed widening is handled by explicit widen instructions from the IR. return c.LLVMBuildZExt(self.builder, val, param_ty, "ca.ext"); } } // Float → Float (width mismatch) if ((val_kind == c.LLVMFloatTypeKind or val_kind == c.LLVMDoubleTypeKind) and (param_kind == c.LLVMFloatTypeKind or param_kind == c.LLVMDoubleTypeKind)) { if (val_kind == c.LLVMFloatTypeKind and param_kind == c.LLVMDoubleTypeKind) { return c.LLVMBuildFPExt(self.builder, val, param_ty, "ca.fpext"); } else { return c.LLVMBuildFPTrunc(self.builder, val, param_ty, "ca.fptrunc"); } } // Int → Float (use SIToFP for i1/bool, UIToFP otherwise for safe default) if (val_kind == c.LLVMIntegerTypeKind and (param_kind == c.LLVMFloatTypeKind or param_kind == c.LLVMDoubleTypeKind)) { const val_w = c.LLVMGetIntTypeWidth(val_ty); if (val_w == 1) { return c.LLVMBuildUIToFP(self.builder, val, param_ty, "ca.uitofp"); } // Default to SIToFP since most sx integers are signed (s64). // Explicit unsigned conversions go through the IR widen/narrow path. return c.LLVMBuildSIToFP(self.builder, val, param_ty, "ca.sitofp"); } // Float → Int if ((val_kind == c.LLVMFloatTypeKind or val_kind == c.LLVMDoubleTypeKind) and param_kind == c.LLVMIntegerTypeKind) { return c.LLVMBuildFPToSI(self.builder, val, param_ty, "ca.fptosi"); } // Ptr → Struct (closure auto-promotion: fn_ptr → {fn_ptr, null_env}) if (val_kind == c.LLVMPointerTypeKind and param_kind == c.LLVMStructTypeKind) { const num_fields = c.LLVMCountStructElementTypes(param_ty); if (num_fields == 2) { const f0 = c.LLVMStructGetTypeAtIndex(param_ty, 0); const f1 = c.LLVMStructGetTypeAtIndex(param_ty, 1); if (c.LLVMGetTypeKind(f0) == c.LLVMPointerTypeKind and c.LLVMGetTypeKind(f1) == c.LLVMPointerTypeKind) { var result = c.LLVMGetUndef(param_ty); result = c.LLVMBuildInsertValue(self.builder, result, val, 0, "ca.cls.fn"); result = c.LLVMBuildInsertValue(self.builder, result, c.LLVMConstNull(f1), 1, "ca.cls.env"); return result; } } } // Scalar → Vector (splat: broadcast scalar to all lanes) if (param_kind == c.LLVMVectorTypeKind or param_kind == c.LLVMScalableVectorTypeKind) { const vec_elem_ty = c.LLVMGetElementType(param_ty); const vec_len = c.LLVMGetVectorSize(param_ty); // First coerce scalar to the vector element type const scalar = self.coerceArg(val, vec_elem_ty); // Then splat into a vector var result = c.LLVMGetUndef(param_ty); var lane: c_uint = 0; while (lane < vec_len) : (lane += 1) { const idx = c.LLVMConstInt(self.cached_i32, lane, 0); result = c.LLVMBuildInsertElement(self.builder, result, scalar, idx, "splat"); } return result; } // Struct → Ptr (string/slice decay: extract field 0 = raw pointer) // Only for 2-field structs {ptr, i64} (fat pointers) — avoids breaking other struct→ptr cases if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMPointerTypeKind) { const num_fields = c.LLVMCountStructElementTypes(val_ty); if (num_fields == 2) { const field0_ty = c.LLVMStructGetTypeAtIndex(val_ty, 0); if (c.LLVMGetTypeKind(field0_ty) == c.LLVMPointerTypeKind) { return c.LLVMBuildExtractValue(self.builder, val, 0, "ca.decay"); } } } // 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"); } // Struct → Array (C ABI coercion for 9..16-byte structs — paired with // abiCoerceParamType's `[2 x i64]` slot for that size class). Same // memory-bitcast pattern as the integer case; the array type carries // 16 bytes of storage so we alloca with param_ty to guarantee size. if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMArrayTypeKind) { const tmp = c.LLVMBuildAlloca(self.builder, param_ty, "abi.struct2arr"); _ = c.LLVMBuildStore(self.builder, val, tmp); return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.coerce.arr"); } // Array → Struct (return-side counterpart for 9..16-byte structs) if (val_kind == c.LLVMArrayTypeKind and param_kind == c.LLVMStructTypeKind) { const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "abi.arr2struct"); _ = c.LLVMBuildStore(self.builder, val, tmp); return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.ret.coerce.arr"); } // 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; } /// Look up the IR type of a Ref in the current function (for store coercion). fn getRefIRType(self: *LLVMEmitter, ref: Ref) ?TypeId { const func = &self.ir_mod.functions.items[self.current_func_idx]; const idx = ref.index(); // Check if it's a function param (refs 0..N-1) if (idx < func.params.len) return func.params[idx].ty; for (func.blocks.items) |blk| { if (idx >= blk.first_ref and idx < blk.first_ref + blk.insts.items.len) { return blk.insts.items[idx - blk.first_ref].ty; } } return null; } /// Coerce both binary operands to match the instruction's result type. /// E.g. if result is i64 but one operand is i32, sext it. fn matchBinOpTypes(self: *LLVMEmitter, lhs: *c.LLVMValueRef, rhs: *c.LLVMValueRef, result_ty: TypeId) void { const target = self.toLLVMType(result_ty); lhs.* = self.coerceArg(lhs.*, target); rhs.* = self.coerceArg(rhs.*, target); } // ── Type conversion ───────────────────────────────────────────── pub fn toLLVMType(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef { return switch (ty) { .void => self.cached_void, .bool => self.cached_i1, .s8 => self.cached_i8, .s16 => self.cached_i16, .s32 => self.cached_i32, .s64 => self.cached_i64, .u8 => self.cached_i8, .u16 => self.cached_i16, .u32 => self.cached_i32, .u64 => self.cached_i64, .f32 => self.cached_f32, .f64 => self.cached_f64, .string => self.getStringStructType(), .any => self.getAnyStructType(), .noreturn => self.cached_void, .isize, .usize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64, else => self.toLLVMTypeInfo(ty), }; } fn toLLVMTypeInfo(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef { const info = self.ir_mod.types.get(ty); return switch (info) { .signed => |w| switch (w) { 1 => self.cached_i1, 8 => self.cached_i8, 16 => self.cached_i16, 32 => self.cached_i32, 64 => self.cached_i64, else => c.LLVMIntTypeInContext(self.context, w), }, .unsigned => |w| switch (w) { 1 => self.cached_i1, 8 => self.cached_i8, 16 => self.cached_i16, 32 => self.cached_i32, 64 => self.cached_i64, else => c.LLVMIntTypeInContext(self.context, w), }, .f32 => self.cached_f32, .f64 => self.cached_f64, .void => self.cached_void, .bool => self.cached_i1, .string => self.getStringStructType(), .pointer, .many_pointer, .function => self.cached_ptr, .closure => self.getClosureStructType(), .slice => self.getStringStructType(), // same {ptr, i64} layout .optional => |opt| { // ?*T / ?fn → bare pointer (null = none) const child_info = self.ir_mod.types.get(opt.child); if (child_info == .pointer or child_info == .many_pointer or child_info == .function) { return self.cached_ptr; } if (child_info == .closure) { return self.getClosureStructType(); } // ?Protocol → protocol struct (ctx ptr = field 0 is null when none). if (child_info == .@"struct" and child_info.@"struct".is_protocol) { return self.toLLVMType(opt.child); } // ?T → { T, i1 } var field_types: [2]c.LLVMTypeRef = .{ self.toLLVMType(opt.child), self.cached_i1, }; return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); }, .array => |arr| { const elem = self.toLLVMType(arr.element); return c.LLVMArrayType2(elem, arr.length); }, .vector => |vec| { const elem = self.toLLVMType(vec.element); return c.LLVMVectorType(elem, vec.length); }, .any => self.getAnyStructType(), .noreturn => self.cached_void, .@"struct" => |s| { // Build LLVM struct type from fields const n: c_uint = @intCast(s.fields.len); const field_llvm_types = self.alloc.alloc(c.LLVMTypeRef, s.fields.len) catch unreachable; defer self.alloc.free(field_llvm_types); for (s.fields, 0..) |field, j| { field_llvm_types[j] = self.toLLVMType(field.ty); } return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0); }, .@"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| { // Untagged union — just [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; 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 = .{ header_llvm, c.LLVMArrayType2(self.cached_i8, @intCast(max_size)), }; return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); }, .tuple => |t| { const n: c_uint = @intCast(t.fields.len); const field_llvm_types = self.alloc.alloc(c.LLVMTypeRef, t.fields.len) catch unreachable; defer self.alloc.free(field_llvm_types); for (t.fields, 0..) |f, j| { field_llvm_types[j] = self.toLLVMType(f); } return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0); }, .protocol => { // Protocol values: { ctx: *void, vtable_or_fn_ptrs... } // For now, use opaque ptr return self.cached_ptr; }, .usize, .isize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64, }; } // ── 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 (universal across all targets for foreign calls) 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; } // WASM32: usize/isize are pointer-sized (i32 on wasm32). // Other integer types (s64, u64) keep their declared size — they represent // genuinely 64-bit values (SDL_WindowFlags, timestamps, etc.). if (self.target_config.isWasm32()) { if (ir_ty == .usize or ir_ty == .isize) return self.cached_i32; return llvm_ty; } // 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 composite (> 16 bytes) → pass by reference: ptr + byval() at // the call/sig sites. LLVM's AArch64/x86_64 backend lowers byval to // the right ABI sequence (caller copy + indirect arg). return self.cached_ptr; } fn needsByval(self: *LLVMEmitter, ir_ty: TypeId, raw_llvm_ty: c.LLVMTypeRef) bool { if (self.target_config.isWasm32()) return false; if (ir_ty == .string) return false; if (!ir_ty.isBuiltin()) { const info = self.ir_mod.types.get(ir_ty); if (info == .slice) return false; } if (c.LLVMGetTypeKind(raw_llvm_ty) != c.LLVMStructTypeKind) return false; const n = c.LLVMCountStructElementTypes(raw_llvm_ty); if (n >= 1 and n <= 4) { var all_f = true; var all_d = true; var i: c_uint = 0; while (i < n) : (i += 1) { const ft = c.LLVMStructGetTypeAtIndex(raw_llvm_ty, i); const fk = c.LLVMGetTypeKind(ft); if (fk != c.LLVMFloatTypeKind) all_f = false; if (fk != c.LLVMDoubleTypeKind) all_d = false; } if (all_f or all_d) return false; } const size = c.LLVMABISizeOfType(c.LLVMGetModuleDataLayout(self.llvm_module), raw_llvm_ty); return size > 16; } fn materializeByvalArg(self: *LLVMEmitter, val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef) c.LLVMValueRef { const tmp = c.LLVMBuildAlloca(self.builder, struct_ty, "byval.tmp"); _ = c.LLVMBuildStore(self.builder, val, tmp); return tmp; } // ── Cached composite types ────────────────────────────────────── fn getStringStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.string_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ self.cached_ptr, // ptr self.cached_i64, // len }; self.string_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); return self.string_struct_type.?; } fn getAnyStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.any_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ self.cached_i64, // type tag self.cached_i64, // value }; self.any_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); return self.any_struct_type.?; } fn getClosureStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.closure_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ self.cached_ptr, // fn_ptr self.cached_ptr, // env }; self.closure_struct_type = c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); return self.closure_struct_type.?; } // ── 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 emitConstAggregate(self: *LLVMEmitter, agg: []const ir_inst.ConstantValue, llvm_ty: c.LLVMTypeRef) c.LLVMValueRef { const kind = c.LLVMGetTypeKind(llvm_ty); const is_struct = kind == c.LLVMStructTypeKind; const n: c_uint = @intCast(agg.len); const vals = self.alloc.alloc(c.LLVMValueRef, agg.len) catch return c.LLVMConstNull(llvm_ty); defer self.alloc.free(vals); for (agg, 0..) |cv, i| { const elem_ty = if (is_struct) c.LLVMStructGetTypeAtIndex(llvm_ty, @intCast(i)) else c.LLVMGetElementType(llvm_ty); vals[i] = switch (cv) { .int => |v| c.LLVMConstInt(elem_ty, @bitCast(v), 1), .float => |v| c.LLVMConstReal(elem_ty, v), .boolean => |v| c.LLVMConstInt(elem_ty, @intFromBool(v), 0), .string => |sid| self.emitConstStringGlobal(self.ir_mod.types.getString(sid)), .aggregate => |inner| self.emitConstAggregate(inner, elem_ty), else => c.LLVMConstNull(elem_ty), }; } if (is_struct) { return c.LLVMConstNamedStruct(llvm_ty, vals.ptr, n); } const elem_ty = c.LLVMGetElementType(llvm_ty); return c.LLVMConstArray(elem_ty, vals.ptr, n); } 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; defer self.alloc.free(str_z); // Create a global constant string and return a fat pointer { ptr, len } const str_global = c.LLVMBuildGlobalStringPtr(self.builder, str_z.ptr, "str"); const len_val = c.LLVMConstInt(self.cached_i64, str.len, 0); const str_ty = self.getStringStructType(); const undef = c.LLVMGetUndef(str_ty); const with_ptr = c.LLVMBuildInsertValue(self.builder, undef, str_global, 0, "str.ptr"); return c.LLVMBuildInsertValue(self.builder, with_ptr, len_val, 1, "str.len"); } // ── Reflection emission helpers ──────────────────────────────── /// Build (or return cached) a global constant array of {ptr, i64} string values /// for the field names of a struct type. fn getOrBuildFieldNameArray(self: *LLVMEmitter, struct_type: TypeId) c.LLVMValueRef { if (self.field_name_arrays.get(struct_type.index())) |g| return g; const info = self.ir_mod.types.get(struct_type); // Collect name StringIds from struct fields, union fields, or enum variants var name_ids = std.ArrayList(StringId).empty; defer name_ids.deinit(self.alloc); switch (info) { .@"struct" => |s| { for (s.fields) |f| name_ids.append(self.alloc, f.name) catch unreachable; }, .@"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; }, else => {}, } const string_ty = self.getStringStructType(); const n: u32 = @intCast(name_ids.items.len); // Build constant initializer: [N x {ptr, i64}] var field_vals = std.ArrayList(c.LLVMValueRef).empty; defer field_vals.deinit(self.alloc); for (name_ids.items) |name_id| { const name_str = self.ir_mod.types.getString(name_id); const str_z = self.alloc.dupeZ(u8, name_str) catch unreachable; defer self.alloc.free(str_z); const global_str = c.LLVMAddGlobal(self.llvm_module, c.LLVMArrayType(self.cached_i8, @intCast(name_str.len + 1)), "fld.str"); c.LLVMSetInitializer(global_str, c.LLVMConstStringInContext(self.context, str_z.ptr, @intCast(name_str.len + 1), 1)); c.LLVMSetGlobalConstant(global_str, 1); c.LLVMSetLinkage(global_str, c.LLVMPrivateLinkage); // Build fat pointer {ptr, len} as constant struct const len_val = c.LLVMConstInt(self.cached_i64, name_str.len, 0); var struct_fields = [2]c.LLVMValueRef{ global_str, len_val }; const const_struct = c.LLVMConstStructInContext(self.context, &struct_fields, 2, 0); field_vals.append(self.alloc, const_struct) catch unreachable; } // Create global array [N x {ptr, i64}] const array_ty = c.LLVMArrayType(string_ty, n); const array_init = c.LLVMConstArray(string_ty, field_vals.items.ptr, n); const global = c.LLVMAddGlobal(self.llvm_module, array_ty, "field_names"); c.LLVMSetInitializer(global, array_init); c.LLVMSetGlobalConstant(global, 1); c.LLVMSetLinkage(global, c.LLVMPrivateLinkage); self.field_name_arrays.put(struct_type.index(), global) catch unreachable; return global; } /// Emit field_value_get: switch on runtime index, each case extracts a field and boxes it as Any. fn emitFieldValueGet(self: *LLVMEmitter, fr: ir_inst.FieldReflect, func_idx: u32) void { const base_val = self.resolveRef(fr.base); const idx_val = self.resolveRef(fr.index); const info = self.ir_mod.types.get(fr.struct_type); const fields = switch (info) { .@"struct" => |s| s.fields, .@"union" => |u| u.fields, .tagged_union => |u| u.fields, else => &[_]TypeInfo.StructInfo.Field{}, }; if (fields.len == 0) { // No fields (e.g., plain enum) — return void-tagged Any so payload is empty const any_ty = self.getAnyStructType(); const void_tag = c.LLVMConstInt(self.cached_i64, TypeId.void.index(), 0); var void_any = c.LLVMGetUndef(any_ty); void_any = c.LLVMBuildInsertValue(self.builder, void_any, void_tag, 0, "fv.vtag"); void_any = c.LLVMBuildInsertValue(self.builder, void_any, c.LLVMConstInt(self.cached_i64, 0, 0), 1, "fv.vval"); self.mapRef(void_any); return; } const any_ty = self.getAnyStructType(); const current_func = self.func_map.get(func_idx) orelse { self.mapRef(c.LLVMGetUndef(any_ty)); return; }; // Create merge block const merge_bb = c.LLVMAppendBasicBlockInContext(self.context, current_func, "fv.merge"); // Create default block (returns undef) const default_bb = c.LLVMAppendBasicBlockInContext(self.context, current_func, "fv.default"); // Build switch const switch_inst = c.LLVMBuildSwitch(self.builder, idx_val, default_bb, @intCast(fields.len)); // Emit case blocks var case_blocks = std.ArrayList(c.LLVMBasicBlockRef).empty; defer case_blocks.deinit(self.alloc); var case_values = std.ArrayList(c.LLVMValueRef).empty; defer case_values.deinit(self.alloc); 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); c.LLVMPositionBuilderAtEnd(self.builder, case_bb); var field_val: c.LLVMValueRef = undefined; if (is_union) { // Union: extract payload via alloca + GEP to payload area + bitcast + load if (field.ty == .void) { // Void variant has no payload — use zero field_val = c.LLVMConstInt(self.cached_i64, 0, 0); } else { const base_ty = c.LLVMTypeOf(base_val); const tmp = c.LLVMBuildAlloca(self.builder, base_ty, "fv.utmp"); _ = c.LLVMBuildStore(self.builder, base_val, tmp); const payload_ptr = c.LLVMBuildStructGEP2(self.builder, base_ty, tmp, 1, "fv.pp"); const field_llvm_ty = self.toLLVMType(field.ty); const typed_ptr = c.LLVMBuildBitCast(self.builder, payload_ptr, self.cached_ptr, "fv.cast"); field_val = c.LLVMBuildLoad2(self.builder, field_llvm_ty, typed_ptr, "fv.field"); } } else { // Struct: direct extractvalue by field index field_val = c.LLVMBuildExtractValue(self.builder, base_val, @intCast(i), "fv.field"); } // Box as Any: {type_tag, value_as_i64} const tag = c.LLVMConstInt(self.cached_i64, self.anyTag(field.ty), 0); const is_field_signed = self.isSignedTypeEx(field.ty); const val_i64 = if (is_field_signed) self.coerceToI64Signed(field_val) else self.coerceToI64(field_val); var any_val = c.LLVMGetUndef(any_ty); any_val = c.LLVMBuildInsertValue(self.builder, any_val, tag, 0, "fv.tag"); any_val = c.LLVMBuildInsertValue(self.builder, any_val, val_i64, 1, "fv.val"); _ = c.LLVMBuildBr(self.builder, merge_bb); case_blocks.append(self.alloc, case_bb) catch unreachable; case_values.append(self.alloc, any_val) catch unreachable; } // Default block: return undef Any c.LLVMPositionBuilderAtEnd(self.builder, default_bb); _ = c.LLVMBuildBr(self.builder, merge_bb); // Merge block: PHI c.LLVMPositionBuilderAtEnd(self.builder, merge_bb); const phi = c.LLVMBuildPhi(self.builder, any_ty, "fv.phi"); // Add incoming from case blocks for (case_blocks.items, case_values.items) |bb, val| { c.LLVMAddIncoming(phi, @constCast(&val), @constCast(&bb), 1); } // Add incoming from default block const undef_any = c.LLVMGetUndef(any_ty); c.LLVMAddIncoming(phi, @constCast(&undef_any), @constCast(&default_bb), 1); self.mapRef(phi); } // ── Helpers ───────────────────────────────────────────────────── fn makeBlockKey(func_idx: u32, block_idx: u32) u64 { return (@as(u64, func_idx) << 32) | @as(u64, block_idx); } /// Dump the LLVM module to a string (for testing). pub fn dumpToString(self: *LLVMEmitter) []const u8 { const raw = c.LLVMPrintModuleToString(self.llvm_module); return std.mem.span(raw); } /// Verify the LLVM module. Returns true if valid. pub fn verify(self: *LLVMEmitter) bool { return c.LLVMVerifyModule(self.llvm_module, c.LLVMReturnStatusAction, null) == 0; } /// Verify the LLVM module, returning an error message on failure. pub fn verifyWithMessage(self: *LLVMEmitter) !void { var err_msg: [*c]u8 = null; 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); } return error.VerificationFailed; } } /// Print the LLVM IR to stderr. pub fn printIR(self: *LLVMEmitter) void { const ir_str = c.LLVMPrintModuleToString(self.llvm_module); defer c.LLVMDisposeMessage(ir_str); const len = std.mem.len(ir_str); std.debug.print("{s}\n", .{ir_str[0..len]}); } /// Emit the module as an object file to disk. pub fn emitObject(self: *LLVMEmitter, output_path: [*:0]const u8) !void { return self.emitToFile(output_path, c.LLVMObjectFile); } /// Emit the module as an assembly file to disk. pub fn emitAssembly(self: *LLVMEmitter, output_path: [*:0]const u8) !void { return self.emitToFile(output_path, c.LLVMAssemblyFile); } /// Emit the module as LLVM bitcode to disk (for emcc to recompile with a newer LLVM). pub fn emitBitcode(self: *LLVMEmitter, output_path: [*:0]const u8) !void { if (c.LLVMWriteBitcodeToFile(self.llvm_module, output_path) != 0) { return error.EmitFailed; } } /// Dump the LLVM IR to a file for debugging. pub fn dumpIRToFile(self: *LLVMEmitter, path: [*:0]const u8) void { _ = c.LLVMPrintModuleToFile(self.llvm_module, path, null); } /// Emit the module as an object file to a memory buffer (for JIT). pub fn emitObjectToMemory(self: *LLVMEmitter) !c.LLVMMemoryBufferRef { const tm = self.target_machine orelse return error.NoTargetMachine; var err_msg: [*c]u8 = null; var buf: c.LLVMMemoryBufferRef = null; if (c.LLVMTargetMachineEmitToMemoryBuffer(tm, self.llvm_module, c.LLVMObjectFile, &err_msg, &buf) != 0) { if (err_msg != null) { std.debug.print("failed to emit object to memory: {s}\n", .{std.mem.span(err_msg)}); c.LLVMDisposeMessage(err_msg); } return error.EmitFailed; } return buf; } fn emitToFile(self: *LLVMEmitter, output_path: [*:0]const u8, file_type: c.LLVMCodeGenFileType) !void { const tm = self.target_machine orelse return error.NoTargetMachine; var err_msg: [*c]u8 = null; if (c.LLVMTargetMachineEmitToFile(tm, self.llvm_module, output_path, file_type, &err_msg) != 0) { if (err_msg != null) { std.debug.print("failed to emit file: {s}\n", .{std.mem.span(err_msg)}); c.LLVMDisposeMessage(err_msg); } 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(); // Check function parameters first (refs 0..N-1) if (ref_idx < func.params.len) { const ty = func.params[ref_idx].ty; return ty == .u8 or ty == .u16 or ty == .u32 or ty == .u64; } 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 ───────────────────────────────────── fn isFloatType(ty: TypeId) bool { return ty == .f32 or ty == .f64; } /// Check if a TypeId is a float type, including float vectors. fn isFloatOrVecFloat(ty: TypeId, types: *const TypeTable) bool { if (ty == .f32 or ty == .f64) return true; if (!ty.isBuiltin()) { const info = types.get(ty); if (info == .vector) return info.vector.element == .f32 or info.vector.element == .f64; } return false; } fn isSignedType(ty: TypeId) bool { return switch (ty) { .s8, .s16, .s32, .s64, .isize => true, else => false, }; } fn floatBits(ty: TypeId) u32 { return switch (ty) { .f32 => 32, .f64 => 64, else => 0, }; } fn intBits(ty: TypeId) u32 { return switch (ty) { .s8, .u8 => 8, .s16, .u16 => 16, .s32, .u32 => 32, .s64, .u64 => 64, .bool => 1, .usize, .isize => 0, // target-dependent — caller must query pointer_size else => 64, }; }