refactor(backend): move call + call-extension handlers into ops.zig (A7.4 slice c)

Relocate the Calls (objc_msg_send / jni_msg_send / call / call_indirect)
and Call-extensions (call_builtin / compiler_call / call_closure) emitInst
handler groups out of emit_llvm.zig into the existing Ops facade. Each
emitInst arm now delegates via self.ops().emit<Op>(...). Behavior-preserving
pure relocation; emitted LLVM IR is byte-identical (361/0 examples, no
snapshot churn).

Shared call infra stays on LLVMEmitter, widened pub only as the moved
bodies require: extractSlicePtr, loadJniFn, getObjcMsgSendValue, the math
F32/F64 declarators + types, getOrDeclareWrite/getWriteType, ffiCtors,
materializeByvalArg, emitCStringGlobal, emitJniConstructor, and the Jni
slot-offset constants. emitJniConstructor remains in emit_llvm.zig (A7.3
decision); the moved jni arm calls it via self.e.emitJniConstructor(...).
This commit is contained in:
agra
2026-06-03 11:45:30 +03:00
parent e1d86e0144
commit 5388895b3e
2 changed files with 749 additions and 724 deletions

View File

@@ -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[Call<T>Method](env, target, mid, args...)
// static: target IS the jclass — skip GetObjectClass
// mid = ifs[GetStaticMethodID](env, target, name, sig)
// ifs[CallStatic<T>Method](env, target, mid, args...)
// ctor: cls = ifs[FindClass](env, parent_class_path)
// mid = ifs[GetMethodID](env, cls, "<init>", sig)
// ifs[NewObject](env, cls, mid, args...) → jobject
// nonvirt: handled below via FindClass + GetMethodID +
// CallNonvirtual<T>Method.
// 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;
};
// Call<Type>Method: (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(<T>)` 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();
}
}
};

View File

@@ -50,46 +50,46 @@ fn isIdentByte(b: u8) bool {
/// spec across versions; locked to the documented order in
/// `<jni.h>`. 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;
// Call<Type>Method (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;
// CallNonvirtual<T>Method (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; `CallStatic<Type>Method` 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[Call<T>Method](env, target, mid, args...)
// static: target IS the jclass — skip GetObjectClass
// mid = ifs[GetStaticMethodID](env, target, name, sig)
// ifs[CallStatic<T>Method](env, target, mid, args...)
// ctor: cls = ifs[FindClass](env, parent_class_path)
// mid = ifs[GetMethodID](env, cls, "<init>", sig)
// ifs[NewObject](env, cls, mid, args...) → jobject
// nonvirt: handled below via FindClass + GetMethodID +
// CallNonvirtual<T>Method.
// 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;
};
// Call<Type>Method: (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(<T>)` 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, &param_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, &param_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, &param_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, "<path>") 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,
/// "<init>", 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));