vendors/ is a third-party namespace (stb_image, kb_text_shape, etc.);
test fixtures don't belong there. The .c/.h companion files for the
Phase-0 FFI baselines now sit alongside the .sx that drives them in
examples/, with matching basenames:
examples/ffi-01-primitives.{sx,c,h} <- was vendors/ffi_primitives/
examples/ffi-02-small-struct.{sx,c,h} <- was vendors/ffi_structs/
examples/ffi-03-large-struct.{sx,c,h} <- was vendors/ffi_large_struct/
examples/ffi-04-fp-struct.{sx,c,h} <- was vendors/ffi_fp_struct/
examples/ffi-05-string-args.{sx,c,h} <- was vendors/ffi_strings/
examples/ffi-06-callback.{sx,c,h} <- was vendors/ffi_callback/
examples/101-ffi-medium-struct.{sx,c} <- was vendors/ffi_medium_struct/
`#source` / `#include` paths in the .sx files become bare filenames
(no prefix) since imports.zig's base_dir resolution finds them
relative to the importing .sx file's directory.
`library/vendors/sx_ffi_resolve_test/` stays put — that one's the
whole point: regression coverage for the stdlib-search branch of
the resolution chain, so it must live where ONLY that branch can
find it.
94/94 regression tests pass.
93/93 regression tests pass (+ffi-06-callback).
Mirrors the `app->onInputEvent` install pattern from
library/modules/platform/android.sx:
1. (s32) -> s32 — single primitive arg/return
2. (*void, s32) -> s32 — opaque ctx pointer + value
(the onInputEvent shape)
Side effects via two file-level globals so the test observes both
the return value AND state mutation across multiple calls:
- g_callback_hits = N proves the callback fired N times.
- g_callback_sum = sum of args proves each individual call landed
with the correct value.
The ctx-pointer variant casts `*void` back to `*s32` inside the
callback and reads through it (`p.*`), proving the pointer survives
the round-trip with no aliasing weirdness.
92/92 regression tests pass (+ffi-05-string-args).
Covers the four shapes that actually appear at the sx ↔ C boundary
today:
1. [:0]u8 string literal -> const char* (ffi_strlen, ffi_first_byte)
2. sx `string` value via .ptr (slice-decay branch in
coerceArg pulls the pointer)
3. [*]u8 raw buffer + length (ffi_sum_bytes, mutated via
ffi_write_byte and read back)
4. C-returned const char* (round-trips back as [*]u8)
The mutate-via-C path catches any pointer-aliasing regression — sx
allocates the fixed array `bytes : [4]u8`, passes `.ptr` to C which
writes index 1, and the sx side reads `bytes[1]` to confirm the
mutation took effect through the same memory.
91/91 regression tests pass (+ffi-04-fp-struct).
Single-file regression net for the all-float / all-double aggregate
ABI path:
FQuad — 16 B, 4×f32 (same slot as ffi-02's Vec4f)
DQuad — 32 B, 4×f64 (UIEdgeInsets-shape — the f32-vs-f64 landmine)
Already nominally covered by ffi-02's Vec4f, but pinning it as a
focused single-file test means a future ABI rule change that breaks
the HFA path fails *this* test directly without a noisy drag-in from
the multi-shape baseline.
DQuad at 32 B straddles the AAPCS64 HFA limit (≤4 floats of same
type, total ≤64 B); it stays as a struct value passed through
v0..v3 rather than going indirect. The snapshot confirms the values
arrive intact.
90/90 regression tests pass (+ffi-03-large-struct).
vendors/ffi_large_struct/{.h,.c} defines:
Big24 — 24 B, three s64 (byval params + sret return)
Big48 — 48 B, six s64 (same path, larger)
`make / rotate-or-reverse / sum` helpers per shape. The sx-side
example imports via `#source` only and declares matching structs +
hand-written #foreign decls.
Snapshot pins today's >16-byte aggregate ABI now that the
emit_llvm.zig sret-return transform is in place (previous commit).
That gives us a regression net for all four C-ABI aggregate slots
in one place:
≤8 B int — i64 coercion (ffi-01 vec-likes)
9..16 B int — [2 x i64] coercion (ffi-02 Pair64/Quad32, 101)
16 B HFA — struct, no coercion (ffi-02 Vec2/Vec4f)
>16 B — byval params + sret (this commit)
Now that emit_llvm.zig bridges the struct<->[2 x i64] ABI mismatch
(previous commit), the 9..16-byte integer-only shapes round-trip
cleanly. Extended `examples/ffi-02-small-struct.sx` to cover all
four aggregate ABI slots in one place:
Vec2 — 8 B, two f32 (register pair, float)
Vec4f — 16 B, four f32 (HFA — homogeneous float aggregate)
Pair64 — 16 B, two s64 (9..16 B int — [2 x i64] coercion slot)
Quad32 — 16 B, four s32 (same slot as Pair64)
Vendor helpers (`vendors/ffi_structs/{ffi_structs.h,ffi_structs.c}`)
grow `ffi_pair64_*` + `ffi_quad32_*` companions. Snapshot updated
to capture the full output. 89/89 regression tests pass.
`examples/101-ffi-medium-struct.sx` keeps a minimal focused repro
of the Pair64 case so the issue's emergence-and-fix history stays
greppable.
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.
Surfaced while writing the ffi-02-small-struct.sx baseline. The sx
#foreign decl lowers `{ s64, s64 }` (and other 16-byte integer-only
shapes like `{ s32, s32, s32, s32 }`) to `[2 x i64]` for the small-
struct register-pair ABI on AAPCS64 / SysV AMD64, but the call site
loads the struct as `{ i64, i64 }`. The two types must agree for the
LLVM verifier to accept the call:
Call parameter type does not match function signature!
%load = load { i64, i64 }, ptr %alloca, align 8
[2 x i64] %call = call [2 x i64] @issue0036_swap({ i64, i64 } %load)
Float-only 16-byte aggregates (e.g. Vec4f) work because they route
through the HFA path which keeps the struct representation. See
examples/ffi-02-small-struct.sx for the working cases.
Phase 1's #foreign lowering rework is the natural place to unify
these representations; check there before fixing inline.
88/88 regression tests pass (+ffi-02-small-struct).
vendors/ffi_structs/ defines:
Vec2 — 8 B, two f32 — register-pair (float) ABI
Vec4f — 16 B, four f32 — homogeneous float aggregate (HFA) on AAPCS64
Both pass cleanly today: the sx-side struct declarations match the C
ABI for these float-only shapes, and the call-site / foreign-decl
type representations agree.
`#source` only (no `#include`) — c_import's type mapping rewrites
struct-typed params/returns to *void, which would link but pass
through the wrong ABI silently. The hand-written #foreign decls keep
sx's struct types end to end.
16-byte integer-only shapes (`{s64, s64}`, `{s32, s32, s32, s32}`)
discovered to trip the LLVM verifier (`[2 x i64]` vs `{ i64, i64 }`
mismatch between foreign decl and call site). Excluded from this
baseline; filed separately in the next commit as issue-0036.
87/87 regression tests pass (was 86; +ffi-01-primitives).
vendors/ffi_primitives/{.h,.c} exposes a trivial identity roundtrip
per primitive C type — int/uint/short/ushort/long long/unsigned long
long/signed char/unsigned char/float/double/void* — plus two-arg
add helpers (int + double) for multi-arg ABI exercise. The sx-side
example imports the .h via `#import c { #include / #source }` and
prints each result; the snapshot in tests/expected pins today's
parameter + return ABI so Phase 1's #objc_call / #jni_call lowering
work can't silently regress primitive marshalling.
Two findings logged in current/CHECKPOINT-FFI.md's Known issues
section (current behavior, not new bugs): (1) c_import.zig maps
`signed char` -> `u8` not `s8`, and (2) sx integer-literal parser
rejects values >= 2^63 as overflow even when the receiver is u64.
Both worked around in this test without blocking the baseline.