q := TQ(r) segfaults; q.x := TQ(r).x does not
type TR = record a, b: Integer; end;
TQ = record x: Int64; end;
var r: TR; q: TQ;
begin
r.a := 1; r.b := 2;
WriteLn(TQ(r).x); { 8589934593 — correct }
q := TQ(r); { SEGFAULT }
end.
A hard cast between two same-size records is an in-place reinterpret; FPC prints
8589934593 for both. PXX got the field read right and crashed on the
assignment — the simpler of the two spellings.
The double case
The record-cast arm builds an AN_PTR_CAST tagged tyPointer, then, only if
a . follows, rebuilds it as AN_DEREF(AN_ADDR(operand)) tagged tyRecord
so field offsets resolve against the cast's type. That rebuild is the value
reinterpret. Without a trailing accessor it never ran, so TQ(r) stayed a
POINTER — and q := <pointer> into a record variable is a record COPY from the
address r's first eight bytes happened to spell. On this test that address is
$0000000200000001. It is not a wild value; it is r's own contents read as an
address, which is why it is reliably a segfault rather than an occasional one.
devdocs/dev/normalise-dont-special-case.md, seventh time this session: one
construct reachable through two shapes, the transform attached to the shape with
the accessor, and the bare shape left on the path that was wrong. The fix is the
condition, not a new branch — the rebuild now also fires when the operand's
static type is a record:
if (CurTok.Kind = tkDot) or
((CurTok.Kind <> tkCaret) and
(IntToTypeKind(ASTTk[ASTLeft[node]]) = tyRecord)) then
^ is excluded deliberately: PR(pp)^ must keep dereferencing the pointer
VALUE. That is the one shape where the pointer reading is the right one, and it
is pinned by a row in the test.
Verification
test/test_whole_record_hard_cast.pas, byte-identical to
fpc 3.2.2 -Mobjfpc -O1:
assign 8589934593 the crashing shape
field 8589934593 the shape that worked, kept as the control
lvalue 7 0 TQ(r).x := 7 writes through the cast
ptr 7 PR(pp)^ still derefs the pointer value
p->i 67305985 packed 4xByte record -> Integer record
i->p 4 3 2 1 and back, so the byte order is pinned both ways
b->q 578437695752307201 8-byte array record -> Int64 record
byref 16909060 a cast as a const parameter
call 1 a cast of a FUNCTION RESULT (no addressable operand)
The last two rows exist because the fix takes the operand's ADDRESS: a temporary and a by-reference argument are the two operands that might not have one.
Found by the type-conversion/cast differential family.
Gate: make compiler/pascal26 (fixedpoint, converged after 1 round) +
tools/gate.sh quick GREEN.