A nil assignment to a dynamic-array field is lowered as a record zero
- Type: bug (miscompile + false reject) — Track A (
compiler/ir.inc,compiler/symtab.inc). - Found in fcl-passrc rung 7, [[feature-pascal-corpus-expansion]].
The boundary
var ra: array of TR; ra := nil; { plain VARIABLE — always worked }
TC.IA: array of Integer; IA := nil; { scalar element — always worked }
TC.RA: array of TR; RA := nil; { record element — refused, then crashed }
fpc 3.2.2 -Mobjfpc sets length 0 for all three.
The two defects, and why the order matters
1. The false reject. AssignSideKind reads the destination's kind off
ASTTk[node], and for a dyn-array field that is the ELEMENT's kind. The
AN_IDENT arm bails on Syms[si].IsArray with the comment "the kind is the
ELEMENT's"; the arm added later for fields, elements and derefs never got the
same bail. So the check described an array of TR destination as a record
and refused legal Pascal — the direction that function's own header calls the
worse one: "a false REJECT of working code is a worse defect than the false
accept being fixed here."
2. The miscompile underneath it. ASTNodeIsWholeArray opens by explaining
that an array's TypeKind is its element's and that a pass deciding from the
kind alone cannot tell arr := other from arr[i] := other — and then
answers only for AN_IDENT. A dyn-array field is a whole array and it said no,
so the store took the "any record-shaped destination := nil" arm: zero-fill
RecSize(TR) = 4 bytes at the field's address, over an 8-byte array handle.
ACCIDENTAL COVER — the part worth carrying
The type error was a refusal, so nothing had ever reached the bad lowering. Fixing the false reject is what exposed the segfault; had they been worked in the other order the crash would have looked like a regression introduced by the whole-array widening. Neither is provable alone, which is why both are in one commit.
The general shape: a false reject can be load-bearing. "We refuse this legal construct" is not a bounded cost — behind it sits an unknown amount of lowering that no instrument has ever exercised, and the refusal is what keeps it off every one of them.
Resolution 2026-09-06
AssignSideKind's AN_INDEX/AN_FIELD/AN_DEREF arm bails onNodeDynDepth(node) > 0— the shared answer, not a fresh walk: it already types fields, elements, derefs, calls,Copyand array constructors.ASTNodeIsWholeArraygained theAN_FIELDarm viaRecFieldIsArray, and moved to the end ofsymtab.incbecause that arm needsResolveNodeRecandGetASTIdentName, both defined later in the file. Its only caller is inir.inc, included after all of it; the FPC seed canary ingate.sh quickcovers the move.
The fixture
test/test_nil_assigned_to_a_dynamic_array_field.pas, byte-identical to fpc.
The int and out rows already worked and are present so the file cannot read
"arrays now work" as "records now work". The mp row must not move: a
method-pointer field is record-SHAPED and OnHit := nil has to keep the
zero-fill arm (bug-a-assigning-nil-to-a-method-pointer-segfaults) — widening
the whole-array predicate is exactly the change that could steal it, so it is
asserted here rather than left to the ticket that owns it.
Log
- 2026-09-06 — fixed and resolved; see the commit carrying this file.