The question
tyRecord describes the STORAGE of three unrelated things: a real record, an
interface (a 16-byte {IMT, instance} fat pointer) and a method pointer (a
16-byte {Code, Data} pair). So every rule that refuses something because the
destination is tyRecord must first ask "is this actually a record?" — and that
question has two conditions, always the same two.
| site | asks | covers |
|---|---|---|
symtab.inc:8130-8141 (MatchArgNilOk's neighbour) |
may nil bind this parameter? |
MethodPtrRecId and UClsIsInterface |
ir.inc:87 AssignSideKind, AN_IDENT arm |
may this destination be kind-checked? | procvar via SymProcSig, and UClsIsInterface |
ir.inc:87 AssignSideKind, AN_INDEX/AN_FIELD/AN_DEREF arms |
same | interface only — the drift |
The third row is the defect, filed and fixed as the method-pointer half of
bug-p-methodptr-nil-assign: the three arms added by fa8f2424d inherited one
of the two conditions, so c.OnHit := nil and arr[1] := nil were refused with
cannot assign Pointer to record while ev := nil kept working.
That fix is a one-line bail and it is the right fix. This ticket is the observation underneath it: the pair had no name, so a new call site had to re-derive it, and re-derived half.
Shape
One predicate — RecIsFatPointer(rec: Integer): Boolean, MethodPtrRecId or
UClsIsInterface, both with the REC_NONE and MAX_UCLASS guards the existing
copies already carry — called from all three rows. Note the AN_IDENT row asks
its half a different way (SymProcSig >= 0, which bails on any procvar,
not just a method pointer); check before collapsing it whether that breadth is
load-bearing there or incidental, because a plain procvar is spelled tyPointer
and the record rule cannot fire on it.
Do not treat "they agree today" as evidence. Row three agreed with row one
about interfaces and disagreed about method pointers, and the disagreement was a
compile error on ordinary code (OnClick := nil is how you detach a handler).
Build the control the way refactor-a-two-predicates-answer-what-a-caret-yields
did: a probe per condition per site, run against a binary with the predicate and
one without, so the takeover is positive rather than an absence of complaints.
Gate
Track A: make compiler/pascal26 (fixedpoint) + tools/gate.sh quick, pinned
by test/test_methodptr_nil_assign.pas and the assign fail/ok pair
(test_assign_lvalue_shapes_fail / _ok, whose 12-row count is itself the
assertion).
Rejected — it had already landed when this was written
2026-08-30, frankA, the same hour. d5fd2a6ca (frankS) is exactly the shape
this ticket asked for:
symtab.inc:8116—RecIsReferenceShaped(rec),MethodPtrRecIdorUClsIsInterface, one name for the pair.symtab.inc:8170—ProcParamIsNilabledelegates to it.ir.inc:106andir.inc:151— bothAssignSideKindarms call it, so the drift this ticket was about is gone rather than documented.
Measured rather than read off the diff: rebuilt at HEAD (fixedpoint, 2 rounds)
and ran test/test_methodptr_nil_assign.pas — compiles, and all three cleared
shapes report assigned=FALSE after being armed and called.
Why it was filed anyway, which is the part worth keeping
I read symtab.inc:8130-8141 at the sha in my checkout and wrote the ticket
from it. The predicate had existed for forty minutes. git fetch/pull before
filing — not merely before writing — is the cheap fix, and it is the same
error the coordinator made tonight from the other side: an absence read as a
fact about the repo when it was a fact about one working tree.
A ticket asserts a present-tense claim about the codebase. That makes filing one a measurement, subject to the same staleness rule as any other, and the board is where a stale claim is most expensive: nobody re-derives it, they act on it. This one would have sent its resolver to write a predicate that was already three call sites deep.