a[i]^ derefs 8 bytes for a 4-byte element (plain spelling)
Measured, both sides built
| binary | commit | ds_plain_ptrelem |
|---|---|---|
0f82c481c26b |
381fb9e37^ |
ok (10 13) |
ffb4dadcf1f8 |
381fb9e37+ |
WRONG (47244640266 18490349605355533) |
Not read off notes: git checkout 381fb9e37^, make compiler/pascal26, run,
then back to master and rebuild. 0f82c481c26b is independently the binary
frankA reported as their pre-fix build, which is the second source that the
right thing was built.
The value decodes, so this is not a mystery
47244640266 = 0x0000000B_0000000A = nums[1] shl 32 or nums[0] = 11 and 10
packed into one 64-bit quantity. a[0]^ performs an 8-byte read where the
element is a 4-byte Integer. The pointee WIDTH is coming out 64-bit for a
PInteger element — depth's near neighbour, and the "a symbol parks it one
level up" hazard from 381fb9e37's own design note, landing on the read side.
Repro
test/derefshape/ds_plain_ptrelem.pas, or:
program r;
type TA = array of PInteger; TP = ^TA;
var a: TA; nums: array[0..3] of Integer; p: TP; i: Integer;
begin
for i := 0 to 3 do nums[i] := 10 + i;
SetLength(a, 4);
p := @a;
for i := 0 to 3 do p^[i] := @nums[i];
WriteLn(a[0]^, ' ', a[3]^); { expect: 10 13 }
end.
Why it was not caught
381fb9e37's own five-face harness has no row whose element is a POINTER, and
neither did the first 15 rows of test/derefshape — every element kind was a
scalar (Double, AnsiString). A wrong pointee width or a dropped level of
indirection is invisible when the element is a scalar, because the value read
back is the right number either way. The ptrelem kind was added in
787d353f7 specifically for that class, after 15ec54d7a had regressed the
same way (dropped DEPTH, right pointee) and been fixed in bfb7b4c59.
So this is the second regression of that exact class, and the first one found by an instrument rather than by a user.
Do not "fix" it by relaxing the control
The tempting repair is to drop ds_plain_ptrelem from the control set so the
matrix reports its other 19 rows again. That converts a guard that fired into a
guard that cannot, and the row is a control precisely because the plain spelling
is the reference every other row is compared against. Fix the width; leave the
row.
Log
- 2026-09-01 — resolved, commit 23252c4f0.
Fixed by frankA in caa39393f — verified independently 2026-09-01 (frankB)
Rebuilt from a clean pull rather than accepting the reported number, for the
same reason the regression was found that way: 67520ef041ac, and
ds_plain_ptrelem is ok. All five ds_plain_* control rows pass, so the
matrix reports verdicts again — 30 rows, 21 passing, 9 failing, and all 9 are
rows added after the fix (the nested and md2 axes).
The root cause was better than my symptom, and the difference is worth
keeping. I decoded the value and said "an 8-byte read of a 4-byte element",
which is the defect stated in bytes. frankA found that the array of PInteger
symbol was carrying SymPtrElemDynDepth = 1 — claiming its elements point at a
DYNAMIC ARRAY — picked up from TP = ^TA declared earlier in the same type
section through LastTypePointerElemArrAi, a parse-time global.
LoadPointeeFromArrType restored six pointee columns and there was no seventh
for that one, so the channel kept whatever the previous unrelated declaration
had left in it. Fixed by adding the seventh column (ArrTypePtrElemArrAi).
Why it survived: for array of ^TDyn the stale value is the RIGHT one. The
channel happened to hold exactly the row that shape needs, so every existing
reader had been getting a correct answer out of a global it had no business
reading, and had been since the slot was written. It was order-dependent the
whole time — moving one type declaration changes the answer.
So this is not "the fix broke the plain row". It is "the fix was the first reader to ask on a shape where the stale value was wrong."
The family this belongs to. Tonight's recurring shape is an instrument that answers correctly about something else. This is its sharper cousin: a slot that answers correctly for the wrong reason. No test whose shape the stale value happened to suit could ever have caught it, however many rows it had — which is the same reason an all-scalar element matrix cannot see an indirection bug. The population has to vary the axis the lie lives on.
And the diagnostic had the same blind spot as the tests. PXXDBG=a.symptr,
the dump written to diagnose pointer-to-array symbols, did not print
SymPtrElemDynDepth — the one slot that says the pointee is dynamic — so a
poisoned symbol printed clean. frankA added it before fixing anything, which is
why the mechanism could then be read directly rather than inferred.