From 6bd353bb8c26d920fd1e7683c3dfb1f972e90227 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 09:23:36 +0700 Subject: [PATCH] The aggregate claim, checked against a control instead of asserted The same three shapes written in C and as first-class IR aggregates, compiled by the same clang. { i8, i64 } agrees. { i8, float } does not: C packs both halves into rax, the IR form answers in al and xmm0. And a 24-byte struct does not agree at all -- C spills through an sret pointer, the IR form returns it in rax, rdx and rcx, and rcx is a register SysV never uses for a return value. That resolves the ret-big anomaly the first pass noted and moved past, and it makes the finding stronger than it was written: the internal convention is not the C ABI, not only undocumented in the emitted IR. spike_call0 deleted with it -- declared, never bound, and the two unused probes were removed for the same reason. --- DISCUSS.md | 27 +++++++++++++++++++-------- spike/backend/jit_stubs.c | 4 ---- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/DISCUSS.md b/DISCUSS.md index a2cd5f0..e1c391b 100644 --- a/DISCUSS.md +++ b/DISCUSS.md @@ -787,17 +787,28 @@ output rather than read off a table: - `%slice` argument → `rdi`:`rsi`, two registers, and the next argument shifts along. - `{ i8, i64 }` return → tag in `al`, value in `rdx`. -- `{ i8, float }` return → tag in `al`, value in **`xmm0`**. C's psABI would classify that single eightbyte as - INTEGER and pack both into `rax`. **LLVM's lowering of a first-class aggregate in IR is per-field, and it is not - the C ABI.** +- `{ i8, float }` return → tag in `al`, value in **`xmm0`**. - `%vec` return (six words) → a hidden `sret` pointer in `rdi`, the real arguments shifted along behind it, and the pointer returned in `rax`. **That pointer does not appear in the `define` line at all.** -So **the internal calling convention is not specified anywhere. It is whatever LLVM's backend does with a first-class -struct, discoverable only by disassembling.** That is the sharpest obstacle in this report, and it is sharper than -raylib for three reasons: the reference is an implementation rather than a document; the failure mode is a garbage -field rather than a link error; and it is not stable by contract across LLVM versions, which is exactly the coupling -`plan.org` chose text IR to avoid. +The last two are not the C ABI, and that was checked against a control rather than asserted — the same three shapes +written in C, compiled by the same clang at `-O2`, beside the same shapes written as first-class IR aggregates: + +| shape | from C | from IR | +|---|---|---| +| `{ i8, i64 }` | `al` + `rdx` | `al` + `rdx` — agree | +| `{ i8, float }` | **packed into `rax`** (`movd`/`shl`/`or`) | `al` + `xmm0` | +| `{ i64, i64, i64 }` | **`sret` pointer in `rdi`** | **`rax` + `rdx` + `rcx`** | + +The 24-byte case is the striking one: C spills to memory through a hidden pointer, and the IR form returns it in three +registers, one of which — `rcx` — the SysV ABI never uses for a return value at all. Somewhere past that, LLVM does +switch to `sret`, which is what `%vec` gets. + +So **the internal calling convention is not the C ABI and is not specified anywhere. It is whatever LLVM's backend +does with a first-class struct, discoverable only by disassembling.** That is the sharpest obstacle in this report, +and it is sharper than raylib for three reasons: the reference is an implementation rather than a document; the +failure mode is a garbage field rather than a link error; and it is not stable by contract across LLVM versions, which +is exactly the coupling `plan.org` chose text IR to avoid. It also forces the decision that determines everything else: diff --git a/spike/backend/jit_stubs.c b/spike/backend/jit_stubs.c index f53b516..6726793 100644 --- a/spike/backend/jit_stubs.c +++ b/spike/backend/jit_stubs.c @@ -45,13 +45,9 @@ value spike_jit_protect(value vp, value vlen) { /* Every Flan function's emitted signature is its parameters followed by the * transfer channel (emit.ml, [signature]), so the trampolines below all pass a * trailing pointer. Nothing in the spike transfers, so it is NULL. */ -typedef int64_t (*fn0)(void *); typedef int64_t (*fn1)(int64_t, void *); typedef int64_t (*fn2)(int64_t, int64_t, void *); -value spike_call0(value vp) { - return caml_copy_int64(((fn0)Nativeint_val(vp))(NULL)); -} value spike_call1(value vp, value a) { return caml_copy_int64(((fn1)Nativeint_val(vp))(Int64_val(a), NULL)); }