← board

A class-method call keeps the receiver's class instead of its return type

Found while reducing rung 3's wall at generics.defaults.pas:3341 ([[feature-pascal-corpus-generics]]). Not the wall itself — a second, more serious defect the reduction walked through on the way.

The silent arm is the one that matters

The spurious rejection is the visible half. The dangerous half:

x := f.GetObjC.OnlyOnFactory(14);   { OnlyOnFactory is a member of f's class,
                                      NOT of the class GetObjC returns }

pxx compiled this and printed 70 — it resolved OnlyOnFactory against the receiver's class and called it on the returned object. FPC rejects the same program: identifier idents no member "OnlyOnFactory". A member lookup that silently lands on the wrong class, called against an object of an unrelated type, is a wrong-value/wrong-dispatch bug, not a diagnostics difference — so it is a bug- ticket rather than compat, per CLAUDE.md's escape row.

Root cause — one missing line

compiler/pasparser_lval.inc, the "a CLASS method reached through an INSTANCE" arm:

  node := mcallNode;
  tk := Procs[mpi].RetType;
  Continue;                      { <-- recName never updated }

The instance-method arm ~300 lines below does the same three lines plus recName := ProcRetRecId[mpi], and carries a comment explaining that the loop must continue so obj.M.M2 chains resolve. This sibling arm was written without it, so the receiver's recName survived into the next selector iteration.

That is this rung's recurring shape, named in the ticket's own recon notes: a facility that works on one path and is missing on its sibling. Worth checking first the next time a wall here looks novel.

Measured, one variable at a time

FPC 3.2.2 answers 42 for every row.

receiver the called method pxx before after
instance instance function 42 42
instance class function "Inst": no such member 42
instance class fn, member exists on BOTH classes 70 — wrong method, silently rejected, as fpc does
instance split via a temp var 42 42

The third row is the one that justifies the priority; the fourth is the control that a fix must not trade away.

Not fixed here, and deliberately separated

Two neighbours measured in the same sweep are different defects and are filed apart rather than folded in:

Gate

make compiler/pascal26 (fixedpoint, 8c0f0e471e61), tools/forwardlint.py clean, gate.sh quick GREEN. Test test/test_class_method_result_type.pas, expected generated from FPC, confirmed refused on the baseline first (line 38, no such member). It carries the working sibling and the split spelling as controls.

Log