ParamSize and AllocParam disagree about a 5-8 byte by-value record
Two functions in symtab.inc answer "how big is this parameter's slot" and give
different answers for the same parameter on a 32-bit target. AllocParam is the
one that is right; ParamSize is the one every backend reads.
{ AllocParam — allocates the slot }
if (tk = tyRecord) and not isRef and not isArray and (RecSize(LastTypeRecId) <= 8) then
begin
sz := RecSize(LastTypeRecId); { 8 for an 8-byte record }
if sz < TARGET_PTR_SIZE then sz := TARGET_PTR_SIZE;
end
else if ABIParamSlotIsPointer(tk, isRef, isArray) then
sz := TARGET_PTR_SIZE
...
{ ParamSize — reports the slot's size }
function ParamSize(idx: Integer): Integer;
begin
if ABIParamSlotIsPointer(Syms[idx].TypeKind, Syms[idx].IsRef, Syms[idx].IsArray) then
Result := TARGET_PTR_SIZE { 4 on a 32-bit target }
else
Result := TypeSize(Syms[idx].TypeKind);
end;
ABIParamSlotIsPointer holds tyRecord unconditionally, so ParamSize
returns TARGET_PTR_SIZE for every record — while AllocParam has already
reserved the record's real width for the <=8-byte by-value case. On 64-bit they
agree by coincidence (TARGET_PTR_SIZE = 8 covers any RecSize <= 8); on
arm32 / riscv32 / xtensa / i386 they do not.
Why this is worth a ticket and not just a note
It is a trap that costs a build cycle and produces a plausible false comment.
Measured, on 2026-08-30, while landing
bug-a-a-by-value-wide-record-on-xtensa-renders-a-live-address: the natural way
to widen xtensa's callee param spill is
((Syms[idx].TypeKind = tyRecord) and (sz > 4) and (sz <= 8))
because sz := ParamSize(idx) is already in scope three lines above and reads
like the width. That guard compiles, self-hosts, and can never be true. The
symptom is a repro that comes back byte-identical, which reads as "my change did
nothing" and sends you back to re-read arms that were already correct. It also
invites a comment asserting that sz asks the real width — one was written, and
it was wrong.
arm32 and riscv32 both dodge it, in this same procedure
(EmitParamSpillsForTarget, ir_codegen.inc), by not using sz for records:
they spell it RecSize(Syms[idx].RecName) in the spill guard and
RecSize(ProcParamRecId[procIdx * MAX_PROC_PARAMS + i]) in the word-count
pre-pass. xtensa now matches them. So all three live call sites are correct
today — this ticket is about the next one, and about a helper whose name
promises an answer it does not give.
No known miscompile today
Checked, so the priority is honest rather than defensive: the else branches
that consume sz only distinguish 1 / 2 / other, and a record reaching them
gets a full-word store, which is right for a <=4-byte record and is the only
case that still reaches there. i386 refuses record parameters outright
(target i386: only ordinal/pointer parameters supported yet), so it cannot be
silently wrong. cparser.inc and eparser.inc also call ParamSize; those
were not audited and are the first thing to check if this is picked up.
Options
- Make
ParamSizeaskRecSizefor the by-value <=8-byte record case, so it agrees withAllocParam. Correct in principle and makes the natural guard work — butParamSizetakes only a symbol index,Syms[idx].RecNameis available, and any consumer currently relying on getting 4 would change behaviour. Needs the C-frontend call sites audited first. - Rename it to say what it answers (
ParamSlotWordSize, or fold it into theabi.incoracle besideABIParamSlotIsPointerwith a comment naming the record exception). Cheap, no behaviour change, kills the trap. - Leave it and document the exception at both definitions.
Recommendation: 2, and only then 1 if an audit shows no consumer depends on the pointer-sized answer. The defect here is a misleading name on a shared helper, not a wrong number reaching the emitter.
Provenance
Found by frankS under a bounded grant for EmitParamSpillsForTarget's xtensa
arm. Filed rather than fixed: symtab.inc and abi.inc are Track A's and were
outside that grant. Sibling of bug-a-param-pointer-rule-divergence, which is
the same two functions disagreeing about tyVariant — that fix is cited in
abi.inc as the small-scale precedent for the whole oracle, so this is the
second instance of one pattern.
Resolved 2026-09-01 (frankA, Track A)
Option 2, and then the part option 2 alone would have left open.
ParamSize -> ParamSlotWordSize at all 15 call sites (cparser.inc ×3,
eparser.inc ×1, ir_codegen.inc ×10, pasparser_proc.inc ×1). And
ParamValueSize(idx) beside it, which is the answer the trap was reaching for:
how many bytes AllocParam actually reserved.
A name a reader has to think about is worth little if the answer they wanted has no name at all. That is why this is not just the rename the ticket asked for.
AllocParam now CALLS ParamValueSize rather than restating the rule. The
two functions gave different answers because the rule was written twice; one
copy is the only fix that stops it happening again. The extraction is exact —
TypeKind, IsArray, IsRef and RecName := LastTypeRecId are all written
before the size block, so the symbol-based form reads the same inputs the
inline form did.
The disagreement, measured
Scratch WriteLn in AllocParam, four by-value record params (4/6/8/16 bytes):
arm32 (TARGET_PTR_SIZE=4) slotword=4 valuesize=8 <- they differ
x86-64 (TARGET_PTR_SIZE=8) slotword=8 valuesize=8 <- they agree
and a const param on arm32 comes through slotword=4 valuesize=4, so the new
function distinguishes by-ref from by-value rather than answering the record's
width unconditionally. Without this the split would have been cosmetic; with it
the trap guard (TypeKind = tyRecord) and (sz > 4) and (sz <= 8) becomes
expressible — against ParamValueSize, and still impossible against
ParamSlotWordSize, whose name now says so.
Control: no behaviour change
The probe (4-, 6-, 8- and 16-byte by-value record parameters, all four called
and summed) compiled with the compiler built from this tree with the change
stashed (07291395282b) and with it (994bcc639fa0):
x86_64 IDENTICAL arm32 IDENTICAL
riscv32 IDENTICAL aarch64 IDENTICAL
byte-for-byte, and the program prints 3 7 5 30 natively and under qemu on all
three cross targets. i386 refuses record parameters outright, as the ticket
said.
Option 1 was NOT taken, and the audit it asked for is the reason
The ticket ranks option 1 (make the reported size ask RecSize) as a
follow-up "only then ... if an audit shows no consumer depends on the
pointer-sized answer". The 15 sites are audited by the rename itself — every
one of them now says which question it asks — and option 1 is no longer
attractive, because the measurement below shows the input to RecSize is the
part that is wrong.
What the measurement found underneath
AllocParam sizes the slot from RecSize(LastTypeRecId), and LastTypeRecId
is REC_NONE for 41 of the 52 record parameters in compiler.pas, where
RecSize answers its 8-byte fallback. So RecSize(..) <= 8 — the test that
chooses between an inline record slot and a pointer slot — is a CONSTANT TRUE
for those 41, and the branch's comment describes a decision it is not making.
The other 11 carry real ids (sizes 16, 40, 56), so the population is not
degenerate.
Not a miscompile: every answer ParamValueSize can give once RecName is
resolved is <= max(8, TARGET_PTR_SIZE), so the slot is over-allocated by up
to four bytes on a 32-bit target and never under-read. Filed as
[[bug-a-a-record-parameters-type-is-not-resolved-when-its-slot-is-sized]]
rather than fixed here — it changes FRAME LAYOUT, which is the widest blast
radius in the compiler and does not belong in a commit that renames a helper.
So the ticket's "AllocParam is the one that is right" holds, but by the clamp rather than by the test, and that is worth knowing before anyone implements option 1 by copying its condition.
Not changed, deliberately
EmitParamSpillsForTarget's arm32 / riscv32 / xtensa arms spell
RecSize(Syms[idx].RecName) longhand and could now say ParamValueSize(idx).
They are correct today, the equivalence over the (>4, <=8) window is subtle
rather than obvious, and rewriting correct backend code for uniformity in the
same commit as a 15-site rename would have made the byte-identity control above
meaningless. Left as a note, not done.
Gate
make compiler/pascal26: converged, 994bcc639fa0. tools/gate.sh quick:
GREEN, FPC seed canary PASS (run with compiler/ dirty).
Log
- 2026-09-01 — resolved, commit 65e43b519.