← board

A member on an array element read the element's own bytes

Found 2026-08-31 by frankS while probing the last open item of [[feature-pascal-type-helpers]], and it is not that item — the ticket's remaining work is rvalue receivers ('abc'.ToLower); an array element is an lvalue and was never on the list.

Measured, against FPC 3.2.2 in delphi mode

program pxx before FPC
a[0].Twice (record helper for AnsiString in scope) -1004535776 zz
a[0].NoSuchMember compiled, printed a pointer Illegal qualifier
ai[0].NoSuch.AndAnother (array of Integer) compiled, printed 7 — the element Illegal qualifier
s[1].NoSuch compiled, printed 113Ord('q') rejected
ar[0].NoSuch (record element) correctly refused rejected

The integer row is the quiet one and therefore the worse one: the selector was dropped, so the typo had no observable effect at all.

Mechanism, and why it is the third time

ParseLValueAST's tkDot arm has two guards for exactly this hole, and both are keyed on ASTKind[node] = AN_IDENT — a plain declared variable. a[0] is an AN_INDEX node, so neither fires and the fall-through builds AN_FIELD at offset 0.

Each earlier fix's own comment explains why the sibling is fine. The array note above the first guard says: "Element access arr[i].field takes the bracket branch above, strings take Length() style intrinsics — neither reaches this check." It does reach it. That sentence is the whole bug, and it was written by someone who had just fixed the same defect one line up.

The fix, and why it is one guard and not two

AN_INDEX + recName = REC_NONE + a memberless scalar element type → ask TypeHelperOnValue first, then error. The shape has a valid reading and an invalid one, and the same fall-through was serving both:

Keyed on AN_INDEX, deliberately not on tk. The scalar guard's own note records that a tk-keyed refusal refused three working programs, because a cast-then-deref receiver (PPyVarRec(@v)^.Payload) also arrives as a scalar with REC_NONE and is correct. Those are AN_DEREF; this arm cannot see them, and arr[i]^.Field over an array of pointer-to-record is untouched (test_arr_of_ptr_elemrec_b354, run and green).

Coverage, and the half that keeps it honest

test/test_member_on_array_element.pas is the accept side — a fix that merely refused everything would pass a refusal-only test. Six lines, all matching FPC on this exact program: zz / yy / 14 / 1 / kk / zzzz, covering a static array, a non-managed element type, a record element still taking its own path, a dynamic array element, and a chained a[0].Twice.Twice. test/refuse/member_on_array_element_{string,int}.pas are the refusal side, and FPC rejects both too, so this is parity rather than a rule of our own.