The shape
type
TLeaf = class X: Integer; function Add(const aX: Integer): Integer; end;
TMid = class Leaf: TLeaf; end;
THolder= class Mid: TMid; end;
var f: function(const aX: Integer): Integer of object;
...
f := @h.Mid.Leaf.Add; { wrong number of parameters in call to TLeaf.Add }
f := @h.Mid.Self_.Leaf.Add;{ a statement cannot start with '.' }
f := @TLeaf.Create.Add; { @TLeaf.Create: unknown method }
fpc 3.2.2 compiles and runs all three.
The cause, and why it read as three bugs
The @ handler has five hand-rolled arms and every one of them is written for
exactly one dot: obj/member, obj/field, obj/method, bare implicit-Self
method, and class-type/method. Depth 2 falls into whichever arm matches the
FIRST selector, and that arm then hands the tail to
ParseClassRecordSelectors — which consumes every . it can reach, so the
method at the end becomes a call.
The error text depends on where the truncated parse stopped, not on the
defect. That is why the same cause reads as a parameter-count error, a
statement-parser error and a name-resolution error, and why the three were never
put together. bug-p-at-over-a-class-base-consumes-only-one-selector (done) is
the same family one step earlier: it fixed the address case and left the
method reference case, because an address needs the whole chain walked and a
method reference needs the chain walked one selector short.
The fix
The value half and the reference half end in different places and the walker had no way to be told so. So it is told:
AtStopDotTok(defs.inc) — the one token indexParseClassRecordSelectorsmust not walk through, or −1. An absolute token index, deliberately: a nested parse inside the chain (@o.Items[i + 1].Ev) runs the same walker over different tokens and can never collide with it, because the stop dot is at the outer level by construction. A flag or a countdown would need saving and restoring at every nested entry, which is the shape this kind of parser state gets wrong.LastDotOfDesignator(pasparser_name.inc) — pure lookahead over the same three postfix openers the walker has (.name,[…],(…)). It resolves no name, so it can be wrong about extent and never about types.- Two arms in pasparser_expr.inc: the instance-base one parses the chain as a
value and resolves the last name itself; the class-type one has a different
value parser rather than a different resolution —
TG.Createis an ordinary factor, so it recurses intoParseFactorwith the stop set instead of growing a sixth hand-rolled walk.
Both gate on the last dot not being the first, so every depth-1 designator keeps the existing arms untouched: the population this opens is exactly the designators that could not compile at all.
When the name after the stop is NOT a method it is a field or a property and the
answer is an address, so the walk resumes with no stop and AN_ADDR is taken
over the whole chain — the same result the old arm produced, reached one
selector later.
Measurement
Fixture test/test_a_method_pointer_can_be_taken_through_a_chain_of_selectors.pas,
six rows, all matching fpc 3.2.2. Positive control on pin v407: three of the six
rows are refused there, with all three of the historical messages.
Row 5 is the one that can fail quietly. THolder.Item is declared TBase and
holds a TDerived, so a reference that dropped the VMT slot would call
TBase.Speak and print base — a plausible answer, not a crash, and the exact
defect bug-a-method-pointer-virtual-captures-static-address records at depth 1.
There is deliberately no TBase(h.Item).Speak row beside it: a cast then a CALL
still dispatches virtually, so it prints derived under a broken compiler too.
Residual
tgeneric106.pp does NOT burn and is a different mechanism, filed as
[[bug-p-a-delphi-parenless-method-reference-cannot-have-a-chained-receiver]]:
Delphi mode takes a method reference with no @ at all, and
TryParseParenlessMethodRef reads exactly Ident . Ident. Its depth-1 form
works; only the chained receiver fails. It is the fifth receiver spelling of
bug-p-a-parenless-method-reference-handles-two-of-four-receiver-spellings
(done), and it is harder than this one because that arm parses SPECULATIVELY and
must consume nothing when it decides the shape is a call.