diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 9f6b583..f177916 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -1439,7 +1439,7 @@ pub const LLVMEmitter = struct { const uses_sret = needs_c_abi and !is_main and self.needsByval(func.ret, raw_ret_ty); const ret_ty = if (is_main) self.cached_i32 else if (uses_sret) self.cached_void - else if (needs_c_abi) self.abiCoerceParamType(func.ret, raw_ret_ty) + else if (needs_c_abi) self.abiCoerceParamTypeEx(func.ret, raw_ret_ty, func.is_extern) else raw_ret_ty; // Build parameter types — apply C ABI coercion for foreign/callconv(.c) functions. @@ -1451,7 +1451,7 @@ pub const LLVMEmitter = struct { if (uses_sret) param_types[0] = self.cached_ptr; for (func.params, 0..) |param, j| { const llvm_ty = self.toLLVMType(param.ty); - param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamType(param.ty, llvm_ty) else llvm_ty; + param_types[j + sret_offset] = if (needs_c_abi) self.abiCoerceParamTypeEx(param.ty, llvm_ty, func.is_extern) else llvm_ty; } const is_var_arg: c_int = if (func.is_variadic) 1 else 0; @@ -4241,11 +4241,28 @@ pub const LLVMEmitter = struct { // - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it) fn abiCoerceParamType(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef { - // String/slice → raw pointer (universal across all targets for foreign calls) - if (ir_ty == .string) return self.cached_ptr; - if (!ir_ty.isBuiltin()) { - const info = self.ir_mod.types.get(ir_ty); - if (info == .slice) return self.cached_ptr; + return self.abiCoerceParamTypeEx(ir_ty, llvm_ty, true); + } + + /// Same as `abiCoerceParamType` but with an explicit + /// `is_foreign_c_api` knob. When true, sx `string` / `[]T` slices + /// collapse to `ptr` — the libc convention where the user writes + /// `string` to mean `char *` and the length is dropped. When + /// false (sx-internal `callconv(.c)` like block trampolines), the + /// full slice shape is preserved and goes through the general + /// struct-coerce path (16-byte slice → `[2 x i64]`, lands in two + /// registers on AArch64 — the true C ABI for a 16-byte + /// aggregate). Without the split, sx-to-sx calls through a + /// `(*Block, string) -> void callconv(.c)` fn-pointer mismatched + /// the caller's `{ptr, i64}` value against the trampoline's + /// collapsed `ptr` param. + fn abiCoerceParamTypeEx(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef, is_foreign_c_api: bool) c.LLVMTypeRef { + if (is_foreign_c_api) { + if (ir_ty == .string) return self.cached_ptr; + if (!ir_ty.isBuiltin()) { + const info = self.ir_mod.types.get(ir_ty); + if (info == .slice) return self.cached_ptr; + } } // WASM32: usize/isize are pointer-sized (i32 on wasm32).