diff --git a/src/backend/llvm/ops.zig b/src/backend/llvm/ops.zig index 36f66bb..f5174bc 100644 --- a/src/backend/llvm/ops.zig +++ b/src/backend/llvm/ops.zig @@ -4,8 +4,10 @@ const c = llvm.c; const emit = @import("../../ir/emit_llvm.zig"); const ir_inst = @import("../../ir/inst.zig"); const ir_types = @import("../../ir/types.zig"); +const interp_mod = @import("../../ir/interp.zig"); const LLVMEmitter = emit.LLVMEmitter; +const Interpreter = interp_mod.Interpreter; const Inst = ir_inst.Inst; const BinOp = ir_inst.BinOp; const UnaryOp = ir_inst.UnaryOp; @@ -14,17 +16,24 @@ const Conversion = ir_inst.Conversion; const GlobalId = ir_inst.GlobalId; const GlobalSet = ir_inst.GlobalSet; const FuncId = ir_inst.FuncId; +const Call = ir_inst.Call; +const CallIndirect = ir_inst.CallIndirect; +const ObjcMsgSend = ir_inst.ObjcMsgSend; +const JniMsgSend = ir_inst.JniMsgSend; +const BuiltinCall = ir_inst.BuiltinCall; const TypeId = ir_types.TypeId; const StringId = ir_types.StringId; /// Instruction-emission handlers for `emitInst`: the constant, arithmetic, -/// bitwise, comparison, logical, memory, globals, conversion, and pointer -/// opcodes. A backend `*LLVMEmitter` facade (field `e`): each method emits one -/// opcode's LLVM IR via `self.e.*`. The shared infra these bodies call back into -/// (`mapRef`/`resolveRef`/`matchBinOpTypes`/`emitCmp`/`emitCmpOrdered`/ -/// `emitStrCmp`/`emitStringConstant`/`reflection`/`emitConversion`/`coerceArg`/ -/// `getRefIRType`) stays on `LLVMEmitter`. `emitInst`'s arms reach these via -/// `self.ops()`. +/// bitwise, comparison, logical, memory, globals, conversion, pointer, and +/// call opcodes (direct/indirect/objc/jni dispatch plus builtin, compiler, +/// and closure calls). A backend `*LLVMEmitter` facade (field `e`): each +/// method emits one opcode's LLVM IR via `self.e.*`. The shared infra these +/// bodies call back into (`mapRef`/`resolveRef`/`matchBinOpTypes`/`emitCmp`/ +/// `emitCmpOrdered`/`emitStrCmp`/`emitStringConstant`/`reflection`/ +/// `emitConversion`/`coerceArg`/`getRefIRType`/`loadJniFn`/`extractSlicePtr`/ +/// `emitJniConstructor`) stays on `LLVMEmitter`. `emitInst`'s arms reach these +/// via `self.ops()`. pub const Ops = struct { e: *LLVMEmitter, @@ -450,4 +459,689 @@ pub const Ops = struct { const llvm_ty = self.e.toLLVMType(instruction.ty); self.e.mapRef(c.LLVMBuildLoad2(self.e.builder, llvm_ty, ptr, "deref")); } + + // ── Calls ───────────────────────────────────────────── + pub fn emitObjcMsgSend(self: Ops, instruction: *const Inst, msg: ObjcMsgSend) void { + const msg_send = self.e.getObjcMsgSendValue(); + // Detect the sret case: >16 B non-HFA struct return. + // Same predicate as the plain-foreign-call path so the + // two arms stay in lockstep. + const raw_ret_ty = self.e.toLLVMType(instruction.ty); + const uses_sret = self.e.needsByval(instruction.ty, raw_ret_ty); + const ret_ty = if (uses_sret) self.e.cached_void else raw_ret_ty; + + // Slot layout: + // uses_sret = false → [recv, sel, args...] + // uses_sret = true → [sret_slot, recv, sel, args...] + const sret_off: usize = if (uses_sret) 1 else 0; + const total_params: usize = 2 + msg.args.len + sret_off; + const param_types = self.e.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable; + defer self.e.alloc.free(param_types); + const call_args = self.e.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable; + defer self.e.alloc.free(call_args); + + var sret_slot: c.LLVMValueRef = null; + if (uses_sret) { + sret_slot = c.LLVMBuildAlloca(self.e.builder, raw_ret_ty, "objc.sret"); + param_types[0] = self.e.cached_ptr; + call_args[0] = sret_slot; + } + + // recv (typed *void from the IR) + param_types[sret_off] = self.e.cached_ptr; + call_args[sret_off] = self.e.coerceArg(self.e.resolveRef(msg.recv), self.e.cached_ptr); + // sel (loaded SEL — opaque ptr) + param_types[sret_off + 1] = self.e.cached_ptr; + call_args[sret_off + 1] = self.e.coerceArg(self.e.resolveRef(msg.sel), self.e.cached_ptr); + // additional args take their IR types, with ABI + // coercion applied so structs / strings decay the + // same way they do for any C foreign call. + for (msg.args, 0..) |arg_ref, i| { + const raw_ty = self.e.getRefIRType(arg_ref) orelse .void; + const raw_llvm = self.e.toLLVMType(raw_ty); + const coerced_ty = self.e.abiCoerceParamType(raw_ty, raw_llvm); + param_types[i + 2 + sret_off] = coerced_ty; + call_args[i + 2 + sret_off] = self.e.coerceArg(self.e.resolveRef(arg_ref), coerced_ty); + } + + const fn_ty = c.LLVMFunctionType(ret_ty, param_types.ptr, @intCast(total_params), 0); + const call_label: [*:0]const u8 = if (instruction.ty == .void or uses_sret) "" else "objc.msg"; + var result = c.LLVMBuildCall2(self.e.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label); + if (uses_sret) { + // Tag the call's arg 0 (sret slot) with the sret + // attribute so the AArch64 / SysV backends route + // through the x8 / hidden-pointer convention. + const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4); + const sret_attr = c.LLVMCreateTypeAttribute(self.e.context, sret_kind, raw_ret_ty); + const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1)); + c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr); + result = c.LLVMBuildLoad2(self.e.builder, raw_ret_ty, sret_slot, "objc.sret.load"); + } + // Always mapRef — the IR Ref counter for this + // instruction advances regardless of return type, + // so skipping it would misalign every subsequent + // ref lookup in this function. + self.e.mapRef(result); + } + + pub fn emitJniMsgSend(self: Ops, instruction: *const Inst, msg: JniMsgSend) void { + // JNI vtable indirection: + // ifs = *env // JNINativeInterface* + // instance: cls = ifs[GetObjectClass](env, target) + // mid = ifs[GetMethodID](env, cls, name, sig) + // ifs[CallMethod](env, target, mid, args...) + // static: target IS the jclass — skip GetObjectClass + // mid = ifs[GetStaticMethodID](env, target, name, sig) + // ifs[CallStaticMethod](env, target, mid, args...) + // ctor: cls = ifs[FindClass](env, parent_class_path) + // mid = ifs[GetMethodID](env, cls, "", sig) + // ifs[NewObject](env, cls, mid, args...) → jobject + // nonvirt: handled below via FindClass + GetMethodID + + // CallNonvirtualMethod. + // The cached path (msg.cache_key != null) still shares one + // (jclass GlobalRef, jmethodID) pair per literal (name, sig). + if (msg.is_constructor) { + self.e.emitJniConstructor(msg, instruction.ty); + return; + } + const ret_ty_id = instruction.ty; + const is_pointer_ret = switch (self.e.ir_mod.types.get(ret_ty_id)) { + .pointer, .many_pointer => true, + else => false, + }; + const call_method_offset: u32 = if (msg.is_static) blk: { + if (is_pointer_ret) break :blk emit.Jni.CallStaticObjectMethod; + break :blk switch (ret_ty_id) { + .void => emit.Jni.CallStaticVoidMethod, + .s32 => emit.Jni.CallStaticIntMethod, + .s64 => emit.Jni.CallStaticLongMethod, + .f32 => emit.Jni.CallStaticFloatMethod, + .f64 => emit.Jni.CallStaticDoubleMethod, + .bool => emit.Jni.CallStaticBooleanMethod, + else => { + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + return; + }, + }; + } else if (msg.is_nonvirtual) blk: { + if (is_pointer_ret) break :blk emit.Jni.CallNonvirtualObjectMethod; + break :blk switch (ret_ty_id) { + .void => emit.Jni.CallNonvirtualVoidMethod, + .s32 => emit.Jni.CallNonvirtualIntMethod, + .s64 => emit.Jni.CallNonvirtualLongMethod, + .f32 => emit.Jni.CallNonvirtualFloatMethod, + .f64 => emit.Jni.CallNonvirtualDoubleMethod, + .bool => emit.Jni.CallNonvirtualBooleanMethod, + else => { + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + return; + }, + }; + } else blk: { + if (is_pointer_ret) break :blk emit.Jni.CallObjectMethod; + break :blk switch (ret_ty_id) { + .void => emit.Jni.CallVoidMethod, + .s32 => emit.Jni.CallIntMethod, + .s64 => emit.Jni.CallLongMethod, + .f32 => emit.Jni.CallFloatMethod, + .f64 => emit.Jni.CallDoubleMethod, + .bool => emit.Jni.CallBooleanMethod, + else => { + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + return; + }, + }; + }; + const get_mid_offset: u32 = if (msg.is_static) emit.Jni.GetStaticMethodID else emit.Jni.GetMethodID; + + const env = self.e.resolveRef(msg.env); + const target = self.e.resolveRef(msg.target); + // String literals lower as `{ptr, i64}` slices in sx IR; + // JNI's `GetMethodID` expects raw C strings, so extract + // field 0 when the source is a slice. + const name_ptr = self.e.extractSlicePtr(self.e.resolveRef(msg.name)); + const sig_ptr = self.e.extractSlicePtr(self.e.resolveRef(msg.sig)); + + const ifs = c.LLVMBuildLoad2(self.e.builder, self.e.cached_ptr, env, "jni.ifs"); + + // Method-ID resolution. When `name` and `sig` are both + // string literals the call site participates in + // `(name, sig)` slot interning (step 1.17): a shared + // pair of static globals holds the `jclass` GlobalRef + // and the `jmethodID`, populated lazily on the first + // call to any matching site. Non-literal sites fall + // back to the per-call `GetObjectClass + GetMethodID` + // sequence (1.15 shape). + const mid = if (msg.cache_key) |ck| blk: { + const pair = self.e.ffiCtors().getOrCreateJniSlots(ck.name_str, ck.sig_str); + const cached_mid = c.LLVMBuildLoad2(self.e.builder, self.e.cached_ptr, pair.mid_slot, "jni.cached.mid"); + const is_cached = c.LLVMBuildICmp(self.e.builder, c.LLVMIntNE, cached_mid, c.LLVMConstNull(self.e.cached_ptr), "jni.is.cached"); + + const cur_fn = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.e.builder)); + const miss_bb = c.LLVMAppendBasicBlockInContext(self.e.context, cur_fn, "jni.miss"); + const cont_bb = c.LLVMAppendBasicBlockInContext(self.e.context, cur_fn, "jni.cont"); + const before_bb = c.LLVMGetInsertBlock(self.e.builder); + _ = c.LLVMBuildCondBr(self.e.builder, is_cached, cont_bb, miss_bb); + + // Miss path: + // instance: GetObjectClass → NewGlobalRef → GetMethodID + // static: target IS class → NewGlobalRef(target) → GetStaticMethodID + c.LLVMPositionBuilderAtEnd(self.e.builder, miss_bb); + const local_cls = if (msg.is_static) target else inst_cls: { + const get_obj_cls = self.e.loadJniFn(ifs, emit.Jni.GetObjectClass, "jni.GetObjectClass"); + var gocls_params = [_]c.LLVMTypeRef{ self.e.cached_ptr, self.e.cached_ptr }; + const gocls_ty = c.LLVMFunctionType(self.e.cached_ptr, &gocls_params, 2, 0); + var gocls_args = [_]c.LLVMValueRef{ env, target }; + break :inst_cls c.LLVMBuildCall2(self.e.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls"); + }; + const new_global_ref = self.e.loadJniFn(ifs, emit.Jni.NewGlobalRef, "jni.NewGlobalRef"); + var ngref_params = [_]c.LLVMTypeRef{ self.e.cached_ptr, self.e.cached_ptr }; + const ngref_ty = c.LLVMFunctionType(self.e.cached_ptr, &ngref_params, 2, 0); + var ngref_args = [_]c.LLVMValueRef{ env, local_cls }; + const global_cls = c.LLVMBuildCall2(self.e.builder, ngref_ty, new_global_ref, &ngref_args, 2, "jni.global.cls"); + _ = c.LLVMBuildStore(self.e.builder, global_cls, pair.cls_slot); + const get_mid = self.e.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "jni.GetMethodID"); + var gmid_params = [_]c.LLVMTypeRef{ self.e.cached_ptr, self.e.cached_ptr, self.e.cached_ptr, self.e.cached_ptr }; + const gmid_ty = c.LLVMFunctionType(self.e.cached_ptr, &gmid_params, 4, 0); + var gmid_args = [_]c.LLVMValueRef{ env, global_cls, name_ptr, sig_ptr }; + const fresh_mid = c.LLVMBuildCall2(self.e.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.fresh.mid"); + _ = c.LLVMBuildStore(self.e.builder, fresh_mid, pair.mid_slot); + const miss_end_bb = c.LLVMGetInsertBlock(self.e.builder); + _ = c.LLVMBuildBr(self.e.builder, cont_bb); + + // Cont: phi the cached vs fresh mid. + c.LLVMPositionBuilderAtEnd(self.e.builder, cont_bb); + const phi = c.LLVMBuildPhi(self.e.builder, self.e.cached_ptr, "jni.mid"); + var phi_vals = [_]c.LLVMValueRef{ cached_mid, fresh_mid }; + var phi_blocks = [_]c.LLVMBasicBlockRef{ before_bb, miss_end_bb }; + c.LLVMAddIncoming(phi, &phi_vals, &phi_blocks, 2); + break :blk phi; + } else blk: { + const cls = if (msg.is_static) target else if (msg.is_nonvirtual) nonvirt_cls: { + // `super.method(args)`: dispatch is bound to a + // specific class (the parent), not subclass-override. + // Resolve via FindClass(parent_path). No caching yet — + // per-call lookup. The parent path is a NUL-terminated + // C string emitted as a private LLVM global. + const path = msg.parent_class_path orelse ""; + const path_global = self.e.emitCStringGlobal(path, "jni.parent.path"); + const find_class = self.e.loadJniFn(ifs, emit.Jni.FindClass, "jni.FindClass"); + var fc_params = [_]c.LLVMTypeRef{ self.e.cached_ptr, self.e.cached_ptr }; + const fc_ty = c.LLVMFunctionType(self.e.cached_ptr, &fc_params, 2, 0); + var fc_args = [_]c.LLVMValueRef{ env, path_global }; + break :nonvirt_cls c.LLVMBuildCall2(self.e.builder, fc_ty, find_class, &fc_args, 2, "jni.parent.cls"); + } else inst_cls: { + const get_obj_cls = self.e.loadJniFn(ifs, emit.Jni.GetObjectClass, "jni.GetObjectClass"); + var gocls_params = [_]c.LLVMTypeRef{ self.e.cached_ptr, self.e.cached_ptr }; + const gocls_ty = c.LLVMFunctionType(self.e.cached_ptr, &gocls_params, 2, 0); + var gocls_args = [_]c.LLVMValueRef{ env, target }; + break :inst_cls c.LLVMBuildCall2(self.e.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls"); + }; + const get_mid = self.e.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "jni.GetMethodID"); + var gmid_params = [_]c.LLVMTypeRef{ self.e.cached_ptr, self.e.cached_ptr, self.e.cached_ptr, self.e.cached_ptr }; + const gmid_ty = c.LLVMFunctionType(self.e.cached_ptr, &gmid_params, 4, 0); + var gmid_args = [_]c.LLVMValueRef{ env, cls, name_ptr, sig_ptr }; + const mid_val = c.LLVMBuildCall2(self.e.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.mid"); + if (msg.is_nonvirtual) { + // Stash cls in a dummy slot so the call site below + // can pick it up. Easiest path: do the call right + // here and return Ref.none, but we need to keep the + // outer phi shape. Instead, return both via tuple + // through an auxiliary local — simplest is to attach + // `cls` to a per-invocation slot. Use a stack alloca. + const cls_slot = c.LLVMBuildAlloca(self.e.builder, self.e.cached_ptr, "jni.parent.cls.slot"); + _ = c.LLVMBuildStore(self.e.builder, cls, cls_slot); + // Tag the slot pointer onto the phi result via the + // generated metadata: we'll re-extract by re-running + // FindClass — actually simpler: lower nonvirtual on + // the spot below. Drop the implicit `break` here: + const call_fn = self.e.loadJniFn(ifs, call_method_offset, "jni.callfn.nonvirtual"); + const raw_ret = self.e.toLLVMType(ret_ty_id); + const total_call_params_nv: usize = 4 + msg.args.len; + const call_param_types_nv = self.e.alloc.alloc(c.LLVMTypeRef, total_call_params_nv) catch unreachable; + defer self.e.alloc.free(call_param_types_nv); + const call_args_nv = self.e.alloc.alloc(c.LLVMValueRef, total_call_params_nv) catch unreachable; + defer self.e.alloc.free(call_args_nv); + call_param_types_nv[0] = self.e.cached_ptr; + call_param_types_nv[1] = self.e.cached_ptr; + call_param_types_nv[2] = self.e.cached_ptr; + call_param_types_nv[3] = self.e.cached_ptr; + call_args_nv[0] = env; + call_args_nv[1] = target; + call_args_nv[2] = cls; + call_args_nv[3] = mid_val; + for (msg.args, 0..) |arg_ref, i| { + const raw_ty = self.e.getRefIRType(arg_ref) orelse .void; + const raw_llvm = self.e.toLLVMType(raw_ty); + const coerced_ty = self.e.abiCoerceParamType(raw_ty, raw_llvm); + call_param_types_nv[i + 4] = coerced_ty; + call_args_nv[i + 4] = self.e.coerceArg(self.e.resolveRef(arg_ref), coerced_ty); + } + const call_fn_ty_nv = c.LLVMFunctionType(raw_ret, call_param_types_nv.ptr, @intCast(total_call_params_nv), 0); + const label_nv: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.nonvirtual.ret"; + const result_nv = c.LLVMBuildCall2(self.e.builder, call_fn_ty_nv, call_fn, call_args_nv.ptr, @intCast(total_call_params_nv), label_nv); + self.e.mapRef(result_nv); + return; + } + break :blk mid_val; + }; + + // CallMethod: (JNIEnv*, jobject, jmethodID, args...) -> RetTy + const call_fn = self.e.loadJniFn(ifs, call_method_offset, "jni.callfn"); + const raw_ret = self.e.toLLVMType(ret_ty_id); + const total_call_params: usize = 3 + msg.args.len; + const call_param_types = self.e.alloc.alloc(c.LLVMTypeRef, total_call_params) catch unreachable; + defer self.e.alloc.free(call_param_types); + const call_args = self.e.alloc.alloc(c.LLVMValueRef, total_call_params) catch unreachable; + defer self.e.alloc.free(call_args); + call_param_types[0] = self.e.cached_ptr; + call_param_types[1] = self.e.cached_ptr; + call_param_types[2] = self.e.cached_ptr; + call_args[0] = env; + call_args[1] = target; + call_args[2] = mid; + for (msg.args, 0..) |arg_ref, i| { + const raw_ty = self.e.getRefIRType(arg_ref) orelse .void; + const raw_llvm = self.e.toLLVMType(raw_ty); + const coerced_ty = self.e.abiCoerceParamType(raw_ty, raw_llvm); + call_param_types[i + 3] = coerced_ty; + call_args[i + 3] = self.e.coerceArg(self.e.resolveRef(arg_ref), coerced_ty); + } + const call_fn_ty = c.LLVMFunctionType(raw_ret, call_param_types.ptr, @intCast(total_call_params), 0); + const label: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.ret"; + const result = c.LLVMBuildCall2(self.e.builder, call_fn_ty, call_fn, call_args.ptr, @intCast(total_call_params), label); + self.e.mapRef(result); + } + + pub fn emitCall(self: Ops, instruction: *const Inst, call_op: Call) void { + // Evaluate comptime functions at compile time + const callee_func = &self.e.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.e.ir_mod, self.e.alloc); + interp_inst.build_config = &self.e.build_config; + if (self.e.import_sources) |sm| interp_inst.setSourceMap(sm); + defer interp_inst.deinit(); + if (interp_inst.call(call_op.callee, &.{})) |result| { + if (result.asInt()) |v| { + self.e.mapRef(c.LLVMConstInt(self.e.toLLVMType(instruction.ty), @bitCast(v), 0)); + return; + } else if (result.asFloat()) |v| { + self.e.mapRef(c.LLVMConstReal(self.e.toLLVMType(instruction.ty), v)); + return; + } else if (result.asBool()) |v| { + self.e.mapRef(c.LLVMConstInt(self.e.toLLVMType(instruction.ty), @intFromBool(v), 0)); + return; + } else if (result == .string) { + self.e.mapRef(self.e.emitStringConstant(result.string)); + return; + } + } else |_| {} + } + const callee = self.e.func_map.get(call_op.callee.index()) orelse { + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + return; + }; + const callee_needs_c_abi = callee_func.is_extern or callee_func.call_conv == .c; + const callee_raw_ret = self.e.toLLVMType(callee_func.ret); + const callee_uses_sret = callee_needs_c_abi and self.e.needsByval(callee_func.ret, callee_raw_ret); + + // When the callee uses sret, prepend an alloca for the result. + // Index alignment: actual_args[0] = sret_slot; actual_args[i+1] = sx arg i. + const sret_off: usize = if (callee_uses_sret) 1 else 0; + const total_args = call_op.args.len + sret_off; + const args = self.e.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable; + defer self.e.alloc.free(args); + var sret_slot: c.LLVMValueRef = null; + if (callee_uses_sret) { + sret_slot = c.LLVMBuildAlloca(self.e.builder, callee_raw_ret, "sret.slot"); + args[0] = sret_slot; + } + for (call_op.args, 0..) |arg_ref, j| { + args[j + sret_off] = self.e.resolveRef(arg_ref); + } + const arg_count: c_uint = @intCast(total_args); + + // Get the function type from LLVM and coerce arguments + const fn_ty = c.LLVMGlobalGetValueType(callee); + const param_count = c.LLVMCountParamTypes(fn_ty); + if (param_count > 0) { + const param_types = self.e.alloc.alloc(c.LLVMTypeRef, param_count) catch unreachable; + defer self.e.alloc.free(param_types); + c.LLVMGetParamTypes(fn_ty, param_types.ptr); + for (0..@min(args.len, param_count)) |j| { + // The sret slot is already a properly-typed pointer; skip coercion. + if (callee_uses_sret and j == 0) continue; + const fn_param_idx = j - sret_off; + // Materialize byval args before coercion so we pass a ptr instead of the struct value. + if (callee_needs_c_abi and fn_param_idx < callee_func.params.len) { + const ir_ty = callee_func.params[fn_param_idx].ty; + const raw_struct = self.e.toLLVMType(ir_ty); + if (self.e.needsByval(ir_ty, raw_struct)) { + args[j] = self.e.materializeByvalArg(args[j], raw_struct); + continue; + } + } + args[j] = self.e.coerceArg(args[j], param_types[j]); + } + } + // A `void`/`noreturn` call has no value, so it must stay + // unnamed (LLVM rejects a named void result). + const call_is_void_like = instruction.ty == .void or instruction.ty == .noreturn; + const call_label: [*:0]const u8 = if (call_is_void_like or callee_uses_sret) "" else "call"; + var result = c.LLVMBuildCall2(self.e.builder, fn_ty, callee, args.ptr, arg_count, call_label); + if (callee_uses_sret) { + // Mirror the function-decl `sret()` attribute on the call site so the + // LLVM backend lowers arg 0 via x8 (AAPCS64) / hidden ptr (SysV AMD64). + const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4); + const sret_attr = c.LLVMCreateTypeAttribute(self.e.context, sret_kind, callee_raw_ret); + const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1)); + c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr); + // Load the actual struct value the callee wrote into the slot. + result = c.LLVMBuildLoad2(self.e.builder, callee_raw_ret, sret_slot, "sret.load"); + } else if (!call_is_void_like and callee_func.is_extern) { + // Coerce ABI return value (e.g. i64 / [2 x i64]) back to IR struct type if needed + const expected_ty = self.e.toLLVMType(instruction.ty); + result = self.e.coerceArg(result, expected_ty); + } + self.e.mapRef(result); + } + + pub fn emitCallIndirect(self: Ops, instruction: *const Inst, call_op: CallIndirect) void { + const callee = self.e.resolveRef(call_op.callee); + const arg_count: c_uint = @intCast(call_op.args.len); + const args = self.e.alloc.alloc(c.LLVMValueRef, call_op.args.len) catch unreachable; + defer self.e.alloc.free(args); + for (call_op.args, 0..) |arg_ref, j| { + args[j] = self.e.resolveRef(arg_ref); + } + + // Get callee's IR type to resolve parameter types accurately + const callee_ir_ty = self.e.getRefIRType(call_op.callee); + const fn_params: ?[]const TypeId = if (callee_ir_ty) |cty| blk: { + if (!cty.isBuiltin()) { + const ci = self.e.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.e.ir_mod.types.get(cty); + if (ci == .function and ci.function.call_conv == .c) break :blk true; + } + break :blk false; + } else false; + + // Default-conv fn-pointers under implicit-ctx carry a hidden + // `*void` (the implicit __sx_ctx) at LLVM slot 0. The IR fn + // type does not include it, so shift fn_params lookups by 1. + const fp_ctx_slots: usize = if (callee_ir_ty) |cty| blk: { + if (!self.e.ir_mod.has_implicit_ctx) break :blk 0; + if (cty.isBuiltin()) break :blk 0; + const ci = self.e.ir_mod.types.get(cty); + switch (ci) { + .function => |f| break :blk if (f.call_conv == .c) @as(usize, 0) else 1, + else => break :blk 0, + } + } else 0; + + const ret_ty = if (callee_ir_ty) |cty| blk: { + if (!cty.isBuiltin()) { + const ci = self.e.ir_mod.types.get(cty); + switch (ci) { + .function => |f| break :blk self.e.toLLVMType(f.ret), + .closure => |cl| break :blk self.e.toLLVMType(cl.ret), + else => {}, + } + } + break :blk self.e.toLLVMType(instruction.ty); + } else self.e.toLLVMType(instruction.ty); + + const param_tys = self.e.alloc.alloc(c.LLVMTypeRef, call_op.args.len) catch unreachable; + defer self.e.alloc.free(param_tys); + if (fn_params) |fp| { + for (0..call_op.args.len) |j| { + // Slots 0..fp_ctx_slots are the implicit __sx_ctx + // (passed as opaque ptr; not in fp). + if (j < fp_ctx_slots) { + param_tys[j] = self.e.cached_ptr; + args[j] = self.e.coerceArg(args[j], self.e.cached_ptr); + continue; + } + const fp_idx = j - fp_ctx_slots; + if (fp_idx < fp.len) { + const raw_struct = self.e.toLLVMType(fp[fp_idx]); + if (fp_is_c_abi and self.e.needsByval(fp[fp_idx], raw_struct)) { + args[j] = self.e.materializeByvalArg(args[j], raw_struct); + param_tys[j] = self.e.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.e.cached_ptr; + } + param_tys[j] = llvm_pty; + args[j] = self.e.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); + const icall_void_like = instruction.ty == .void or instruction.ty == .noreturn; + var result = c.LLVMBuildCall2(self.e.builder, fn_ty, callee, args.ptr, arg_count, if (icall_void_like) "" else "icall"); + + // Coerce call result to instruction's expected type + const expected_ty = self.e.toLLVMType(instruction.ty); + if (!icall_void_like and c.LLVMTypeOf(result) != expected_ty) { + result = self.e.coerceArg(result, expected_ty); + } + self.e.mapRef(result); + } + + // ── Call extensions ─────────────────────────────────────── + pub fn emitCallBuiltin(self: Ops, instruction: *const Inst, bi: BuiltinCall) void { + // Builtins that map to libc functions or LLVM intrinsics + switch (bi.builtin) { + .sqrt, .sin, .cos, .floor => { + const val = self.e.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.e.getOrDeclareMathF32(bi.builtin); + var args = [_]c.LLVMValueRef{val}; + self.e.mapRef(c.LLVMBuildCall2(self.e.builder, self.e.getMathF32Type(), f, &args, 1, @tagName(bi.builtin))); + } else { + const coerced = if (val_kind != c.LLVMDoubleTypeKind) self.e.coerceArg(val, self.e.cached_f64) else val; + const f = self.e.getOrDeclareMathF64(bi.builtin); + var args = [_]c.LLVMValueRef{coerced}; + self.e.mapRef(c.LLVMBuildCall2(self.e.builder, self.e.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.e.resolveRef(bi.args[0]); + const raw_ptr = c.LLVMBuildExtractValue(self.e.builder, str_val, 0, "str.ptr"); + const str_len = c.LLVMBuildExtractValue(self.e.builder, str_val, 1, "str.len"); + // On wasm32, count param is i32 (size_t) + const count = if (self.e.target_config.isWasm32()) + c.LLVMBuildTrunc(self.e.builder, str_len, self.e.cached_i32, "len.tr") + else + str_len; + const write_fn = self.e.getOrDeclareWrite(); + var write_args = [_]c.LLVMValueRef{ + c.LLVMConstInt(self.e.cached_i32, 1, 0), // fd = stdout + raw_ptr, + count, + }; + _ = c.LLVMBuildCall2(self.e.builder, self.e.getWriteType(), write_fn, &write_args, 3, ""); + self.e.advanceRefCounter(); + }, + .type_name => { + // Dynamic `type_name(t)` at runtime: extract + // the TypeId from the arg (an Any-boxed Type + // value: tag=`.s64.index()`, value=tid), GEP + // into the compiler-emitted `__sx_type_names` + // global, load the string. The arg's LLVM + // shape is the `{i64, i64}` Any aggregate + // (because the IR-side arg type is `.any` + // when boxed); for unboxed direct call sites + // (the arg IR type is `.s64` from + // `const_type`), the value IS the TypeId + // index directly. + const arg_ref = bi.args[0]; + const arg_val = self.e.resolveRef(arg_ref); + const arg_ir_ty = self.e.getRefIRType(arg_ref) orelse TypeId.s64; + const tid_idx = blk: { + if (arg_ir_ty == .any) { + // Boxed: extract value field. + break :blk c.LLVMBuildExtractValue(self.e.builder, arg_val, 1, "tn.tid"); + } + // Bare i64 (TypeId index). + break :blk arg_val; + }; + const arr_global = self.e.reflection().getOrBuildTypeNameArray(); + const arr_len = self.e.type_name_array_len; + const string_ty = self.e.getStringStructType(); + const arr_ty = c.LLVMArrayType(string_ty, arr_len); + const zero = c.LLVMConstInt(self.e.cached_i64, 0, 0); + var indices = [2]c.LLVMValueRef{ zero, tid_idx }; + const gep = c.LLVMBuildInBoundsGEP2(self.e.builder, arr_ty, arr_global, &indices, 2, "tn.gep"); + const result = c.LLVMBuildLoad2(self.e.builder, string_ty, gep, "tn.load"); + self.e.mapRef(result); + }, + .type_eq => { + // Dynamic `type_eq(a, b)` — both args are + // Type values. Extract TypeId from each Any + // box (or use directly if `.s64`-typed), + // icmp eq. + const a = blk: { + const v = self.e.resolveRef(bi.args[0]); + const ty = self.e.getRefIRType(bi.args[0]) orelse TypeId.s64; + if (ty == .any) break :blk c.LLVMBuildExtractValue(self.e.builder, v, 1, "te.a"); + break :blk v; + }; + const b = blk: { + const v = self.e.resolveRef(bi.args[1]); + const ty = self.e.getRefIRType(bi.args[1]) orelse TypeId.s64; + if (ty == .any) break :blk c.LLVMBuildExtractValue(self.e.builder, v, 1, "te.b"); + break :blk v; + }; + const eq_res = c.LLVMBuildICmp(self.e.builder, c.LLVMIntEQ, a, b, "te.eq"); + self.e.mapRef(eq_res); + }, + .has_impl => { + // Runtime has_impl needs a protocol-map + // snapshot — not wired yet. Silent false for + // now; the lower-time fold via + // `tryConstBoolCondition` covers every + // statically-resolvable call. + self.e.mapRef(c.LLVMConstInt(self.e.cached_i1, 0, 0)); + }, + else => { + // size_of, cast — handled by lowering or codegen glue + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + }, + } + } + + pub fn emitCompilerCall(self: Ops, instruction: *const Inst) void { + // Compiler hooks are comptime-only; if one reaches emission, produce undef + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + } + + pub fn emitCallClosure(self: Ops, instruction: *const Inst, call_op: CallIndirect) void { + // Closure: { fn_ptr, env }. + // + // ABI (when module.has_implicit_ctx): + // trampoline signature: (__sx_ctx, env, args...) + // call_op.args[0] = __sx_ctx (prepended by lowering) + // call_op.args[1..] = user args + // extracted env_ptr = inserted at LLVM slot 1 + // + // ABI (without implicit_ctx): + // trampoline signature: (env, args...) + // call_op.args = user args (no ctx prepend) + // extracted env_ptr = inserted at LLVM slot 0 + const closure = self.e.resolveRef(call_op.callee); + const cl_kind = c.LLVMGetTypeKind(c.LLVMTypeOf(closure)); + if (cl_kind != c.LLVMStructTypeKind) { + self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty))); + return; + } + const fn_ptr = c.LLVMBuildExtractValue(self.e.builder, closure, 0, "cl.fn"); + const env_ptr = c.LLVMBuildExtractValue(self.e.builder, closure, 1, "cl.env"); + + // Get the closure's declared parameter types from the IR type system + const callee_ir_ty = self.e.getRefIRType(call_op.callee); + const closure_params: ?[]const TypeId = if (callee_ir_ty) |cty| blk: { + if (!cty.isBuiltin()) { + const ci = self.e.ir_mod.types.get(cty); + if (ci == .closure) break :blk ci.closure.params; + } + break :blk null; + } else null; + + const has_ctx = self.e.ir_mod.has_implicit_ctx; + const user_args_offset_in_op: usize = if (has_ctx) 1 else 0; + const user_args_count: usize = call_op.args.len -| user_args_offset_in_op; + const ctx_slots: usize = if (has_ctx) 1 else 0; + const total_args = ctx_slots + 1 + user_args_count; // [ctx?] + env + user_args + + const args = self.e.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable; + defer self.e.alloc.free(args); + if (has_ctx) { + args[0] = self.e.resolveRef(call_op.args[0]); // ctx + } + args[ctx_slots] = env_ptr; + for (0..user_args_count) |j| { + args[ctx_slots + 1 + j] = self.e.resolveRef(call_op.args[user_args_offset_in_op + j]); + } + + // Build function type using declared param types (not arg types). + // closure_params is user-visible (no ctx, no env), so they line + // up with args[ctx_slots+1..]. + const ret_ty = self.e.toLLVMType(instruction.ty); + const param_tys = self.e.alloc.alloc(c.LLVMTypeRef, total_args) catch unreachable; + defer self.e.alloc.free(param_tys); + if (has_ctx) param_tys[0] = self.e.cached_ptr; // __sx_ctx + param_tys[ctx_slots] = self.e.cached_ptr; // env + if (closure_params) |cp| { + for (0..user_args_count) |j| { + const param_ir_ty = if (j < cp.len) cp[j] else null; + if (param_ir_ty) |pty| { + const llvm_pty = self.e.toLLVMType(pty); + param_tys[ctx_slots + 1 + j] = llvm_pty; + args[ctx_slots + 1 + j] = self.e.coerceArg(args[ctx_slots + 1 + j], llvm_pty); + } else { + param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]); + } + } + } else { + for (0..user_args_count) |j| { + param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]); + } + } + 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.e.builder, fn_ty, fn_ptr, args.ptr, @intCast(total_args), if (is_void) "" else "ccall"); + if (!is_void) { + self.e.mapRef(result); + } else { + self.e.advanceRefCounter(); + } + } }; diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 037d396..4712652 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -50,46 +50,46 @@ fn isIdentByte(b: u8) bool { /// spec across versions; locked to the documented order in /// ``. Slot numbers here MUST match the order of fields in /// the C `JNINativeInterface_` struct. -const Jni = struct { - const FindClass: u32 = 6; - const NewGlobalRef: u32 = 21; - const NewObject: u32 = 28; - const GetObjectClass: u32 = 31; - const GetMethodID: u32 = 33; +pub const Jni = struct { + pub const FindClass: u32 = 6; + pub const NewGlobalRef: u32 = 21; + pub const NewObject: u32 = 28; + pub const GetObjectClass: u32 = 31; + pub const GetMethodID: u32 = 33; // CallMethod (instance, varargs variant). Each numeric type // has its own slot — distinct ABI per return type, so the JNI // runtime dispatches the right arg-shuffle for each. - const CallObjectMethod: u32 = 34; - const CallBooleanMethod: u32 = 37; - const CallIntMethod: u32 = 49; - const CallLongMethod: u32 = 52; - const CallFloatMethod: u32 = 55; - const CallDoubleMethod: u32 = 58; - const CallVoidMethod: u32 = 61; + pub const CallObjectMethod: u32 = 34; + pub const CallBooleanMethod: u32 = 37; + pub const CallIntMethod: u32 = 49; + pub const CallLongMethod: u32 = 52; + pub const CallFloatMethod: u32 = 55; + pub const CallDoubleMethod: u32 = 58; + pub const CallVoidMethod: u32 = 61; // CallNonvirtualMethod (instance, super-dispatch variant). Used by // `super.method(args)` from inside a `#jni_main` Activity method body: // dispatch is bound to a specific class rather than going through the // vtable, so subclass overrides don't intercept the call. Signature: // `(JNIEnv*, jobject obj, jclass clazz, jmethodID, args...)`. - const CallNonvirtualObjectMethod: u32 = 64; - const CallNonvirtualBooleanMethod: u32 = 67; - const CallNonvirtualIntMethod: u32 = 79; - const CallNonvirtualLongMethod: u32 = 82; - const CallNonvirtualFloatMethod: u32 = 85; - const CallNonvirtualDoubleMethod: u32 = 88; - const CallNonvirtualVoidMethod: u32 = 91; + pub const CallNonvirtualObjectMethod: u32 = 64; + pub const CallNonvirtualBooleanMethod: u32 = 67; + pub const CallNonvirtualIntMethod: u32 = 79; + pub const CallNonvirtualLongMethod: u32 = 82; + pub const CallNonvirtualFloatMethod: u32 = 85; + pub const CallNonvirtualDoubleMethod: u32 = 88; + pub const CallNonvirtualVoidMethod: u32 = 91; // Static-dispatch siblings — `target` IS already a `jclass`, so // no `GetObjectClass` step. `GetStaticMethodID` returns a // method-ID that's bound to a class+method+sig like the instance // variant; `CallStaticMethod` dispatches without a `this`. - const GetStaticMethodID: u32 = 113; - const CallStaticObjectMethod: u32 = 114; - const CallStaticBooleanMethod: u32 = 117; - const CallStaticIntMethod: u32 = 129; - const CallStaticLongMethod: u32 = 132; - const CallStaticFloatMethod: u32 = 135; - const CallStaticDoubleMethod: u32 = 138; - const CallStaticVoidMethod: u32 = 141; + pub const GetStaticMethodID: u32 = 113; + pub const CallStaticObjectMethod: u32 = 114; + pub const CallStaticBooleanMethod: u32 = 117; + pub const CallStaticIntMethod: u32 = 129; + pub const CallStaticLongMethod: u32 = 132; + pub const CallStaticFloatMethod: u32 = 135; + pub const CallStaticDoubleMethod: u32 = 138; + pub const CallStaticVoidMethod: u32 = 141; }; // ── LLVMEmitter ───────────────────────────────────────────────────────── @@ -641,7 +641,7 @@ pub const LLVMEmitter = struct { /// (the ptr); otherwise return it unchanged. Used by JNI dispatch /// to feed string-literal method names + signatures to /// `GetMethodID`, which expects raw C strings. - fn extractSlicePtr(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef { + pub fn extractSlicePtr(self: *LLVMEmitter, val: c.LLVMValueRef) c.LLVMValueRef { const val_ty = c.LLVMTypeOf(val); if (c.LLVMGetTypeKind(val_ty) != c.LLVMStructTypeKind) return val; if (c.LLVMCountStructElementTypes(val_ty) != 2) return val; @@ -653,7 +653,7 @@ pub const LLVMEmitter = struct { /// Load a JNI vtable function pointer at the given offset. `ifs` /// is the `JNINativeInterface*` loaded from `JNIEnv*`. Treats the /// vtable as an array of opaque `ptr`s and indexes into it. - fn loadJniFn(self: *LLVMEmitter, ifs: c.LLVMValueRef, offset: u32, name: [*:0]const u8) c.LLVMValueRef { + pub fn loadJniFn(self: *LLVMEmitter, ifs: c.LLVMValueRef, offset: u32, name: [*:0]const u8) c.LLVMValueRef { const offset_val = c.LLVMConstInt(self.cached_i32, offset, 0); var idx = [_]c.LLVMValueRef{offset_val}; const slot = c.LLVMBuildInBoundsGEP2(self.builder, self.cached_ptr, ifs, &idx, 1, ""); @@ -664,7 +664,7 @@ pub const LLVMEmitter = struct { /// Cached on the emitter; all `objc_msg_send` instructions hand /// LLVMBuildCall2 their own per-call-site function type — the /// underlying function value is just an opaque `ptr` symbol. - fn getObjcMsgSendValue(self: *LLVMEmitter) c.LLVMValueRef { + pub fn getObjcMsgSendValue(self: *LLVMEmitter) c.LLVMValueRef { if (self.objc_msg_send_value) |v| return v; const name_z = "objc_msgSend"; if (c.LLVMGetNamedFunction(self.llvm_module, name_z)) |existing| { @@ -1411,492 +1411,10 @@ pub const LLVMEmitter = struct { .deref => |un| self.ops().emitDeref(instruction, un), // ── Calls ───────────────────────────────────────────── - .objc_msg_send => |msg| { - const msg_send = self.getObjcMsgSendValue(); - // Detect the sret case: >16 B non-HFA struct return. - // Same predicate as the plain-foreign-call path so the - // two arms stay in lockstep. - const raw_ret_ty = self.toLLVMType(instruction.ty); - const uses_sret = self.needsByval(instruction.ty, raw_ret_ty); - const ret_ty = if (uses_sret) self.cached_void else raw_ret_ty; - - // Slot layout: - // uses_sret = false → [recv, sel, args...] - // uses_sret = true → [sret_slot, recv, sel, args...] - const sret_off: usize = if (uses_sret) 1 else 0; - const total_params: usize = 2 + msg.args.len + sret_off; - const param_types = self.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable; - defer self.alloc.free(param_types); - const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable; - defer self.alloc.free(call_args); - - var sret_slot: c.LLVMValueRef = null; - if (uses_sret) { - sret_slot = c.LLVMBuildAlloca(self.builder, raw_ret_ty, "objc.sret"); - param_types[0] = self.cached_ptr; - call_args[0] = sret_slot; - } - - // recv (typed *void from the IR) - param_types[sret_off] = self.cached_ptr; - call_args[sret_off] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr); - // sel (loaded SEL — opaque ptr) - param_types[sret_off + 1] = self.cached_ptr; - call_args[sret_off + 1] = self.coerceArg(self.resolveRef(msg.sel), self.cached_ptr); - // additional args take their IR types, with ABI - // coercion applied so structs / strings decay the - // same way they do for any C foreign call. - for (msg.args, 0..) |arg_ref, i| { - const raw_ty = self.getRefIRType(arg_ref) orelse .void; - const raw_llvm = self.toLLVMType(raw_ty); - const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm); - param_types[i + 2 + sret_off] = coerced_ty; - call_args[i + 2 + sret_off] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty); - } - - const fn_ty = c.LLVMFunctionType(ret_ty, param_types.ptr, @intCast(total_params), 0); - const call_label: [*:0]const u8 = if (instruction.ty == .void or uses_sret) "" else "objc.msg"; - var result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label); - if (uses_sret) { - // Tag the call's arg 0 (sret slot) with the sret - // attribute so the AArch64 / SysV backends route - // through the x8 / hidden-pointer convention. - const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4); - const sret_attr = c.LLVMCreateTypeAttribute(self.context, sret_kind, raw_ret_ty); - const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1)); - c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr); - result = c.LLVMBuildLoad2(self.builder, raw_ret_ty, sret_slot, "objc.sret.load"); - } - // Always mapRef — the IR Ref counter for this - // instruction advances regardless of return type, - // so skipping it would misalign every subsequent - // ref lookup in this function. - self.mapRef(result); - }, - .jni_msg_send => |msg| { - // JNI vtable indirection: - // ifs = *env // JNINativeInterface* - // instance: cls = ifs[GetObjectClass](env, target) - // mid = ifs[GetMethodID](env, cls, name, sig) - // ifs[CallMethod](env, target, mid, args...) - // static: target IS the jclass — skip GetObjectClass - // mid = ifs[GetStaticMethodID](env, target, name, sig) - // ifs[CallStaticMethod](env, target, mid, args...) - // ctor: cls = ifs[FindClass](env, parent_class_path) - // mid = ifs[GetMethodID](env, cls, "", sig) - // ifs[NewObject](env, cls, mid, args...) → jobject - // nonvirt: handled below via FindClass + GetMethodID + - // CallNonvirtualMethod. - // The cached path (msg.cache_key != null) still shares one - // (jclass GlobalRef, jmethodID) pair per literal (name, sig). - if (msg.is_constructor) { - self.emitJniConstructor(msg, instruction.ty); - return; - } - const ret_ty_id = instruction.ty; - const is_pointer_ret = switch (self.ir_mod.types.get(ret_ty_id)) { - .pointer, .many_pointer => true, - else => false, - }; - const call_method_offset: u32 = if (msg.is_static) blk: { - if (is_pointer_ret) break :blk Jni.CallStaticObjectMethod; - break :blk switch (ret_ty_id) { - .void => Jni.CallStaticVoidMethod, - .s32 => Jni.CallStaticIntMethod, - .s64 => Jni.CallStaticLongMethod, - .f32 => Jni.CallStaticFloatMethod, - .f64 => Jni.CallStaticDoubleMethod, - .bool => Jni.CallStaticBooleanMethod, - else => { - self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); - return; - }, - }; - } else if (msg.is_nonvirtual) blk: { - if (is_pointer_ret) break :blk Jni.CallNonvirtualObjectMethod; - break :blk switch (ret_ty_id) { - .void => Jni.CallNonvirtualVoidMethod, - .s32 => Jni.CallNonvirtualIntMethod, - .s64 => Jni.CallNonvirtualLongMethod, - .f32 => Jni.CallNonvirtualFloatMethod, - .f64 => Jni.CallNonvirtualDoubleMethod, - .bool => Jni.CallNonvirtualBooleanMethod, - else => { - self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); - return; - }, - }; - } else blk: { - if (is_pointer_ret) break :blk Jni.CallObjectMethod; - break :blk switch (ret_ty_id) { - .void => Jni.CallVoidMethod, - .s32 => Jni.CallIntMethod, - .s64 => Jni.CallLongMethod, - .f32 => Jni.CallFloatMethod, - .f64 => Jni.CallDoubleMethod, - .bool => Jni.CallBooleanMethod, - else => { - self.mapRef(c.LLVMGetUndef(self.toLLVMType(instruction.ty))); - return; - }, - }; - }; - const get_mid_offset: u32 = if (msg.is_static) Jni.GetStaticMethodID else Jni.GetMethodID; - - const env = self.resolveRef(msg.env); - const target = self.resolveRef(msg.target); - // String literals lower as `{ptr, i64}` slices in sx IR; - // JNI's `GetMethodID` expects raw C strings, so extract - // field 0 when the source is a slice. - const name_ptr = self.extractSlicePtr(self.resolveRef(msg.name)); - const sig_ptr = self.extractSlicePtr(self.resolveRef(msg.sig)); - - const ifs = c.LLVMBuildLoad2(self.builder, self.cached_ptr, env, "jni.ifs"); - - // Method-ID resolution. When `name` and `sig` are both - // string literals the call site participates in - // `(name, sig)` slot interning (step 1.17): a shared - // pair of static globals holds the `jclass` GlobalRef - // and the `jmethodID`, populated lazily on the first - // call to any matching site. Non-literal sites fall - // back to the per-call `GetObjectClass + GetMethodID` - // sequence (1.15 shape). - const mid = if (msg.cache_key) |ck| blk: { - const pair = self.ffiCtors().getOrCreateJniSlots(ck.name_str, ck.sig_str); - const cached_mid = c.LLVMBuildLoad2(self.builder, self.cached_ptr, pair.mid_slot, "jni.cached.mid"); - const is_cached = c.LLVMBuildICmp(self.builder, c.LLVMIntNE, cached_mid, c.LLVMConstNull(self.cached_ptr), "jni.is.cached"); - - const cur_fn = c.LLVMGetBasicBlockParent(c.LLVMGetInsertBlock(self.builder)); - const miss_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "jni.miss"); - const cont_bb = c.LLVMAppendBasicBlockInContext(self.context, cur_fn, "jni.cont"); - const before_bb = c.LLVMGetInsertBlock(self.builder); - _ = c.LLVMBuildCondBr(self.builder, is_cached, cont_bb, miss_bb); - - // Miss path: - // instance: GetObjectClass → NewGlobalRef → GetMethodID - // static: target IS class → NewGlobalRef(target) → GetStaticMethodID - c.LLVMPositionBuilderAtEnd(self.builder, miss_bb); - const local_cls = if (msg.is_static) target else inst_cls: { - const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass"); - var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr }; - const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0); - var gocls_args = [_]c.LLVMValueRef{ env, target }; - break :inst_cls c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls"); - }; - const new_global_ref = self.loadJniFn(ifs, Jni.NewGlobalRef, "jni.NewGlobalRef"); - var ngref_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr }; - const ngref_ty = c.LLVMFunctionType(self.cached_ptr, &ngref_params, 2, 0); - var ngref_args = [_]c.LLVMValueRef{ env, local_cls }; - const global_cls = c.LLVMBuildCall2(self.builder, ngref_ty, new_global_ref, &ngref_args, 2, "jni.global.cls"); - _ = c.LLVMBuildStore(self.builder, global_cls, pair.cls_slot); - const get_mid = self.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "jni.GetMethodID"); - var gmid_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_ptr, self.cached_ptr }; - const gmid_ty = c.LLVMFunctionType(self.cached_ptr, &gmid_params, 4, 0); - var gmid_args = [_]c.LLVMValueRef{ env, global_cls, name_ptr, sig_ptr }; - const fresh_mid = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.fresh.mid"); - _ = c.LLVMBuildStore(self.builder, fresh_mid, pair.mid_slot); - const miss_end_bb = c.LLVMGetInsertBlock(self.builder); - _ = c.LLVMBuildBr(self.builder, cont_bb); - - // Cont: phi the cached vs fresh mid. - c.LLVMPositionBuilderAtEnd(self.builder, cont_bb); - const phi = c.LLVMBuildPhi(self.builder, self.cached_ptr, "jni.mid"); - var phi_vals = [_]c.LLVMValueRef{ cached_mid, fresh_mid }; - var phi_blocks = [_]c.LLVMBasicBlockRef{ before_bb, miss_end_bb }; - c.LLVMAddIncoming(phi, &phi_vals, &phi_blocks, 2); - break :blk phi; - } else blk: { - const cls = if (msg.is_static) target else if (msg.is_nonvirtual) nonvirt_cls: { - // `super.method(args)`: dispatch is bound to a - // specific class (the parent), not subclass-override. - // Resolve via FindClass(parent_path). No caching yet — - // per-call lookup. The parent path is a NUL-terminated - // C string emitted as a private LLVM global. - const path = msg.parent_class_path orelse ""; - const path_global = self.emitCStringGlobal(path, "jni.parent.path"); - const find_class = self.loadJniFn(ifs, Jni.FindClass, "jni.FindClass"); - var fc_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr }; - const fc_ty = c.LLVMFunctionType(self.cached_ptr, &fc_params, 2, 0); - var fc_args = [_]c.LLVMValueRef{ env, path_global }; - break :nonvirt_cls c.LLVMBuildCall2(self.builder, fc_ty, find_class, &fc_args, 2, "jni.parent.cls"); - } else inst_cls: { - const get_obj_cls = self.loadJniFn(ifs, Jni.GetObjectClass, "jni.GetObjectClass"); - var gocls_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr }; - const gocls_ty = c.LLVMFunctionType(self.cached_ptr, &gocls_params, 2, 0); - var gocls_args = [_]c.LLVMValueRef{ env, target }; - break :inst_cls c.LLVMBuildCall2(self.builder, gocls_ty, get_obj_cls, &gocls_args, 2, "jni.cls"); - }; - const get_mid = self.loadJniFn(ifs, get_mid_offset, if (msg.is_static) "jni.GetStaticMethodID" else "jni.GetMethodID"); - var gmid_params = [_]c.LLVMTypeRef{ self.cached_ptr, self.cached_ptr, self.cached_ptr, self.cached_ptr }; - const gmid_ty = c.LLVMFunctionType(self.cached_ptr, &gmid_params, 4, 0); - var gmid_args = [_]c.LLVMValueRef{ env, cls, name_ptr, sig_ptr }; - const mid_val = c.LLVMBuildCall2(self.builder, gmid_ty, get_mid, &gmid_args, 4, "jni.mid"); - if (msg.is_nonvirtual) { - // Stash cls in a dummy slot so the call site below - // can pick it up. Easiest path: do the call right - // here and return Ref.none, but we need to keep the - // outer phi shape. Instead, return both via tuple - // through an auxiliary local — simplest is to attach - // `cls` to a per-invocation slot. Use a stack alloca. - const cls_slot = c.LLVMBuildAlloca(self.builder, self.cached_ptr, "jni.parent.cls.slot"); - _ = c.LLVMBuildStore(self.builder, cls, cls_slot); - // Tag the slot pointer onto the phi result via the - // generated metadata: we'll re-extract by re-running - // FindClass — actually simpler: lower nonvirtual on - // the spot below. Drop the implicit `break` here: - const call_fn = self.loadJniFn(ifs, call_method_offset, "jni.callfn.nonvirtual"); - const raw_ret = self.toLLVMType(ret_ty_id); - const total_call_params_nv: usize = 4 + msg.args.len; - const call_param_types_nv = self.alloc.alloc(c.LLVMTypeRef, total_call_params_nv) catch unreachable; - defer self.alloc.free(call_param_types_nv); - const call_args_nv = self.alloc.alloc(c.LLVMValueRef, total_call_params_nv) catch unreachable; - defer self.alloc.free(call_args_nv); - call_param_types_nv[0] = self.cached_ptr; - call_param_types_nv[1] = self.cached_ptr; - call_param_types_nv[2] = self.cached_ptr; - call_param_types_nv[3] = self.cached_ptr; - call_args_nv[0] = env; - call_args_nv[1] = target; - call_args_nv[2] = cls; - call_args_nv[3] = mid_val; - for (msg.args, 0..) |arg_ref, i| { - const raw_ty = self.getRefIRType(arg_ref) orelse .void; - const raw_llvm = self.toLLVMType(raw_ty); - const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm); - call_param_types_nv[i + 4] = coerced_ty; - call_args_nv[i + 4] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty); - } - const call_fn_ty_nv = c.LLVMFunctionType(raw_ret, call_param_types_nv.ptr, @intCast(total_call_params_nv), 0); - const label_nv: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.nonvirtual.ret"; - const result_nv = c.LLVMBuildCall2(self.builder, call_fn_ty_nv, call_fn, call_args_nv.ptr, @intCast(total_call_params_nv), label_nv); - self.mapRef(result_nv); - return; - } - break :blk mid_val; - }; - - // CallMethod: (JNIEnv*, jobject, jmethodID, args...) -> RetTy - const call_fn = self.loadJniFn(ifs, call_method_offset, "jni.callfn"); - const raw_ret = self.toLLVMType(ret_ty_id); - const total_call_params: usize = 3 + msg.args.len; - const call_param_types = self.alloc.alloc(c.LLVMTypeRef, total_call_params) catch unreachable; - defer self.alloc.free(call_param_types); - const call_args = self.alloc.alloc(c.LLVMValueRef, total_call_params) catch unreachable; - defer self.alloc.free(call_args); - call_param_types[0] = self.cached_ptr; - call_param_types[1] = self.cached_ptr; - call_param_types[2] = self.cached_ptr; - call_args[0] = env; - call_args[1] = target; - call_args[2] = mid; - for (msg.args, 0..) |arg_ref, i| { - const raw_ty = self.getRefIRType(arg_ref) orelse .void; - const raw_llvm = self.toLLVMType(raw_ty); - const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm); - call_param_types[i + 3] = coerced_ty; - call_args[i + 3] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty); - } - const call_fn_ty = c.LLVMFunctionType(raw_ret, call_param_types.ptr, @intCast(total_call_params), 0); - const label: [*:0]const u8 = if (ret_ty_id == .void) "" else "jni.ret"; - const result = c.LLVMBuildCall2(self.builder, call_fn_ty, call_fn, call_args.ptr, @intCast(total_call_params), label); - self.mapRef(result); - }, - .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; - if (self.import_sources) |sm| interp_inst.setSourceMap(sm); - 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 callee_needs_c_abi = callee_func.is_extern or callee_func.call_conv == .c; - const callee_raw_ret = self.toLLVMType(callee_func.ret); - const callee_uses_sret = callee_needs_c_abi and self.needsByval(callee_func.ret, callee_raw_ret); - - // When the callee uses sret, prepend an alloca for the result. - // Index alignment: actual_args[0] = sret_slot; actual_args[i+1] = sx arg i. - const sret_off: usize = if (callee_uses_sret) 1 else 0; - const total_args = call_op.args.len + sret_off; - const args = self.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable; - defer self.alloc.free(args); - var sret_slot: c.LLVMValueRef = null; - if (callee_uses_sret) { - sret_slot = c.LLVMBuildAlloca(self.builder, callee_raw_ret, "sret.slot"); - args[0] = sret_slot; - } - for (call_op.args, 0..) |arg_ref, j| { - args[j + sret_off] = self.resolveRef(arg_ref); - } - const arg_count: c_uint = @intCast(total_args); - - // Get the function type from LLVM and coerce arguments - const fn_ty = c.LLVMGlobalGetValueType(callee); - const param_count = c.LLVMCountParamTypes(fn_ty); - 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| { - // The sret slot is already a properly-typed pointer; skip coercion. - if (callee_uses_sret and j == 0) continue; - const fn_param_idx = j - sret_off; - // Materialize byval args before coercion so we pass a ptr instead of the struct value. - if (callee_needs_c_abi and fn_param_idx < callee_func.params.len) { - const ir_ty = callee_func.params[fn_param_idx].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]); - } - } - // A `void`/`noreturn` call has no value, so it must stay - // unnamed (LLVM rejects a named void result). - const call_is_void_like = instruction.ty == .void or instruction.ty == .noreturn; - const call_label: [*:0]const u8 = if (call_is_void_like or callee_uses_sret) "" else "call"; - var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, call_label); - if (callee_uses_sret) { - // Mirror the function-decl `sret()` attribute on the call site so the - // LLVM backend lowers arg 0 via x8 (AAPCS64) / hidden ptr (SysV AMD64). - const sret_kind = c.LLVMGetEnumAttributeKindForName("sret", 4); - const sret_attr = c.LLVMCreateTypeAttribute(self.context, sret_kind, callee_raw_ret); - const param1_idx: c.LLVMAttributeIndex = @bitCast(@as(i32, 1)); - c.LLVMAddCallSiteAttribute(result, param1_idx, sret_attr); - // Load the actual struct value the callee wrote into the slot. - result = c.LLVMBuildLoad2(self.builder, callee_raw_ret, sret_slot, "sret.load"); - } else if (!call_is_void_like and callee_func.is_extern) { - // Coerce ABI return value (e.g. i64 / [2 x i64]) back to IR struct type if needed - 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; - - // Default-conv fn-pointers under implicit-ctx carry a hidden - // `*void` (the implicit __sx_ctx) at LLVM slot 0. The IR fn - // type does not include it, so shift fn_params lookups by 1. - const fp_ctx_slots: usize = if (callee_ir_ty) |cty| blk: { - if (!self.ir_mod.has_implicit_ctx) break :blk 0; - if (cty.isBuiltin()) break :blk 0; - const ci = self.ir_mod.types.get(cty); - switch (ci) { - .function => |f| break :blk if (f.call_conv == .c) @as(usize, 0) else 1, - else => break :blk 0, - } - } else 0; - - 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| { - // Slots 0..fp_ctx_slots are the implicit __sx_ctx - // (passed as opaque ptr; not in fp). - if (j < fp_ctx_slots) { - param_tys[j] = self.cached_ptr; - args[j] = self.coerceArg(args[j], self.cached_ptr); - continue; - } - const fp_idx = j - fp_ctx_slots; - if (fp_idx < fp.len) { - const raw_struct = self.toLLVMType(fp[fp_idx]); - if (fp_is_c_abi and self.needsByval(fp[fp_idx], 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); - const icall_void_like = instruction.ty == .void or instruction.ty == .noreturn; - var result = c.LLVMBuildCall2(self.builder, fn_ty, callee, args.ptr, arg_count, if (icall_void_like) "" else "icall"); - - // Coerce call result to instruction's expected type - const expected_ty = self.toLLVMType(instruction.ty); - if (!icall_void_like and c.LLVMTypeOf(result) != expected_ty) { - result = self.coerceArg(result, expected_ty); - } - self.mapRef(result); - }, + .objc_msg_send => |msg| self.ops().emitObjcMsgSend(instruction, msg), + .jni_msg_send => |msg| self.ops().emitJniMsgSend(instruction, msg), + .call => |call_op| self.ops().emitCall(instruction, call_op), + .call_indirect => |call_op| self.ops().emitCallIndirect(instruction, call_op), // ── Terminators ──────────────────────────────────────── .ret => |un| { @@ -2400,196 +1918,9 @@ pub const LLVMEmitter = struct { }, // ── Call extensions ─────────────────────────────────────── - .call_builtin => |bi| { - // Builtins that map to libc functions or LLVM intrinsics - switch (bi.builtin) { - .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(); - }, - .type_name => { - // Dynamic `type_name(t)` at runtime: extract - // the TypeId from the arg (an Any-boxed Type - // value: tag=`.s64.index()`, value=tid), GEP - // into the compiler-emitted `__sx_type_names` - // global, load the string. The arg's LLVM - // shape is the `{i64, i64}` Any aggregate - // (because the IR-side arg type is `.any` - // when boxed); for unboxed direct call sites - // (the arg IR type is `.s64` from - // `const_type`), the value IS the TypeId - // index directly. - const arg_ref = bi.args[0]; - const arg_val = self.resolveRef(arg_ref); - const arg_ir_ty = self.getRefIRType(arg_ref) orelse @import("types.zig").TypeId.s64; - const tid_idx = blk: { - if (arg_ir_ty == .any) { - // Boxed: extract value field. - break :blk c.LLVMBuildExtractValue(self.builder, arg_val, 1, "tn.tid"); - } - // Bare i64 (TypeId index). - break :blk arg_val; - }; - const arr_global = self.reflection().getOrBuildTypeNameArray(); - const arr_len = self.type_name_array_len; - const string_ty = self.getStringStructType(); - const arr_ty = c.LLVMArrayType(string_ty, arr_len); - const zero = c.LLVMConstInt(self.cached_i64, 0, 0); - var indices = [2]c.LLVMValueRef{ zero, tid_idx }; - const gep = c.LLVMBuildInBoundsGEP2(self.builder, arr_ty, arr_global, &indices, 2, "tn.gep"); - const result = c.LLVMBuildLoad2(self.builder, string_ty, gep, "tn.load"); - self.mapRef(result); - }, - .type_eq => { - // Dynamic `type_eq(a, b)` — both args are - // Type values. Extract TypeId from each Any - // box (or use directly if `.s64`-typed), - // icmp eq. - const a = blk: { - const v = self.resolveRef(bi.args[0]); - const ty = self.getRefIRType(bi.args[0]) orelse @import("types.zig").TypeId.s64; - if (ty == .any) break :blk c.LLVMBuildExtractValue(self.builder, v, 1, "te.a"); - break :blk v; - }; - const b = blk: { - const v = self.resolveRef(bi.args[1]); - const ty = self.getRefIRType(bi.args[1]) orelse @import("types.zig").TypeId.s64; - if (ty == .any) break :blk c.LLVMBuildExtractValue(self.builder, v, 1, "te.b"); - break :blk v; - }; - const eq_res = c.LLVMBuildICmp(self.builder, c.LLVMIntEQ, a, b, "te.eq"); - self.mapRef(eq_res); - }, - .has_impl => { - // Runtime has_impl needs a protocol-map - // snapshot — not wired yet. Silent false for - // now; the lower-time fold via - // `tryConstBoolCondition` covers every - // statically-resolvable call. - self.mapRef(c.LLVMConstInt(self.cached_i1, 0, 0)); - }, - 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 }. - // - // ABI (when module.has_implicit_ctx): - // trampoline signature: (__sx_ctx, env, args...) - // call_op.args[0] = __sx_ctx (prepended by lowering) - // call_op.args[1..] = user args - // extracted env_ptr = inserted at LLVM slot 1 - // - // ABI (without implicit_ctx): - // trampoline signature: (env, args...) - // call_op.args = user args (no ctx prepend) - // extracted env_ptr = inserted at LLVM slot 0 - 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; - - const has_ctx = self.ir_mod.has_implicit_ctx; - const user_args_offset_in_op: usize = if (has_ctx) 1 else 0; - const user_args_count: usize = call_op.args.len -| user_args_offset_in_op; - const ctx_slots: usize = if (has_ctx) 1 else 0; - const total_args = ctx_slots + 1 + user_args_count; // [ctx?] + env + user_args - - const args = self.alloc.alloc(c.LLVMValueRef, total_args) catch unreachable; - defer self.alloc.free(args); - if (has_ctx) { - args[0] = self.resolveRef(call_op.args[0]); // ctx - } - args[ctx_slots] = env_ptr; - for (0..user_args_count) |j| { - args[ctx_slots + 1 + j] = self.resolveRef(call_op.args[user_args_offset_in_op + j]); - } - - // Build function type using declared param types (not arg types). - // closure_params is user-visible (no ctx, no env), so they line - // up with args[ctx_slots+1..]. - 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); - if (has_ctx) param_tys[0] = self.cached_ptr; // __sx_ctx - param_tys[ctx_slots] = self.cached_ptr; // env - if (closure_params) |cp| { - for (0..user_args_count) |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[ctx_slots + 1 + j] = llvm_pty; - args[ctx_slots + 1 + j] = self.coerceArg(args[ctx_slots + 1 + j], llvm_pty); - } else { - param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]); - } - } - } else { - for (0..user_args_count) |j| { - param_tys[ctx_slots + 1 + j] = c.LLVMTypeOf(args[ctx_slots + 1 + j]); - } - } - 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(); - } - }, + .call_builtin => |bi| self.ops().emitCallBuiltin(instruction, bi), + .compiler_call => self.ops().emitCompilerCall(instruction), + .call_closure => |call_op| self.ops().emitCallClosure(instruction, call_op), // ── Tuple ops ──────────────────────────────────────────── .tuple_init => |agg| { @@ -3244,7 +2575,7 @@ pub const LLVMEmitter = struct { return c.LLVMFunctionType(self.cached_ptr, ¶m_types, 3, 0); } - fn getOrDeclareMathF64(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef { + pub fn getOrDeclareMathF64(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef { const name: [*:0]const u8 = switch (id) { .sqrt => "sqrt", .sin => "sin", @@ -3256,12 +2587,12 @@ pub const LLVMEmitter = struct { return c.LLVMAddFunction(self.llvm_module, name, self.getMathF64Type()); } - fn getMathF64Type(self: *LLVMEmitter) c.LLVMTypeRef { + pub 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 { + pub fn getOrDeclareMathF32(self: *LLVMEmitter, id: ir_inst.BuiltinId) c.LLVMValueRef { const name: [*:0]const u8 = switch (id) { .sqrt => "sqrtf", .sin => "sinf", @@ -3273,7 +2604,7 @@ pub const LLVMEmitter = struct { return c.LLVMAddFunction(self.llvm_module, name, self.getMathF32Type()); } - fn getMathF32Type(self: *LLVMEmitter) c.LLVMTypeRef { + pub fn getMathF32Type(self: *LLVMEmitter) c.LLVMTypeRef { var param_types = [_]c.LLVMTypeRef{self.cached_f32}; return c.LLVMFunctionType(self.cached_f32, ¶m_types, 1, 0); } @@ -3286,12 +2617,12 @@ pub const LLVMEmitter = struct { return c.LLVMAddFunction(self.llvm_module, "memcmp", fn_ty); } - fn getOrDeclareWrite(self: *LLVMEmitter) c.LLVMValueRef { + pub 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 { + pub 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 }; @@ -3628,7 +2959,7 @@ pub const LLVMEmitter = struct { return .{ .e = self }; } - fn ffiCtors(self: *LLVMEmitter) llvm_ffi_ctors.FfiCtors { + pub fn ffiCtors(self: *LLVMEmitter) llvm_ffi_ctors.FfiCtors { return .{ .e = self }; } @@ -3659,7 +2990,7 @@ pub const LLVMEmitter = struct { return self.abiLowering().needsByval(ir_ty, raw_llvm_ty); } - fn materializeByvalArg(self: *LLVMEmitter, val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef) c.LLVMValueRef { + pub fn materializeByvalArg(self: *LLVMEmitter, val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef) c.LLVMValueRef { return self.abiLowering().materializeByvalArg(val, struct_ty); } @@ -3777,7 +3108,7 @@ pub const LLVMEmitter = struct { /// Emit a NUL-terminated C string as a private LLVM global and return /// the pointer to its first byte. Used for FindClass(env, "") etc. /// where the runtime expects raw `const char *`, not the sx slice shape. - fn emitCStringGlobal(self: *LLVMEmitter, str: []const u8, name: [*:0]const u8) c.LLVMValueRef { + pub fn emitCStringGlobal(self: *LLVMEmitter, str: []const u8, name: [*:0]const u8) c.LLVMValueRef { const z = self.alloc.dupeZ(u8, str) catch unreachable; defer self.alloc.free(z); return c.LLVMBuildGlobalStringPtr(self.builder, z.ptr, name); @@ -3787,7 +3118,7 @@ pub const LLVMEmitter = struct { /// `FindClass(env, parent_class_path)` → `GetMethodID(env, clazz, /// "", sig)` → `NewObject(env, clazz, mid, args...)`. Returns /// the new jobject. Per-call lookups — no caching yet. - fn emitJniConstructor(self: *LLVMEmitter, msg: ir_inst.JniMsgSend, ret_ty_id: TypeId) void { + pub fn emitJniConstructor(self: *LLVMEmitter, msg: ir_inst.JniMsgSend, ret_ty_id: TypeId) void { const env = self.resolveRef(msg.env); const sig_ptr = self.extractSlicePtr(self.resolveRef(msg.sig)); const name_ptr = self.extractSlicePtr(self.resolveRef(msg.name));