The shared cdecl spill arm cannot yet do the job it would be given
Found by frankA and frankC independently, from opposite directions, on 2026-08-30. Split out of [[bug-c-a-c-function-s-calling-convention-depends-on-the-target]], which is blocked on this — the ordering matters and it is the opposite of what both of us first assumed.
Why it is a prerequisite and not a follow-on
The C-frontend fix deletes cparser.inc's positional prologue arms so a
ProcCdecl proc's prologue comes from EmitParamSpillsForTarget instead. That
is only correct if the shared arm can already do everything the local arms did.
It cannot. Routing into an arm with these gaps breaks pure C regardless of
what the call sites do — measured with the call-site guards deleted as well,
so this is not the guards.
Measured, paired on identical source
Baseline 8a42f93ffe74 → both halves applied (cparser.inc arms deleted, the
seven not CProgramMode guards removed) 7d91463cbbfc. Subjects:
test/test_c_abi_pascal_caller.pas and test/c_abi_pure_c_control.c.
| target | Pascal→C bridge | pure C control |
|---|---|---|
| x86-64 | PASS → PASS | PASS → PASS |
| aarch64 | 3 fail → 1 fail (flt only) |
PASS → FAIL (flt) |
| arm32 | 1 fail → PASS | flt-fail → COMPILE FAIL |
| riscv32 | PASS → PASS | flt-fail → flt-fail (untouched) |
| i386 | 5 fail → order fixed, floats Nan | flt-fail → COMPILE FAIL |
The bridge improves substantially — arm32 goes fully green and aarch64 drops to Single-only — which is the evidence that the C-side change is right. The pure-C column is what blocks it.
The three gaps
- aarch64: a by-value
Single. Everydoubleshape comes right;flt(afloatparameter andfloatreturn) gives0.00on both subjects. The arm's own comment says a by-value single arrives raw ins[n]and is stored without conversion — the C frontend's value model may be handing it double bits instead. Classification of Single, not order. - i386: no float classification. The order fix lands (
321→123) and every float shape staysNanon the bridge; pure C fails to compile withnear: unit builtinheap. The i386 cdecl arm looks like it never handled floats, which is invisible today because nothing routes into it. - arm32: a varargs-using translation unit will not compile.
near: overflow nfp apon a plain C program that builds fine today.cparser.inc's prologue does__va_saveregister-area setup before its target dispatch; something in that path does not survive the routing. Distinct from [[bug-a-arm32-cdecl-has-no-aapcs-stack-argument-area]], which owns the four-core-register argument-block refusal — this one is about a TU that has no wide call in it at all.
Order of work
- Fix the three gaps here. Pure C must stay clean with the local arms still in place — nothing routes into the shared arm yet, so it should.
- Then land the C-side deletion plus the seven guards ([[bug-c-a-c-function-s-calling-convention-depends-on-the-target]], frankC, claimed). Both tables should go green together.
Reproduce
/tmp/frankC-share/abi-probe/ and the two wired subjects above. The cross rows
are test-c-abi-cross (RED by design until the pair lands). Note flt is
ALREADY red on arm32/riscv32/i386 in pure C before any of this — that is
[[bug-c-a-float-parameter-and-return-are-wrong-in-pure-c-on-three-targets]] and
must not be read as caused by this work.
GAP 1 IS NOT A Single MISHANDLING — the RETURN convention has no counterpart
frankA, 2026-08-30. Measured under the scaffold (cparser routing applied locally, never committed), compiler shas quoted per run.
The complementary table that settles it
fnarrow.c isolates the two halves: D is a float parameter with an int
return; C is an int parameter with a float return. Both run on aarch64,
same source, one variable changed between the two runs.
| routing ON, guards PRESENT | routing ON, guards REMOVED | |
|---|---|---|
| D — float PARAM, int return | 0 — BAD | 10 — ok |
| C — int param, float RETURN | 10 — ok | 0 — BAD |
Exactly complementary. Removing the guards fixes the parameter and breaks the
return; keeping them does the reverse. Nothing here is a Single
classification defect.
The mechanism. The C-side change routes only the parameter spill into the
shared arm. Nothing routes the callee's return. So with the guards removed,
the caller takes the AAPCS arm — which, per its own comment at
ir_codegen_aarch64.inc:2993, "bridges a float result d0 -> x0 for the GPR
value model" — while the C callee's epilogue still returns the float in the
positional model. Caller and callee disagree about where the result lives.
The parameter half was routed; the return half was not. That is the gap, and it is one defect, not the per-target list below.
What this does to the ticket's own framing
- The "aarch64 mishandles a by-value
Single" reading is wrong. I tested it first, because the arm's comment invited it: I made the cdecl arm narrow withfcvt s0, d[n]instead of a rawstr s[n].fltstayed0.00and the change was reverted — the arm's existing comment is correct and states the caller already narrows (fmov d[hi], x9thenfcvt s[hi], d[hi]). Recorded so nobody re-runs it. - i386's "floats stay Nan" is very likely the same defect, not a separate
"no float classification": the i386 arm has the identical shape, an st0->xmm0
result bridge behind the same guard (
ir_codegen386.inc:3646). - Gap 3 is not distinct from
[[bug-a-arm32-cdecl-has-no-aapcs-stack-argument-area]] after all. The full
error, which the ticket quotes only the tail of, is
target arm32: a cdecl routine whose argument block exceeds 4 core registers is not supported yet, raised while compilinglib/crtl/src/stdarg.c. It is that ticket's refusal, reached because routing sends astdarg.chelper through the shared arm. Not "a TU with no wide call in it".
Correction to a claim I made earlier in this ticket's history
I reported the pure-C baseline as CLEAN on all five targets and frankC reported
flt failing on three. Both were right about their own control. Mine routes
the float result through a prototyped chk(const char*, double, double);
frankC's hands it straight to variadic printf. Isolated on baseline: a float
result assigned to a local and compared in C is correct on all of
x86-64/arm32/riscv32/i386, and only the direct-into-variadic form fails. That is
[[bug-c-a-float-parameter-and-return-are-wrong-in-pure-c-on-three-targets]],
which is also mis-titled — the parameter is fine and the return is fine; the
variadic argument conversion is not.
Consequence for anyone using these subjects as an oracle: on arm32/riscv32/i386 an absolute float row in the pure-C control is reading that defect, not this one. The deltas in the paired table remain valid.
Where this leaves the work
The prerequisite is bigger than "three gaps in the spill arm" and is not only a
spill-arm change: the C-defined callee's return must move to the C
convention at the same time as its parameters, or the two halves will keep
trading places. Whether that lands as a return-side counterpart to
EmitParamSpillsForTarget or as part of the C-side commit is the open design
question.
RESOLVED — one defect, two halves, landed together
All eight rows of the test-c-abi-cross pair are green, on binary
1d879495ffcd (fixedpoint, 1 round), against baseline 8ebdd42b6322 at
1d868b658:
| target | bridge before | bridge after | control before | control after |
|---|---|---|---|---|
| aarch64 | FAIL (dbl_first, two_dbl, flt) | PASS | PASS | PASS |
| arm32 | FAIL (int_first) | PASS | PASS | PASS |
| riscv32 | PASS | PASS | PASS | PASS |
| i386 | FAIL (all five) | PASS | PASS | PASS |
Bridge 3 red→green, control unmoved on all five.
The premise the whole ticket was built on was wrong
CProgramMode does not mean "this is a C program". It means "the source in
front of me is C". ParseCUnit (cparser.inc:13340) sets it exactly as
ParseCProgram (:10085) does, so it is True while compiling a C translation
unit that a Pascal program uses.
Measured, not read: a probe on the prologue gate printed cprog=1 for every
function of a C unit reached from a Pascal program. frankC confirmed it from
outside the compiler with no source change — a double-taking C function called
from other C code inside a Pascal-used unit is 1000/1000 on all five targets,
which the positional prologue can only manage if those call sites agree with it.
Two consequences, and they are the content of this ticket:
- The four positional prologue arms are load-bearing, not dead. They are the convention both sides of an intra-C call already speak. Deleting them outright — the shape the handoff patch proposed — moves only the callee, and takes the pure-C control from green to a 309-digit double (aarch64), a compile failure (arm32) and a segfault (i386). Both instruments measured that independently and agreed row for row.
ProcCdecl and (not CProgramMode)is a no-op at a callee prologue. It was the obvious gate and it was tried first: built, measured, and the table came back byte-identical to baseline. That null result is what located the flag's real meaning.
The fix
A new flag CUnitOfPascalProgram (defs.inc), saved/set/restored by
ParseCUnit, read through one named predicate CProcUsesCAbi(procIdx)
(symtab.inc) — requirement 3 of the parent ticket, and the constraint both
frankC and the coordinator asked for. Two call sites, one spelling.
- Parameters: the i386/arm32/aarch64 positional arms in
cparser.incare gated, not deleted, so a Pascal-used C unit falls through toEmitParamSpillsForTarget— the arm x86-64 has always used — while a C program keeps the positional arms. - Return:
EmitProcEpilogplaces a C function's float result in the target's float return register —fmov d0, x0(+fcvt s0, d0for a Single) on aarch64,push/fld/add espon i386. This is the exact inverse of the bridges the callers already had (ir_codegen_aarch64.inc:3074,ir_codegen386.inc:3651). arm32 and riscv32 need nothing: their bridge rows went green on the parameter half alone.
Routing the parameters without the return is what made the two halves trade places in the earlier scaffold measurements. They are one defect and had to land in one commit.
Open, and deliberately not settled here
This gate makes a C function's ABI depend on who included its unit, which
re-parameterises the parent ticket's axis rather than removing it. That fork is
[[decide-does-a-c-function-always-use-the-c-abi-or-only-when-a-pascal-program-uses-it]]
(U, p65) — filed by frankC, scoped by the coordinator as "build the gate, name
the destination". CProcUsesCAbi is that single place: if Track U answers
"always the C ABI", it changes in one function body.
Log
- 2026-08-30 — resolved, commit b4ff9adea.