a var record parameter's write-back is dropped where no implementation header repairs the row
type TBig = record a, b, c: Int64; end; { 24 bytes }
procedure Bump(var r: TBig); { body: r.a := r.a + N }
One file, one signature, six receivers, r.a set to 1 before each call:
| receiver | pxx before | fpc 3.2.2 |
|---|---|---|
| class method with a body | 301 | 301 |
| free routine | 401 | 401 |
| record method | 501 | 501 |
| interface method | 1 | 101 |
virtual; abstract |
1 | 201 |
| procedural type | 1 | 401 |
Cause
ProcParamExplicitByRef answers "was this parameter declared var/out/const
in the SOURCE, or is it by-ref only because the >8-byte record ABI forces it?"
ir.inc's by-ref argument arm asks it: False means the callee gets a private
copy (correct for an ABI-promoted by-value parameter, and the fix for
bug-byvalue-record-managed-field-aliases-caller); True means pass the
caller's storage through.
It was written by pasparser_proc.inc only (:1699, :2154, :2569).
None of the four parameter parsers in pasparser_decl.inc wrote it, and
symtab.inc:11892 zeroes it per proc — so it read False, not stale.
Why the population is exactly three, and it is structural rather than a
sample: a method is declared twice, and the implementation header goes through
ParseSubroutine, which overwrites the parameter row. Every routine with a body
was repaired. The three declarations with no implementation header are an
interface method, virtual; abstract, and a procedural type. Same mechanism as
[[bug-p-an-interface-dispatched-call-passing-a-named-dynamic-array-segfaults]],
one column over.
The row that was written as a control and became a finding
A 4-byte record was added as a row that could not fail — no ABI promotion, so
surely the column is never consulted. It failed the positive control, 1 where
11 was wanted. A var parameter carries IsRef at any size, so ir.inc
reaches the same arm and reads the same False. The flag's name describes the
case it was invented for and not the set of cases that read it.
ProcParamIsConst had to move with it
symtab.inc's ByRefArgNeedsLvalue asks ExplicitByRef and not IsConst.
ParseRecordMethodDecl and ParseProcTypeSignature wrote neither column, so
writing ExplicitByRef alone would have turned a const record parameter — which
carries by-ref for exactly the ABI reason above — into one that refuses a
non-lvalue argument. Both columns land together; the fixture carries the
const-and-by-value rows that would have caught it.
Fix
ProcParamExplicitByRef (and ProcParamIsConst at the two that lacked it) written
at all four row-writes in pasparser_decl.inc: ParseRecordMethodDecl,
ParseProcTypeSignature, the interface-method arm and the class-method arm of
ParseTypeSection.
Fixture test/test_a_var_record_parameter_writes_back_through_every_receiver_that_has_no_implementation_header.pas
(VARRECWRITEBACK OK, test-core, 11 rows): the three repaired receivers as
controls, the three broken ones, the small-record row, and the const/by-value
regression rows. Positive control measured — VARRECWRITEBACK FAILED 4 with the
fix reverted and rebuilt. Byte-identical to fpc 3.2.2.
The pairing rule, for whoever adds the fifth site
ProcParamIsConst and ProcParamExplicitByRef are correct only when written
together. pasparser_proc.inc writes them adjacent at every site:
1698/1699 2153/2154 2568/2569
pasparser_decl.inc before this fix wrote IsConst alone at two sites and
neither at the other two. A parallel-array channel whose writes are correct
only in pairs cannot say so at its declaration, so the check has to be
mechanical: grep each name and compare the counts. Three hits for one and four
for the other is the bug. (frankS's framing, and their parallel case the same
day: a bug whose entire population is "sites that write one of a pair".)
Downstream
frankA is building by-value record parameter lifecycle and needs exactly this
column to tell a genuine var/out record parameter from an ABI-promoted one.
They said the parser-side route was unavailable because getting that
discriminator wrong "finalizes the caller's live record through a var
parameter" — the column was already wrong in that direction for these three
spellings. Told, with the measurement.
Log
- 2026-09-06 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit 74e823da0.