From 71f1cb2fb0992bb9845d24a17d3a9b5d8a452555 Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 3 Jun 2026 09:10:27 +0300 Subject: [PATCH] refactor(backend): extract LLVM type/ABI lowering into src/backend/llvm/ (A7.1 step 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the LLVM type-mapping and C-ABI coercion helpers out of emit_llvm.zig into the first src/backend/llvm/ modules. Behavior-preserving relocation — the only rewrites are module plumbing and self.* -> self.e.* facade access. - src/backend/llvm/types.zig (TypeLowering): toLLVMType + toLLVMTypeInfo. - src/backend/llvm/abi.zig (AbiLowering): abiCoerceParamType / abiCoerceParamTypeEx / needsByval / materializeByvalArg. - Both are backend *LLVMEmitter facades (field `e`) — the backend analogue of the IR-side *Lowering facades, NOT a *Lowering facade. They reach the cached LLVM handles, IR type table, module data layout, builder, and the memoizing composite-type getters via self.e.*. - LLVMEmitter stays the facade: toLLVMType (~97 callers) + abiCoerceParamType / abiCoerceParamTypeEx / needsByval / materializeByvalArg kept as thin wrappers delegating through new typeLowering()/abiLowering() accessors. Zero caller churn. toLLVMTypeInfo deleted (sole caller moved). - Widened getStringStructType / getAnyStructType / getClosureStructType to pub (the moved toLLVMTypeInfo calls them back; their memoization stays on LLVMEmitter). verifySizes stays in emit_llvm.zig (size-assertion pass, not type/ ABI lowering). No ABI/type logic, branch order, diagnostic text, or snapshot changed. Circular import (emit_llvm <-> backend/llvm) resolves via the pointer facade. Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0 (1202 .ir + the 2 ABI unit tests unchanged, no churn). --- src/backend/llvm/abi.zig | 123 +++++++++++++++++ src/backend/llvm/types.zig | 178 ++++++++++++++++++++++++ src/ir/emit_llvm.zig | 272 +++---------------------------------- 3 files changed, 322 insertions(+), 251 deletions(-) create mode 100644 src/backend/llvm/abi.zig create mode 100644 src/backend/llvm/types.zig diff --git a/src/backend/llvm/abi.zig b/src/backend/llvm/abi.zig new file mode 100644 index 0000000..e02ca48 --- /dev/null +++ b/src/backend/llvm/abi.zig @@ -0,0 +1,123 @@ +const std = @import("std"); +const llvm = @import("../../llvm_api.zig"); +const c = llvm.c; +const ir_types = @import("../../ir/types.zig"); +const emit = @import("../../ir/emit_llvm.zig"); + +const TypeId = ir_types.TypeId; +const LLVMEmitter = emit.LLVMEmitter; + +/// C-ABI parameter coercion (architecture phase A7.1), extracted from +/// `LLVMEmitter`. A backend `*LLVMEmitter` facade: it borrows the emitter for +/// the cached LLVM handles, the IR type table, the module data layout, and the +/// IR builder. `LLVMEmitter.{abiCoerceParamType, abiCoerceParamTypeEx, +/// needsByval, materializeByvalArg}` are thin wrappers delegating here. +/// +/// On ARM64 (and x86_64), the C calling convention coerces small struct +/// arguments to integers for register passing: +/// - String/slice {ptr, i64} → ptr (extract raw pointer) +/// - Small integer struct (≤ 8 bytes, non-HFA) → i64 +/// - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it) +pub const AbiLowering = struct { + e: *LLVMEmitter, + + pub fn abiCoerceParamType(self: AbiLowering, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef { + 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. + pub fn abiCoerceParamTypeEx(self: AbiLowering, 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.e.cached_ptr; + if (!ir_ty.isBuiltin()) { + const info = self.e.ir_mod.types.get(ir_ty); + if (info == .slice) return self.e.cached_ptr; + } + } + + // WASM32: usize/isize are pointer-sized (i32 on wasm32). + // Other integer types (s64, u64) keep their declared size — they represent + // genuinely 64-bit values (SDL_WindowFlags, timestamps, etc.). + if (self.e.target_config.isWasm32()) { + if (ir_ty == .usize or ir_ty == .isize) return self.e.cached_i32; + return llvm_ty; + } + + // Only coerce struct types + if (c.LLVMGetTypeKind(llvm_ty) != c.LLVMStructTypeKind) return llvm_ty; + + // Check if it's an HFA (all float or all double fields) — leave as-is + const n_fields = c.LLVMCountStructElementTypes(llvm_ty); + if (n_fields >= 1 and n_fields <= 4) { + var all_float = true; + var all_double = true; + var fi: c_uint = 0; + while (fi < n_fields) : (fi += 1) { + const ft = c.LLVMStructGetTypeAtIndex(llvm_ty, fi); + const fk = c.LLVMGetTypeKind(ft); + if (fk != c.LLVMFloatTypeKind) all_float = false; + if (fk != c.LLVMDoubleTypeKind) all_double = false; + } + if (all_float or all_double) return llvm_ty; + } + + // Small struct (≤ 8 bytes) → coerce to i64 + const size = c.LLVMABISizeOfType( + c.LLVMGetModuleDataLayout(self.e.llvm_module), + llvm_ty, + ); + if (size <= 8) return self.e.cached_i64; + + // Medium struct (9-16 bytes) → coerce to [2 x i64] + if (size <= 16) { + return c.LLVMArrayType2(self.e.cached_i64, 2); + } + + // Large composite (> 16 bytes) → pass by reference: ptr + byval() at + // the call/sig sites. LLVM's AArch64/x86_64 backend lowers byval to + // the right ABI sequence (caller copy + indirect arg). + return self.e.cached_ptr; + } + + pub fn needsByval(self: AbiLowering, ir_ty: TypeId, raw_llvm_ty: c.LLVMTypeRef) bool { + if (self.e.target_config.isWasm32()) return false; + if (ir_ty == .string) return false; + if (!ir_ty.isBuiltin()) { + const info = self.e.ir_mod.types.get(ir_ty); + if (info == .slice) return false; + } + if (c.LLVMGetTypeKind(raw_llvm_ty) != c.LLVMStructTypeKind) return false; + const n = c.LLVMCountStructElementTypes(raw_llvm_ty); + if (n >= 1 and n <= 4) { + var all_f = true; + var all_d = true; + var i: c_uint = 0; + while (i < n) : (i += 1) { + const ft = c.LLVMStructGetTypeAtIndex(raw_llvm_ty, i); + const fk = c.LLVMGetTypeKind(ft); + if (fk != c.LLVMFloatTypeKind) all_f = false; + if (fk != c.LLVMDoubleTypeKind) all_d = false; + } + if (all_f or all_d) return false; + } + const size = c.LLVMABISizeOfType(c.LLVMGetModuleDataLayout(self.e.llvm_module), raw_llvm_ty); + return size > 16; + } + + pub fn materializeByvalArg(self: AbiLowering, val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef) c.LLVMValueRef { + const tmp = c.LLVMBuildAlloca(self.e.builder, struct_ty, "byval.tmp"); + _ = c.LLVMBuildStore(self.e.builder, val, tmp); + return tmp; + } +}; diff --git a/src/backend/llvm/types.zig b/src/backend/llvm/types.zig new file mode 100644 index 0000000..aa7c54c --- /dev/null +++ b/src/backend/llvm/types.zig @@ -0,0 +1,178 @@ +const std = @import("std"); +const llvm = @import("../../llvm_api.zig"); +const c = llvm.c; +const ir_types = @import("../../ir/types.zig"); +const emit = @import("../../ir/emit_llvm.zig"); + +const TypeId = ir_types.TypeId; +const LLVMEmitter = emit.LLVMEmitter; + +/// IR-type → LLVM-type lowering (architecture phase A7.1), extracted from +/// `LLVMEmitter`. A backend `*LLVMEmitter` facade (the backend analogue of the +/// IR-side `*Lowering` facades): it borrows the emitter for the cached LLVM +/// handles (`context`/`cached_*`), the IR type table (`ir_mod`), the scratch +/// allocator, and the memoizing composite-type getters +/// (`getStringStructType`/`getAnyStructType`/`getClosureStructType`) that stay +/// on `LLVMEmitter`. `LLVMEmitter.toLLVMType` is a thin wrapper delegating here. +pub const TypeLowering = struct { + e: *LLVMEmitter, + + pub fn toLLVMType(self: TypeLowering, ty: TypeId) c.LLVMTypeRef { + return switch (ty) { + .void => self.e.cached_void, + .bool => self.e.cached_i1, + .s8 => self.e.cached_i8, + .s16 => self.e.cached_i16, + .s32 => self.e.cached_i32, + .s64 => self.e.cached_i64, + .u8 => self.e.cached_i8, + .u16 => self.e.cached_i16, + .u32 => self.e.cached_i32, + .u64 => self.e.cached_i64, + .f32 => self.e.cached_f32, + .f64 => self.e.cached_f64, + .string => self.e.getStringStructType(), + .any => self.e.getAnyStructType(), + .noreturn => self.e.cached_void, + .isize, .usize => if (self.e.target_config.isWasm32()) self.e.cached_i32 else self.e.cached_i64, + else => self.toLLVMTypeInfo(ty), + }; + } + + fn toLLVMTypeInfo(self: TypeLowering, ty: TypeId) c.LLVMTypeRef { + const info = self.e.ir_mod.types.get(ty); + return switch (info) { + .signed => |w| switch (w) { + 1 => self.e.cached_i1, + 8 => self.e.cached_i8, + 16 => self.e.cached_i16, + 32 => self.e.cached_i32, + 64 => self.e.cached_i64, + else => c.LLVMIntTypeInContext(self.e.context, w), + }, + .unsigned => |w| switch (w) { + 1 => self.e.cached_i1, + 8 => self.e.cached_i8, + 16 => self.e.cached_i16, + 32 => self.e.cached_i32, + 64 => self.e.cached_i64, + else => c.LLVMIntTypeInContext(self.e.context, w), + }, + .f32 => self.e.cached_f32, + .f64 => self.e.cached_f64, + .void => self.e.cached_void, + .bool => self.e.cached_i1, + .error_set => self.e.cached_i32, // u32 tag id on the error channel + .string => self.e.getStringStructType(), + .pointer, .many_pointer, .function => self.e.cached_ptr, + .closure => self.e.getClosureStructType(), + .slice => self.e.getStringStructType(), // same {ptr, i64} layout + .optional => |opt| { + // ?*T / ?fn → bare pointer (null = none) + const child_info = self.e.ir_mod.types.get(opt.child); + if (child_info == .pointer or child_info == .many_pointer or child_info == .function) { + return self.e.cached_ptr; + } + if (child_info == .closure) { + return self.e.getClosureStructType(); + } + // ?Protocol → protocol struct (ctx ptr = field 0 is null when none). + if (child_info == .@"struct" and child_info.@"struct".is_protocol) { + return self.toLLVMType(opt.child); + } + // ?T → { T, i1 } + var field_types: [2]c.LLVMTypeRef = .{ + self.toLLVMType(opt.child), + self.e.cached_i1, + }; + return c.LLVMStructTypeInContext(self.e.context, &field_types, 2, 0); + }, + .array => |arr| { + const elem = self.toLLVMType(arr.element); + return c.LLVMArrayType2(elem, arr.length); + }, + .vector => |vec| { + const elem = self.toLLVMType(vec.element); + return c.LLVMVectorType(elem, vec.length); + }, + .any => self.e.getAnyStructType(), + .noreturn => self.e.cached_void, + .@"struct" => |s| { + // Build LLVM struct type from fields + const n: c_uint = @intCast(s.fields.len); + const field_llvm_types = self.e.alloc.alloc(c.LLVMTypeRef, s.fields.len) catch unreachable; + defer self.e.alloc.free(field_llvm_types); + for (s.fields, 0..) |field, j| { + field_llvm_types[j] = self.toLLVMType(field.ty); + } + return c.LLVMStructTypeInContext(self.e.context, field_llvm_types.ptr, n, 0); + }, + .@"enum" => |e| { + // Use backing type if declared (e.g. enum u32 → i32), else i64 + if (e.backing_type) |bt| return self.toLLVMType(bt); + return self.e.cached_i64; + }, + .@"union" => |u| { + // Untagged union — just [N x i8] + var max_size: usize = 0; + for (u.fields) |field| { + const sz = self.e.ir_mod.types.typeSizeBytes(field.ty); + if (sz > max_size) max_size = sz; + } + if (max_size == 0) max_size = 8; + return c.LLVMArrayType2(self.e.cached_i8, @intCast(max_size)); + }, + .tagged_union => |u| { + // Tagged union — { header, [N x i8] } + var max_size: usize = 0; + for (u.fields) |field| { + const sz = self.e.ir_mod.types.typeSizeBytes(field.ty); + if (sz > max_size) max_size = sz; + } + if (max_size == 0) max_size = 8; + + var header_size: usize = self.e.ir_mod.types.typeSizeBytes(u.tag_type); + if (u.backing_type) |bt| { + const bi = self.e.ir_mod.types.get(bt); + if (bi == .@"struct" and bi.@"struct".fields.len > 1) { + header_size = 0; + const fields = bi.@"struct".fields; + for (fields[0 .. fields.len - 1]) |f| { + header_size += self.e.ir_mod.types.typeSizeBytes(f.ty); + } + const backing_payload = self.e.ir_mod.types.typeSizeBytes(fields[fields.len - 1].ty); + if (backing_payload > max_size) max_size = backing_payload; + } + } + + const header_llvm = c.LLVMIntTypeInContext(self.e.context, @intCast(header_size * 8)); + var field_types: [2]c.LLVMTypeRef = .{ + header_llvm, + c.LLVMArrayType2(self.e.cached_i8, @intCast(max_size)), + }; + return c.LLVMStructTypeInContext(self.e.context, &field_types, 2, 0); + }, + .tuple => |t| { + const n: c_uint = @intCast(t.fields.len); + const field_llvm_types = self.e.alloc.alloc(c.LLVMTypeRef, t.fields.len) catch unreachable; + defer self.e.alloc.free(field_llvm_types); + for (t.fields, 0..) |f, j| { + field_llvm_types[j] = self.toLLVMType(f); + } + return c.LLVMStructTypeInContext(self.e.context, field_llvm_types.ptr, n, 0); + }, + .protocol => { + // Protocol values: { ctx: *void, vtable_or_fn_ptrs... } + // For now, use opaque ptr + return self.e.cached_ptr; + }, + .usize, .isize => if (self.e.target_config.isWasm32()) self.e.cached_i32 else self.e.cached_i64, + // Comptime-only: a pack is expanded to flat positional args before + // codegen, so it must never reach LLVM type emission. + .pack => @panic("pack type has no LLVM representation (comptime-only)"), + // Tripwire: a failed type resolution must have been diagnosed and + // aborted long before LLVM emission. + .unresolved => @panic("unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted"), + }; + } +}; diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index a0c2deb..9f60884 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -10,6 +10,8 @@ const TypeInfo = ir_types.TypeInfo; const TypeTable = ir_types.TypeTable; const StringId = ir_types.StringId; const errors = @import("../errors.zig"); +const llvm_types = @import("../backend/llvm/types.zig"); +const llvm_abi = @import("../backend/llvm/abi.zig"); const ir_inst = @import("inst.zig"); const Ref = ir_inst.Ref; const Span = ir_inst.Span; @@ -4551,276 +4553,44 @@ pub const LLVMEmitter = struct { // ── Type conversion ───────────────────────────────────────────── - pub fn toLLVMType(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef { - return switch (ty) { - .void => self.cached_void, - .bool => self.cached_i1, - .s8 => self.cached_i8, - .s16 => self.cached_i16, - .s32 => self.cached_i32, - .s64 => self.cached_i64, - .u8 => self.cached_i8, - .u16 => self.cached_i16, - .u32 => self.cached_i32, - .u64 => self.cached_i64, - .f32 => self.cached_f32, - .f64 => self.cached_f64, - .string => self.getStringStructType(), - .any => self.getAnyStructType(), - .noreturn => self.cached_void, - .isize, .usize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64, - else => self.toLLVMTypeInfo(ty), - }; + fn typeLowering(self: *LLVMEmitter) llvm_types.TypeLowering { + return .{ .e = self }; } - fn toLLVMTypeInfo(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef { - const info = self.ir_mod.types.get(ty); - return switch (info) { - .signed => |w| switch (w) { - 1 => self.cached_i1, - 8 => self.cached_i8, - 16 => self.cached_i16, - 32 => self.cached_i32, - 64 => self.cached_i64, - else => c.LLVMIntTypeInContext(self.context, w), - }, - .unsigned => |w| switch (w) { - 1 => self.cached_i1, - 8 => self.cached_i8, - 16 => self.cached_i16, - 32 => self.cached_i32, - 64 => self.cached_i64, - else => c.LLVMIntTypeInContext(self.context, w), - }, - .f32 => self.cached_f32, - .f64 => self.cached_f64, - .void => self.cached_void, - .bool => self.cached_i1, - .error_set => self.cached_i32, // u32 tag id on the error channel - .string => self.getStringStructType(), - .pointer, .many_pointer, .function => self.cached_ptr, - .closure => self.getClosureStructType(), - .slice => self.getStringStructType(), // same {ptr, i64} layout - .optional => |opt| { - // ?*T / ?fn → bare pointer (null = none) - const child_info = self.ir_mod.types.get(opt.child); - if (child_info == .pointer or child_info == .many_pointer or child_info == .function) { - return self.cached_ptr; - } - if (child_info == .closure) { - return self.getClosureStructType(); - } - // ?Protocol → protocol struct (ctx ptr = field 0 is null when none). - if (child_info == .@"struct" and child_info.@"struct".is_protocol) { - return self.toLLVMType(opt.child); - } - // ?T → { T, i1 } - var field_types: [2]c.LLVMTypeRef = .{ - self.toLLVMType(opt.child), - self.cached_i1, - }; - return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); - }, - .array => |arr| { - const elem = self.toLLVMType(arr.element); - return c.LLVMArrayType2(elem, arr.length); - }, - .vector => |vec| { - const elem = self.toLLVMType(vec.element); - return c.LLVMVectorType(elem, vec.length); - }, - .any => self.getAnyStructType(), - .noreturn => self.cached_void, - .@"struct" => |s| { - // Build LLVM struct type from fields - const n: c_uint = @intCast(s.fields.len); - const field_llvm_types = self.alloc.alloc(c.LLVMTypeRef, s.fields.len) catch unreachable; - defer self.alloc.free(field_llvm_types); - for (s.fields, 0..) |field, j| { - field_llvm_types[j] = self.toLLVMType(field.ty); - } - return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0); - }, - .@"enum" => |e| { - // Use backing type if declared (e.g. enum u32 → i32), else i64 - if (e.backing_type) |bt| return self.toLLVMType(bt); - return self.cached_i64; - }, - .@"union" => |u| { - // Untagged union — just [N x i8] - var max_size: usize = 0; - for (u.fields) |field| { - const sz = self.ir_mod.types.typeSizeBytes(field.ty); - if (sz > max_size) max_size = sz; - } - if (max_size == 0) max_size = 8; - return c.LLVMArrayType2(self.cached_i8, @intCast(max_size)); - }, - .tagged_union => |u| { - // Tagged union — { header, [N x i8] } - var max_size: usize = 0; - for (u.fields) |field| { - const sz = self.ir_mod.types.typeSizeBytes(field.ty); - if (sz > max_size) max_size = sz; - } - if (max_size == 0) max_size = 8; + fn abiLowering(self: *LLVMEmitter) llvm_abi.AbiLowering { + return .{ .e = self }; + } - var header_size: usize = self.ir_mod.types.typeSizeBytes(u.tag_type); - if (u.backing_type) |bt| { - const bi = self.ir_mod.types.get(bt); - if (bi == .@"struct" and bi.@"struct".fields.len > 1) { - header_size = 0; - const fields = bi.@"struct".fields; - for (fields[0 .. fields.len - 1]) |f| { - header_size += self.ir_mod.types.typeSizeBytes(f.ty); - } - const backing_payload = self.ir_mod.types.typeSizeBytes(fields[fields.len - 1].ty); - if (backing_payload > max_size) max_size = backing_payload; - } - } - - const header_llvm = c.LLVMIntTypeInContext(self.context, @intCast(header_size * 8)); - var field_types: [2]c.LLVMTypeRef = .{ - header_llvm, - c.LLVMArrayType2(self.cached_i8, @intCast(max_size)), - }; - return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); - }, - .tuple => |t| { - const n: c_uint = @intCast(t.fields.len); - const field_llvm_types = self.alloc.alloc(c.LLVMTypeRef, t.fields.len) catch unreachable; - defer self.alloc.free(field_llvm_types); - for (t.fields, 0..) |f, j| { - field_llvm_types[j] = self.toLLVMType(f); - } - return c.LLVMStructTypeInContext(self.context, field_llvm_types.ptr, n, 0); - }, - .protocol => { - // Protocol values: { ctx: *void, vtable_or_fn_ptrs... } - // For now, use opaque ptr - return self.cached_ptr; - }, - .usize, .isize => if (self.target_config.isWasm32()) self.cached_i32 else self.cached_i64, - // Comptime-only: a pack is expanded to flat positional args before - // codegen, so it must never reach LLVM type emission. - .pack => @panic("pack type has no LLVM representation (comptime-only)"), - // Tripwire: a failed type resolution must have been diagnosed and - // aborted long before LLVM emission. - .unresolved => @panic("unresolved type reached LLVM emission — a type resolution failure was not diagnosed/aborted"), - }; + /// IR-type → LLVM-type lowering lives in `backend/llvm/types.zig` + /// (`TypeLowering`). This stays the facade entry point (~97 callers). + pub fn toLLVMType(self: *LLVMEmitter, ty: TypeId) c.LLVMTypeRef { + return self.typeLowering().toLLVMType(ty); } // ── C ABI coercion for foreign functions ────────────────────────── - // - // On ARM64 (and x86_64), the C calling convention coerces small struct - // arguments to integers for register passing: - // - String/slice {ptr, i64} → ptr (extract raw pointer) - // - Small integer struct (≤ 8 bytes, non-HFA) → i64 - // - HFA (homogeneous float aggregate) → leave as-is (LLVM handles it) + // The coercion logic lives in `backend/llvm/abi.zig` (`AbiLowering`); + // these stay the facade entry points (callers in signature/call emission + + // the block-trampoline path use abiCoerceParamTypeEx directly). pub fn abiCoerceParamType(self: *LLVMEmitter, ir_ty: TypeId, llvm_ty: c.LLVMTypeRef) c.LLVMTypeRef { - return self.abiCoerceParamTypeEx(ir_ty, llvm_ty, true); + return self.abiLowering().abiCoerceParamType(ir_ty, llvm_ty); } - /// 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). - // Other integer types (s64, u64) keep their declared size — they represent - // genuinely 64-bit values (SDL_WindowFlags, timestamps, etc.). - if (self.target_config.isWasm32()) { - if (ir_ty == .usize or ir_ty == .isize) return self.cached_i32; - return llvm_ty; - } - - // Only coerce struct types - if (c.LLVMGetTypeKind(llvm_ty) != c.LLVMStructTypeKind) return llvm_ty; - - // Check if it's an HFA (all float or all double fields) — leave as-is - const n_fields = c.LLVMCountStructElementTypes(llvm_ty); - if (n_fields >= 1 and n_fields <= 4) { - var all_float = true; - var all_double = true; - var fi: c_uint = 0; - while (fi < n_fields) : (fi += 1) { - const ft = c.LLVMStructGetTypeAtIndex(llvm_ty, fi); - const fk = c.LLVMGetTypeKind(ft); - if (fk != c.LLVMFloatTypeKind) all_float = false; - if (fk != c.LLVMDoubleTypeKind) all_double = false; - } - if (all_float or all_double) return llvm_ty; - } - - // Small struct (≤ 8 bytes) → coerce to i64 - const size = c.LLVMABISizeOfType( - c.LLVMGetModuleDataLayout(self.llvm_module), - llvm_ty, - ); - if (size <= 8) return self.cached_i64; - - // Medium struct (9-16 bytes) → coerce to [2 x i64] - if (size <= 16) { - return c.LLVMArrayType2(self.cached_i64, 2); - } - - // Large composite (> 16 bytes) → pass by reference: ptr + byval() at - // the call/sig sites. LLVM's AArch64/x86_64 backend lowers byval to - // the right ABI sequence (caller copy + indirect arg). - return self.cached_ptr; + return self.abiLowering().abiCoerceParamTypeEx(ir_ty, llvm_ty, is_foreign_c_api); } pub fn needsByval(self: *LLVMEmitter, ir_ty: TypeId, raw_llvm_ty: c.LLVMTypeRef) bool { - if (self.target_config.isWasm32()) return false; - if (ir_ty == .string) return false; - if (!ir_ty.isBuiltin()) { - const info = self.ir_mod.types.get(ir_ty); - if (info == .slice) return false; - } - if (c.LLVMGetTypeKind(raw_llvm_ty) != c.LLVMStructTypeKind) return false; - const n = c.LLVMCountStructElementTypes(raw_llvm_ty); - if (n >= 1 and n <= 4) { - var all_f = true; - var all_d = true; - var i: c_uint = 0; - while (i < n) : (i += 1) { - const ft = c.LLVMStructGetTypeAtIndex(raw_llvm_ty, i); - const fk = c.LLVMGetTypeKind(ft); - if (fk != c.LLVMFloatTypeKind) all_f = false; - if (fk != c.LLVMDoubleTypeKind) all_d = false; - } - if (all_f or all_d) return false; - } - const size = c.LLVMABISizeOfType(c.LLVMGetModuleDataLayout(self.llvm_module), raw_llvm_ty); - return size > 16; + return self.abiLowering().needsByval(ir_ty, raw_llvm_ty); } fn materializeByvalArg(self: *LLVMEmitter, val: c.LLVMValueRef, struct_ty: c.LLVMTypeRef) c.LLVMValueRef { - const tmp = c.LLVMBuildAlloca(self.builder, struct_ty, "byval.tmp"); - _ = c.LLVMBuildStore(self.builder, val, tmp); - return tmp; + return self.abiLowering().materializeByvalArg(val, struct_ty); } // ── Cached composite types ────────────────────────────────────── - fn getStringStructType(self: *LLVMEmitter) c.LLVMTypeRef { + pub fn getStringStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.string_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ self.cached_ptr, // ptr @@ -4892,7 +4662,7 @@ pub const LLVMEmitter = struct { return c.LLVMConstPtrToInt(g, self.cached_i64); } - fn getAnyStructType(self: *LLVMEmitter) c.LLVMTypeRef { + pub fn getAnyStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.any_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ self.cached_i64, // type tag @@ -4902,7 +4672,7 @@ pub const LLVMEmitter = struct { return self.any_struct_type.?; } - fn getClosureStructType(self: *LLVMEmitter) c.LLVMTypeRef { + pub fn getClosureStructType(self: *LLVMEmitter) c.LLVMTypeRef { if (self.closure_struct_type) |t| return t; var field_types = [_]c.LLVMTypeRef{ self.cached_ptr, // fn_ptr