← board

The ranking, as it stands

FindTypeAlias, compiler/symtab.inc:

owned := AliasOwnerCi[i] >= 0;
hop   := ScopeHopsToProc(AliasOwnerProc[i]);
if (hop >= 0) and
   ((Result < 0) or (hop < bestHop) or
    ((hop = bestHop) and (owned and (not bestOwned))) or
    ((hop = bestHop) and (owned = bestOwned) and (r > bestRank))) then

Three keys, compared in order. The first is a DISTANCE and the second is a BOOLEAN, and that asymmetry is the whole of this ticket.

Why the Boolean is sound today, and what makes it stop being sound

The comment above it says so explicitly:

AliasVisibleHere has already excluded every OTHER class's rows, so any owned row still here is owned by the scope we are in.

With at most one class's rows surviving the filter, "is it class-owned" IS "is it owned by us" and two values suffice. The moment AliasVisibleHere admits rows from more than one class the two questions come apart, and the key degrades without any diagnostic: two class-owned rows at equal hop take owned = bestOwned, fall through to r > bestRank, tie when both classes are in one unit, and a tie keeps the FIRST row. First in AliasCount order is declaration order, and a base class is necessarily declared before the class that inherits from it. So the base wins and the derived class's nested type loses.

Why this is not a bug, measured

fpc 3.2.2 refuses the only program that can reach it:

type
  TBase = class type TSel = LongInt;    end;
  TDer  = class(TBase) type TSel = AnsiString; end;
shadow.pas(9,15) Error: Duplicate identifier "TSel"

A derived class may not re-declare a base's nested type name at all, so there is no correct program in which the two rows compete. pxx answers 4 / 8 on the equivalent pair today and is right for a reason that will not survive the widening — which is exactly the kind of green that certifies nothing.

The replacement, so it is written down somewhere with a reader

owned becomes a DISTANCE: hops from MethImplOwnerCi (or ParsingClassBodyCi) up UClsParent to AliasOwnerCi[i], with a sentinel for "not in this hierarchy" — precisely what ScopeHopsToProc already is for the lexical key one position over. Then a derived class's own row is 0 hops, a base's is 1, and nearest wins the way the lexical key already makes nearest win.

The trap is recorded two paragraphs above the code and applies unchanged: UsesRankOf returns MaxInt for a row in the CURRENT unit — a sentinel, not a count — so nothing can be encoded as "larger than any rank", and the distance must be a separate key compared in order, never a bonus folded into r. That mistake was made once here already and inverted the answer.

THE CODE NOW CARRIES THIS (2026-09-07, ba2bd5b05)

frank-optimize landed the inheritance widening and rewrote the comment in the same commit: compiler/symtab.inc states the retired invariant, the Duplicate identifier measurement, the distance replacement and the sentinel trap, at the owned := site itself. That is the reader that matters and this ticket is no longer the only copy — it exists so the deferral is findable from the ranker, not so the design is recoverable. If the two ever disagree, the comment wins and this file is the stale one.

Why it is filed rather than done

The class axis is being widened right now by frank-optimize (AliasVisibleHere arm 3 walking UClsParent, so a nested type declared in a BASE class is visible inside a DERIVED class's method implementation). The immediate obligation on that change is the COMMENT at symtab.inc:408, which their diff makes false; the distance version is a separate, larger change with no failing program behind it. Doing it now would be a refactor in a file two sessions are editing, for an observable fpc rejects.

Take this when someone widens the class axis a second time, or when a real program needs an inherited nested type to lose to a nearer one.

Provenance

Found 2026-09-07 while holding both halves of a collision frank-coordinator routed: [[bug-p-routine-local-name-scoping-is-implemented-in-one-of-three-tables]] added the lexical key and wired ScopeReachesProc into FindTypeAlias; the inheritance widening arrived at the predicate that feeds it. The composition of the two turned out to be already specified BY the ranking — a routine-local type shadows a class's nested type because hop is the first key — and this asymmetry is the only thing the pair left unsettled.

Where the reader actually is — ba2bd5b05

The code comment is the primary record and this ticket is the back-link, not the other way round. frank-optimize landed the inheritance widening at ba2bd5b05 (arm 3 of AliasVisibleHere walks UClsParent; arm 4 still does not; AliasOwnsThrough untouched) and took this whole account into the comment at symtab.inc's owned := site: the retired invariant, the Duplicate identifier measurement that is why it stays sound, the distance-based replacement mirroring ScopeHopsToProc, and the UsesRankOf MaxInt sentinel trap.

They could not cite this slug from there, correctly: it was not on origin yet — I was holding the push behind a --tier limited run — and citing a slug just verified as absent is a ghost citation. So the pointer is added from this side.

If the comment and this file ever disagree, the comment wins. It sits where the decision is made and it is what a reader of FindTypeAlias will find; this file is the deferred plan.