emit_llvm: bridge struct<->array ABI for 9..16-byte foreign structs
Resolves issue-0036 (LLVM verifier failure on 16-byte integer-only
struct by value through #foreign). The mismatch:
Call parameter type does not match function signature!
%load = load { i64, i64 }, ptr %alloca, align 8
[2 x i64] %call = call [2 x i64] @fn({ i64, i64 } %load)
`abiCoerceParamType` had already chosen `[2 x i64]` for 9..16-byte
non-HFA structs (the AAPCS64 / SysV AMD64 register-pair ABI slot for
that size class) on the foreign-decl side, but `coerceArg` only knew
how to bridge struct<->integer (the ≤8 B case) — not struct<->array.
LLVM's verifier rejects type-mismatched call args, so the call site
never landed.
Added the symmetric branches in coerceArg:
- Struct -> Array : alloca <array>; store <struct>; load <array>
- Array -> Struct : alloca <array>; store <array>; load <struct>
Both use the LLVM opaque-pointer memory-bitcast pattern already in
place for the integer case. They're paired with the existing
i64 <-> small-struct bridge so all four (≤8 B int, 9..16 B int,
16 B HFA, >16 B byval) ABI slots round-trip cleanly through
emit_llvm now.
File mechanics: promotes the issue-0036 repro to a focused feature
example per CLAUDE.md's issue-resolution workflow:
examples/issue-0036.sx -> examples/101-ffi-medium-struct.sx
tests/expected/issue-0036.{txt,exit} -> tests/expected/101-ffi-medium-struct.{txt,exit}
vendors/issue_0036/issue_0036.c -> vendors/ffi_medium_struct/ffi_medium_struct.c
Snapshot updated to the passing output. 89/89 regression tests pass;
chess Android build still clean.
This commit is contained in:
33
examples/101-ffi-medium-struct.sx
Normal file
33
examples/101-ffi-medium-struct.sx
Normal file
@@ -0,0 +1,33 @@
|
||||
// 16-byte integer-only struct passed by value through `#foreign`.
|
||||
//
|
||||
// emit_llvm.zig's `abiCoerceParamType` routes 9..16-byte non-HFA
|
||||
// structs through `[2 x i64]` for register-pair passing on AAPCS64 /
|
||||
// SysV AMD64. The call-site side loads the sx struct as `{ i64, i64 }`,
|
||||
// so the verifier needs a memory-bitcast bridge in both directions
|
||||
// (`abi.struct2arr` on the way in, `abi.arr2struct` on the way back).
|
||||
//
|
||||
// Different ABI path from 8-byte register-pair structs (coerce to
|
||||
// i64), HFAs at the same total size (Vec4f stays a struct), and
|
||||
// >16-byte aggregates (passed via pointer + byval).
|
||||
//
|
||||
// This file was originally filed as issue-0036 (LLVM verifier
|
||||
// failure repro). Promoted to a focused feature example once the
|
||||
// emit_llvm.zig coerceArg branches landed.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#source "vendors/ffi_medium_struct/ffi_medium_struct.c";
|
||||
};
|
||||
|
||||
Pair64 :: struct { a: s64; b: s64; }
|
||||
|
||||
ffi_pair64_swap :: (p: Pair64) -> Pair64 #foreign;
|
||||
|
||||
main :: () -> s32 {
|
||||
p : Pair64 = .{ a = 1, b = 2 };
|
||||
q := ffi_pair64_swap(p);
|
||||
print("swap = ({}, {})\n", q.a, q.b);
|
||||
print("ok = {}\n", q.a == 2 and q.b == 1);
|
||||
0;
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
// 16-byte integer-only struct by value through `#foreign` trips
|
||||
// the LLVM verifier:
|
||||
//
|
||||
// Call parameter type does not match function signature!
|
||||
// %loadN = load { i64, i64 }, ptr %allocaM, align 8
|
||||
// [2 x i64] %callN = call [2 x i64] @issue0036_swap({ i64, i64 } %load...)
|
||||
//
|
||||
// The foreign-decl side lowers `Pair64` to `[2 x i64]` (ABI-coercion
|
||||
// for the small-struct register-pair return on AAPCS64 / SysV AMD64),
|
||||
// while the call site loads the sx struct as `{ i64, i64 }`. The two
|
||||
// representations need to agree.
|
||||
//
|
||||
// Float-only aggregates of the same total size (e.g. Vec4f, 4×f32 =
|
||||
// 16 B) work today because they're routed through the HFA path which
|
||||
// keeps the struct representation. See `examples/ffi-02-small-struct.sx`
|
||||
// for the working cases.
|
||||
//
|
||||
// Expected once fixed: print "ok = true" with exit 0. Today this
|
||||
// example fails at codegen with a non-zero exit; the snapshot captures
|
||||
// that failure so the test suite stays honest.
|
||||
|
||||
#import "modules/std.sx";
|
||||
|
||||
#import c {
|
||||
#source "vendors/issue_0036/issue_0036.c";
|
||||
};
|
||||
|
||||
Pair64 :: struct { a: s64; b: s64; }
|
||||
|
||||
issue0036_swap :: (p: Pair64) -> Pair64 #foreign;
|
||||
|
||||
main :: () -> s32 {
|
||||
p : Pair64 = .{ a = 1, b = 2 };
|
||||
q := issue0036_swap(p);
|
||||
print("swap = ({}, {})\n", q.a, q.b);
|
||||
print("ok = {}\n", q.a == 2 and q.b == 1);
|
||||
0;
|
||||
}
|
||||
@@ -2607,6 +2607,21 @@ pub const LLVMEmitter = struct {
|
||||
_ = c.LLVMBuildStore(self.builder, val, tmp);
|
||||
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.ret.coerce");
|
||||
}
|
||||
// Struct → Array (C ABI coercion for 9..16-byte structs — paired with
|
||||
// abiCoerceParamType's `[2 x i64]` slot for that size class). Same
|
||||
// memory-bitcast pattern as the integer case; the array type carries
|
||||
// 16 bytes of storage so we alloca with param_ty to guarantee size.
|
||||
if (val_kind == c.LLVMStructTypeKind and param_kind == c.LLVMArrayTypeKind) {
|
||||
const tmp = c.LLVMBuildAlloca(self.builder, param_ty, "abi.struct2arr");
|
||||
_ = c.LLVMBuildStore(self.builder, val, tmp);
|
||||
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.coerce.arr");
|
||||
}
|
||||
// Array → Struct (return-side counterpart for 9..16-byte structs)
|
||||
if (val_kind == c.LLVMArrayTypeKind and param_kind == c.LLVMStructTypeKind) {
|
||||
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "abi.arr2struct");
|
||||
_ = c.LLVMBuildStore(self.builder, val, tmp);
|
||||
return c.LLVMBuildLoad2(self.builder, param_ty, tmp, "abi.ret.coerce.arr");
|
||||
}
|
||||
// Array → Ptr (array decay: alloca + GEP to first element)
|
||||
if (val_kind == c.LLVMArrayTypeKind and param_kind == c.LLVMPointerTypeKind) {
|
||||
const tmp = c.LLVMBuildAlloca(self.builder, val_ty, "ca.arr");
|
||||
|
||||
1
tests/expected/101-ffi-medium-struct.exit
Normal file
1
tests/expected/101-ffi-medium-struct.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
2
tests/expected/101-ffi-medium-struct.txt
Normal file
2
tests/expected/101-ffi-medium-struct.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
swap = (2, 1)
|
||||
ok = true
|
||||
@@ -1 +0,0 @@
|
||||
1
|
||||
@@ -1,3 +0,0 @@
|
||||
LLVM verification failed: Call parameter type does not match function signature!
|
||||
%load = load { i64, i64 }, ptr %alloca5, align 8
|
||||
[2 x i64] %call = call [2 x i64] @issue0036_swap({ i64, i64 } %load)
|
||||
13
vendors/ffi_medium_struct/ffi_medium_struct.c
vendored
Normal file
13
vendors/ffi_medium_struct/ffi_medium_struct.c
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
// Companion to examples/101-ffi-medium-struct.sx — a single
|
||||
// roundtrip through a 16-byte integer-only struct. Pinned in a
|
||||
// dedicated example because integer aggregates in this size class
|
||||
// route through emit_llvm.zig's `[2 x i64]` ABI coercion slot, a
|
||||
// different path from the small (≤8 B) integer struct, the 16-byte
|
||||
// HFA, and the >16 B byval-pointer cases.
|
||||
|
||||
typedef struct { long long a; long long b; } Pair64;
|
||||
|
||||
Pair64 ffi_pair64_swap(Pair64 p) {
|
||||
Pair64 r = { p.b, p.a };
|
||||
return r;
|
||||
}
|
||||
9
vendors/issue_0036/issue_0036.c
vendored
9
vendors/issue_0036/issue_0036.c
vendored
@@ -1,9 +0,0 @@
|
||||
// Repro for issue-0036: 16-byte integer-only struct by-value through
|
||||
// #foreign. See examples/issue-0036.sx for details.
|
||||
|
||||
typedef struct { long long a; long long b; } Pair64;
|
||||
|
||||
Pair64 issue0036_swap(Pair64 p) {
|
||||
Pair64 r = { p.b, p.a };
|
||||
return r;
|
||||
}
|
||||
Reference in New Issue
Block a user