A by-value struct parameter is passed as a POINTER to every C-ABI callee
ABIParamSlotIsPointer (compiler/abi.inc) answers True for tyRecord, so a
by-value struct parameter occupies one pointer-sized slot everywhere in the
C ABI. Both sides of a pxx-only program agree, so nothing in the suite fails.
Across a real C boundary the callee dereferences the caller's data.
Repro — three runs, and the third is the control
/* pxx side */ struct Pair { int a; int b; };
int take_pair(struct Pair p) { return p.a * 100 + p.b; }
/* gcc side */ struct Pair p = {3, 7}; printf("take %d\n", take_pair(p));
| both sides | result |
|---|---|
gcc + gcc (-m32 and native) |
take 307 — the oracle |
| pxx + pxx (i386) | take 307 — self-consistent, which is why this was invisible |
| gcc caller -> pxx callee | SEGFAULT, x86-64 and i386 |
Control, same link shape, scalar parameters instead of a struct:
take_ints(3,7) -> ints 307. So the link, the object and the calling
sequence are all fine; the struct is the variable.
Why no existing test sees it
test-c-abi-cross's three subjects are all pxx-compiled on both sides —
deliberately, since they were built to catch a convention change, which is
self-consistent by construction either way. test-c-abi-glibc-oracle does
cross a real boundary, but only with scalars and a variadic tail through
glibc's dprintf; no glibc entry point in it takes a struct by value.
The gap is the same one this ticket's family keeps rediscovering: a
self-consistent pair cannot judge a convention. frankA's sharper form, from
the probe that could not fail: a differential oracle only covers the
population where the two implementations can actually disagree, and a calling
convention is agreed by construction inside one implementation — so
self-consistency is not evidence about an interface. The corollary is the
gate this ticket needs: the new subject must be a MIXED LINK, and no amount of
strengthening a pxx-vs-pxx subject substitutes for it.
Scope — bigger than the one line
Changing the predicate is not the fix. The psABI wants the aggregate's own
bytes classified: SysV x86-64 splits into eightbytes with INTEGER/SSE classes
(and MEMORY past two), AAPCS64 has the HFA/HVA rule plus an 8-byte-slot copy
past the banks, AAPCS32 has its own. ABIA64CdeclArgSlot currently advances
NSAA by exactly 8 per stack argument, which is correct only while every slot is
a pointer. The callee spill (EmitParamSpillsForTarget) needs the mirror.
Returns are NOT in scope and appear to be right: RetViaHiddenDest implements
the hidden-destination convention and cee_pairsum matches gcc on four
targets.
A consequence for whoever takes this, recorded here rather than in an audit
note because it is a property of the FIX and not of today's code (frankA):
ABIA64CdeclArgSlot advancing NSAA by exactly 8 per stack argument is correct
only while every slot is a pointer. It is right today for that reason, and it
becomes wrong on the first commit that classifies aggregates properly. The
direct arm, the indirect arm and EmitParamSpillsForTarget all read that one
oracle, so the advance and the three readers move together or not at all.
Found by
frankA asked whether the ldr x9 single-word move in the aarch64 stack half
truncates a by-value aggregate spilled past the register bank, built the case,
and measured it MATCHING — because both sides were pxx. The negative result was
sound and the hypothesis was unreachable: the move is correct by construction
precisely because the slot is a pointer. Chasing why it could not fail is what
found this.
Gate — BUILT, and RED
make test-c-abi-mixed-link (deliberately NOT wired into any suite while this
ticket is open; wire it in when it closes). test/c_abi_mixed_link_pxx.c is
compiled by pxx to an object; test/c_abi_mixed_link_main.c is compiled by gcc
and links against it. Both directions, because they fail independently:
take_* is a pxx CALLEE reading what gcc laid down, relay_* is a pxx CALLER
laying down what a gcc callee reads. Shapes hit the SysV boundaries — 1
eightbyte / 2 eightbytes / MEMORY past 16 / all-SSE / mixed INTEGER+SSE /
sub-word / an aggregate arriving after the GP bank is nearly full.
Current state: 2 of 2 targets measured, FAIL x86_64, FAIL i386, both a
segfault. gcc-on-both-sides gives the expected text, so the values are gcc's
and not ours.
x86-64 and i386 only, and that is a hard limit, not a shortcut: there is no gcc cross for arm32, aarch64 or riscv32 on this box, so no mixed link is constructible for them. The glibc substitute oracle does not extend here either — it needs a glibc entry point that takes a struct by value.
What the investigation added (2026-08-31, later)
Instruction-level confirmation. objdump of the pxx object:
take_p2: mov %rdi,-0x8(%rbp) ; store the incoming register
mov -0x8(%rbp),%rax
movslq (%rax),%rax ; DEREFERENCE it
gcc puts the struct's eight bytes in rdi (0x0000000700000003 here), so the
callee dereferences data as an address. Not inference: that is the emitted code.
The duplication blocker named in ir_codegen.inc is GONE, and its warning was
stale in the more dangerous direction. That comment said cparser.inc carried a
second full SysV classification and "do not fix one of these two without the
other", citing cparser.inc:11282. The collapse was already done
(refactor-a-collapse-the-c-frontend-sysv-prologue-copy, in done/), and the
cited line had drifted onto unrelated va_arg code — a real line that was not
the thing it named. Corrected in place: there is now one SysV prologue, so
this fix has one site, not two.
i386 has a PRIOR gap that must be closed first. i386 refuses a by-value
record in any convention, not just the C one — a plain Pascal
function TakeP2(p: TP2) gives target i386: only ordinal/pointer parameters supported yet (ir_codegen.inc:1279, since 2797638e5, 2026-08-21). x86-64,
arm32, aarch64 and riscv32 all compile and run that same Pascal. So i386 needs
by-value aggregates at all before it can have them correctly.
ROOT CAUSE, and it is a DELIBERATE DESIGN, not an oversight
Settled with gdb (-g, then disassemble — objdump -d reads nothing from a
pxx executable, which carries no section headers; gdb reads the program headers).
The IR is identical for both frontends (lea sym=p then field), so the
divergence is entirely in how the symbol is FLAGGED.
cparser.inc:11107 marks every C struct parameter isRef := True, on
purpose, and says so:
"A C record param is by-value but passed via the by-ref ABI (an 8-byte pointer slot the callee derefs); the caller copies the record to a temp and passes &temp (IRLowerCallArg), giving true by-value with correct field access for records of ANY size (the inline 8-byte slot could not hold >8B)."
That is a coherent scheme. It delivers correct by-value semantics at every
size — it is simply not the ABI gcc implements. isRef then makes
ABIParamSlotHoldsValueAddr true, and IR_LEA lowers to mov (load the
pointer) instead of lea (address of the slot).
Pascal's rule is different and size-dependent (pasparser_proc.inc:2284,
"Records larger than a qword are passed by reference"):
| <= 8 bytes | > 8 bytes | |
|---|---|---|
| Pascal | by VALUE in a register (isRef False -> lea) |
by reference (mov) |
| C | by reference, ALWAYS | by reference |
| SysV wants | eightbyte-classified in registers, up to 16 | MEMORY: bytes on the stack |
Measured, x86-64, same 8-byte {int a, b;}:
Pascal TakeP2: mov %rdi,-0x8(%rbp) ; lea -0x8(%rbp),%rax ; movslq (%rax)
C take_p2: mov %rdi,-0x8(%rbp) ; mov -0x8(%rbp),%rax ; movslq (%rax)
One instruction apart. Pascal's arm is SysV-correct here by construction; the C
arm dereferences the data gcc put in rdi. Pascal's TakeP4/TakeP6/TakeD2
all use mov, so Pascal is equally non-psABI above 8 bytes — it just happens to
agree below it.
So this is not "restore a broken path", it is "replace a working convention with the psABI one" on the C side, and extend it above 8 bytes on both. That is a larger and more deliberate change than the original filing implied, and it is why the estimate belongs in the ticket rather than in someone's head.
THE OBVIOUS FIRST ATTEMPT, TRIED AND MEASURED — it does not work
Do not repeat it. cparser.inc:11107 changed from True to
RecSize(precid[i]) > 8, so C's rule matches Pascal's at the 8-byte boundary.
Self-host converged; binary c93061a625bd. Then, on x86-64:
-
The CALLEE half becomes correct.
take_p2emitslea -0x8(%rbp),%raxinstead ofmov, and an 8-byte struct handed over by a real gcc caller reads back correctly:take 307, where the same link segfaulted before. -
The CALLER half breaks on every target. A pxx caller still copies to a temp and passes
&temp, so a pxx callee now reads the POINTER's bits as the struct's fields. Measured on a pxx-only C program that was correct before:before with the probe gcc x86-64 37 91-587090340 9137 91arm32 37 91-981461088 9137 91aarch64 37 91-272680545 9137 91riscv32 37 91-623531480 9137 91i386 37 91compile error 37 91A live address rendered as a decimal number — the same signature as [[bug-a-a-by-value-wide-record-on-xtensa-renders-a-live-address]]. Reverted with
git checkout --; all five targets confirmed back at37 91.
And it corrects the "safe split" advice this ticket used to give. That advice
said a <= 8-byte record occupies ONE slot under both schemes, so switching it
cannot shift a later argument. The claim is TRUE and it is NOT the safety
property — nothing shifted above, and the values were still garbage. The binding
constraint is that the caller and the callee must agree about what the slot
CONTAINS, and the caller's half lives in shared IR lowering
(IRLowerCallArg, ir.inc:3250), not in the per-parameter flag. So the pair
moves together or not at all, and size is not a seam you can cut along.
i386 nuance, correcting an earlier note in this ticket. i386 refuses by-value
records in the Pascal convention (ir_codegen.inc:1279), but C structs compile
and run correctly on i386 today — precisely BECAUSE the C frontend routes them
through the by-ref ABI. The deliberate design is what makes i386 work at all, so
"fix i386's prior gap first" is a real prerequisite and not an aside: on i386
this change cannot even be attempted until by-value records exist there.
Why this was not started here: partial aggregate classification is worse than none. The precedent is in this repo — a two-of-three xtensa state "turned the data loss into active corruption", because a caller pushing two words and a spill consuming one shifts every later parameter rather than failing. The same applies per-target here, so it lands whole or not at all.
THE ROOT CAUSE IS A CONFLATION, AND IT IS NOT THE isRef FLAG (2026-09-01, frankC)
Verified by reading BOTH halves, not inferred from one:
| site | what it assumes | |
|---|---|---|
| caller | ir_codegen.inc:5769-5772 — argIsSse/argIsStack/argRegIdx/argNodeArr, all array[0..127] indexed by ARGUMENT |
one argument occupies exactly one register-or-stack slot |
| callee | EmitParamSpillsForTarget (ir_codegen.inc:1279) — for i := 0 to nparams-1, intIdx/sseIdx incremented once per param |
the same |
The concept "argument" and the concept "ABI slot" are the same variable
everywhere. SysV needs one argument to occupy 0, 1 or 2 register slots, or
ceil(size/8) stack slots for MEMORY. No assignment to isRef, and no change
to ABIParamSlotIsPointer, can express that in a structure with one index.
This is why THE OBVIOUS FIRST ATTEMPT above failed the way it did, and the failure is now explained rather than merely recorded: flipping the flag moved the CALLEE onto the value convention while the caller kept passing one slot containing an address, because passing two slots is not something the caller's arrays can represent. The garbage values in that table are an address read as field bytes — exactly what a one-slot caller and a two-slot callee produce.
It also makes frankA's NSAA note precise. ABIA64CdeclArgSlot advancing
NSAA by exactly 8 per stack argument is not a separate hazard; it is the SAME
conflation on aarch64, in the oracle rather than in the backend arrays.
So the order of work is the reverse of what this ticket implied. Not "implement SysV classification, then fix the fallout" but:
- Split argument from slot — the caller's arrays become slot-indexed with an argument->slots mapping; the callee spill walks slots. Inert by construction while every argument still maps to exactly one slot, so it lands GREEN on its own and is verifiable by the existing suite showing no change. This is the whole risk of the ticket, and it is separable.
- Then classification is a table lookup in
abi.inc, read by both halves, which is the small half.
Step 1 is a refactor that DELETES a case rather than adding one, which is the
shape root-cause-over-microfix.md predicts. It is also the only part that can
be landed and proven without the mixed-link gate going green, so it is the
right first commit and it is NOT a partial aggregate classification -- nothing
observable changes until step 2.
STEP 1 IS DONE for x86-64 (47c95af42). Both cdecl arms -- direct
(IR_CALL) and indirect (IR_CALL_IND) -- classify into slot-indexed arrays
and emit by iterating SLOTS, with slotArg[k] naming the argument feeding slot
k. slotArg is the identity, so nSlots = nArgs and no emitted byte moved:
15 images byte-identical across x86_64/i386/aarch64/arm32
(tools/argslot_inertness.sh). argIsStack[]/argRegIdx[] deleted after
grep-verifying no readers remained. Giving an argument more than one slot is
now a change in the classification loop and nowhere else.
What step 2 still needs, in the order the code will demand it:
abi.incgains the SysV eightbyte classifier (INTEGER/SSE up to 16 bytes, MEMORY beyond) and both arms read it in the classification loop.- The caller must evaluate an argument ONCE and split the result. The
emit loops evaluate once per SLOT, and that is only equivalent to once per
argument while
slotArgis injective. Flagged in place at both loops. - The CALLEE half,
EmitParamSpillsForTarget, has the same conflation (for i := 0 to nparams-1,intIdx/sseIdxadvanced once each) and needs the same split before it can consume multi-slot arguments. ABIA64CdeclArgSlot's fixed 8-byte NSAA advance is this conflation on aarch64 and moves with its three readers.
Not to be re-derived from the isRef flag, which is a symptom.
A PREREQUISITE THIS TICKET DID NOT NAME (2026-09-01, frankC)
blocked-by wired above, and it is not paperwork: step 2 cannot be correct
until bug-a-a-record-parameters-type-is-not-resolved-when-its-slot-is-sized
is fixed.
AllocParam sizes a by-value record parameter's frame slot through
ParamValueSize, which reads Syms[idx].RecName -- and that is REC_NONE for
41 of the 52 record parameters in compiler.pas, so RecSize returns its
8-byte fallback. That ticket correctly records this as NOT a miscompile today:
every answer the function could give later is <= the 8 it reserves, so the slot
is over-allocated and never under-read.
Step 2 inverts that argument. A register-classified aggregate needs a slot
of RecSize -- up to 16 bytes -- and the callee spill writes the second
eightbyte at Syms[idx].Offset + 8. With the 8-byte fallback that write lands
outside the parameter's slot, in the neighbouring one. Over-allocation becomes
under-allocation the moment the convention changes, silently, and it would be
one of those the fixedpoint cannot see: compiler.pas is Pascal, not C.
This is the same shape as frankA's NSAA note recorded above -- a quantity that is right today BECAUSE of the current convention and becomes wrong on the first commit that changes it. Two now, in one ticket, from two people, found the same way: by asking what a present-tense fact depends on.
How far that prerequisite actually reaches — READ, NOT EXECUTED
The 41-of-52 figure is from compiler.pas, which is Pascal. On the C path
the id looks resolved: precid[nparams] := CTypeBaseRec is set at parse time
(cparser.inc:10699, commented "record id for a tyRecord param") and
LastTypeRecId := precid[i] sits on the line immediately before the
AllocParam call (cparser.inc:11161). ParamValueSize reads
Syms[idx].RecName, which AllocParam fills from LastTypeRecId, so for a C
record parameter it should be the real id rather than REC_NONE.
If that holds, the prerequisite blocks the PASCAL half of this change and not
the C half — and this ticket's gate (test-c-abi-mixed-link) is C-only. That
would make step 2 for the C ABI reachable without fixing the sizing bug first,
while leaving blocked-by correct for the ticket as a whole.
It is a conclusion from reading two lines, not from running anything. No
instrumented build was made and no slot size was observed; the two citations are
what it rests on. Whoever takes step 2 should measure it before relying on it —
the cheap way is to make one C record parameter non-isRef and check the frame
slot is RecSize and not 8. Recorded at this strength on purpose rather than
promoted to a finding.
THE PREREQUISITE WAS MEASURED, AND IT REACHES THE C HALF BY A DIFFERENT ROUTE (2026-09-01, frankC)
The section above recorded, at deliberately low strength, a conclusion from
reading two lines: that precid is resolved on the C path, so the REC_NONE
sizing bug blocks the Pascal half and not this ticket's C-only gate. It said
"whoever takes step 2 should measure it before relying on it". Measured now,
with a PXXDBG=a.cparamslot probe at the AllocParam call
(cparser.inc, kept — it costs nothing when the topic is off):
| C struct | RecSize | precid | RecName | slot |
|---|---|---|---|---|
{int a,b} |
8 | 26 | 26 | 8 |
{int a,b,c} |
12 | 27 | 27 | 8 |
{long a,b} |
16 | 28 | 28 | 8 |
{double x,y} |
16 | 29 | 29 | 8 |
{char buf[40]} |
40 | 30 | 30 | 8 |
{int a} |
4 | 31 | 31 | 8 |
The reading was RIGHT and the conclusion drawn from it was WRONG.
precid = RecName on every row and RecSize returns gcc's true size, so the
id really is resolved on the C path and the 41-of-52 REC_NONE problem really
is Pascal-only. But the prerequisite still binds the C half, through a
mechanism neither the blocking ticket nor this one had named:
ParamValueSize (symtab.inc) stores a record inline only when
RecSize(...) <= 8. Above that it falls through to ABIParamSlotIsPointer,
which answers True for tyRecord, and the slot is TARGET_PTR_SIZE. So
flipping isRef to False leaves a 12- or 16-byte by-value record with an
8-byte frame slot, and the callee spill's second eightbyte at
Syms[idx].Offset + 8 lands in the neighbouring parameter exactly as feared.
The <= 8 cap is not a bug today: it is the size at which the current
convention switches to by-reference, so the two agree by construction. It
becomes an under-allocation on the commit that changes the convention.
Same shape as the other two hazards in this ticket, and now there are three:
a quantity that is correct today BECAUSE of the convention being replaced.
The NSAA advance (frankA), the REC_NONE fallback (frankC), and now the
<= 8 inline cap. All three were found by asking what a present-tense fact
depends on, and none of them is reachable by testing the current compiler.
Correction to the plan, therefore: step 2's first move is not the
classification loop. It is ParamValueSize learning to size a
register-classified aggregate at ceil(RecSize/8)*8, because every later piece
writes into that slot. It is also inert while nothing sets isRef := False,
so it lands and is provable on its own like step 1 was.
A note on how this was found, because it was nearly missed. The probe was
written to answer the REC_NONE question, and on that question the answer is
"resolved, no problem". The slot=8 column was printed only because it was one
more field to print. The question the ticket asked was answered YES and the
prerequisite held anyway -- the id was never the binding constraint, and
verifying the named suspect would have cleared the case. This is the
the-name-is-not-the-thing pattern in a prerequisite rather than an
identifier: the blocker was real, and it was filed under the wrong reason.
STEP 2 IS DONE FOR x86-64 (2026-09-01, frankC, 747d3479f)
make test-c-abi-mixed-link PASSES for x86_64: all 13 rows, both
directions, every SysV boundary the subject was built around -- 1 eightbyte,
2 eightbytes, INTEGER+SSE, all-SSE, MEMORY past 16 bytes, and a struct arriving
after five integers so the bank ACCOUNTING is exercised and not just the
classifier. It had segfaulted since the day it was written.
i386 is still RED and untouched. It needs its own arm, and the note above about by-value records not existing there yet still stands. Landing x86-64 alone is consistent with "whole per target, or not at all" -- the gate reports per target and nothing about i386 changed.
What went in, in the order the code demanded:
ParamValueSizelost its<= 8cap (f64044118) -- the convention rule wearing a size rule's clothes, described in the section above.ProcParamRecIdpopulated from the C frontend (dbe2fcf72). C was the only frontend leaving it empty. It is the carrier the classifier needs, andSyms[Params[i].SymIdx].RecNamecannot substitute: a PROTOTYPE allocates no param syms, which is exactly therelay_*direction.ABISysVArgPlace(f39e158dd), validated againstgcc -Sfor all seven shapes -- bank AND index, not just classification.- The callee spill reads it (
6e622be95), which introduced a regression fixed in747d3479f; seeregression-test-core-cva-arg-pointer-pointee-b201indone/. - The caller emits eightbytes, and
ABICRecordParamByValuedecides (747d3479f).
The three hazards this ticket recorded all came true, and a fourth appeared.
ParamValueSize's cap bound the C half after all. The caller's evaluate-once
warning was real: the fix evaluates an aggregate at its HIGHEST slot and pushes
high->low, because per-slot evaluation would run a side effect in the argument
twice. The NSAA advance is still ahead, on aarch64. The fourth was not a
present-tense quantity but a stale READER -- intIdx kept its name while the
oracle took over its meaning.
Still open for this ticket:
- i386, the remaining gate row.
ABIA64CdeclArgSlot's fixed 8-byte NSAA advance and its three readers, for aarch64 -- unchanged, and now the only target where an aggregate argument is still a pointer by construction rather than by classification.The INDIRECT cdecl armCLOSED in4205cb3e6. Two function-pointer rows were added to the subject and confirmed RED before the fix; the gate now passes 15 rows for x86_64. It needed a second piece: a$cfnptrsignature is registered byRegisterProcand is CALLER-ONLY -- no body, no param sym, and no declaration for the prototype fixup to correct -- so the record ids and the by-value decision are threaded at the registration itself.ABIParamSlotIsPointerstill answers True for everytyRecord. It is no longer consulted for a register-classified C parameter (ParamValueSizeanswers first), so it is stale rather than wrong -- but it is exactly the kind of predicate the next reader will believe.
And the gate itself grew a hole while nobody was looking, which is worth
recording next to the fix rather than in a commit message: the acceptance
criterion this ticket names went GREEN for x86_64 while a whole call shape was
still wrong, because the subject contained no indirect call. Every visible
instrument agreed — gate PASS, fixedpoint held, the earlier regression fixed.
The only thing between that and a false claim in this file was
normalise-dont-special-case's instruction to grep for the sibling arm, which
was followed as ritual and not as suspicion. Read a PASS as being about the
shapes the subject contains, never about the change you made.
STEP 2 IS DONE FOR i386 TOO, AND THE GATE IS ENROLLED (2026-09-01, frankC)
test-c-abi-mixed-link: PASS x86_64
test-c-abi-mixed-link: PASS i386
test-c-abi-mixed-link: 2 of 2 targets measured, 0 skipped
15 rows now, both directions, on both targets. The acceptance criterion this
ticket named is met, so it closes. The gate is in the limited and full
tiers as of the same day — the Makefile said "wire it in when that ticket
closes" and that is not paperwork: an unenrolled check asserts nothing while
reporting success.
i386 took four things, and only the first was the aggregate ABI:
- The i386 aggregate ABI itself (
bb4773a19) — cdecl has no register arguments, so every aggregate isceil(size/4)4-byte stack slots, in the callee spill, the direct caller and the indirect caller. All of it gated onProcCdecl: Pascal passes a<=8-byte record withIsRef=Falseand the internal caller still pushes an ADDRESS for it, so an ungatedtyRecord and not IsReftest would have accepted Pascal's records into the C path and broken the language. Caught by an assertion, not by review. - The address of an external routine on i386 (frankA,
91c293722) — the two function-pointer rows could not COMPILE for i386 before it, which took the target from 13 informative rows to a compile failure that said nothing about the other 13. A gate can be made less useful by adding coverage. TypeFieldAlign— see [[bug-a-an-8-byte-scalar-is-over-aligned-inside-a-struct-on-i386]]. The last three rows were not a marshalling defect at all: pxx and gcc agreed about how to pass the struct and disagreed about where the field WAS. It was reachable only after this ticket's fix, because a struct behind a pointer never has its fields read across the boundary — the defect was hidden by a second bug standing in front of it, not by a missing test.- A dead guard in the gate itself, found while enrolling it: the
ran != wantcheck could not fire, because everycontinueincrementedranfirst. It had read as coverage since the day it was written.
The blocked-by edge was routed around, not satisfied
This ticket is blocked-by: [[bug-a-a-record-parameters-type-is-not-resolved-when-its-slot-is-sized]],
which is still open and still true. AllocParam still takes a record
parameter's identity from the global LastTypeRecId, and that is still
REC_NONE for most of them.
It stopped blocking because the ABI path no longer asks it. ProcParamRecId is
a durable per-param carrier written at REGISTRATION, and the classifier reads
that instead — which it had to, for a reason unrelated to the blocker: a
PROTOTYPE allocates no param syms at all, so Syms[Params[i].SymIdx].RecName
has nothing to be right or wrong about in the relay_* direction. The parallel
carrier was not a workaround for the blocker; the blocker simply stopped being
on the path.
So do not read this closing as evidence about that ticket. The over-allocation it describes is unchanged, and it now has one fewer consumer whose breakage would have surfaced it.
What remains, and it is one thing
ABIA64CdeclArgSlot's fixed 8-byte NSAA advance and its three readers, on
aarch64 — the last place an aggregate argument is a pointer by construction
rather than by classification. It is filed separately rather than held here,
because it has no oracle: there is no gcc cross for aarch64, arm32 or
riscv32 on this box, so no mixed link is constructible for any of them. Fixing
it would mean writing the psABI from the document and verifying it against
pxx-on-pxx, which is the exact self-consistency this whole ticket exists to
show is worthless for a calling convention.
Log
- 2026-09-01 — resolved, commit 8981641d6.