← board

A base class's nested type resolves but cannot be assigned or called

Repro

program r;
{$mode delphi}
type
  TBase = class
  public type
    TSel = function(a: Integer): Integer of object;
  end;
  TDer = class(TBase)
    FSel: TSel;                     { resolves -- the type name is found }
    function Twice(a: Integer): Integer;
  end;
function TDer.Twice(a: Integer): Integer;
begin Twice := a * 2; end;
var d: TDer; q: TDer.TSel;
begin
  d := TDer.Create;
  d.FSel := d.Twice;                { A: refused, "wrong number of parameters" }
  q := d.Twice;                     { assigns fine }
  WriteLn(q(21));                   { B: refused, "expected ')' before '('" }
end.

fpc 3.2.2 compiles and runs both. pxx refuses A, and with A removed refuses B.

What the pair tells you

spelling type resolves assign call
local in a derived METHOD BODY (var s: TSel) yes yes yes
field of the derived class (FSel: TSel) yes no
qualified TDer.TSel yes yes no
the same two against the OWNING class (TC.TSel) yes yes yes

The type name is never the problem — every row resolves it. What does not travel along the inheritance path is the alias's procedural signature (AliasProcSig), which is what tells the parser that d.Twice in that position is a method REFERENCE rather than a call, and that q(21) is a call rather than a parenthesised expression. Row 1 works because the method-implementation scope now walks UClsParent; rows 2 and 3 reach the alias by other paths that do not.

Do not reuse the method-pointer framing

The sibling fix's test carries a plain TAmt = Integer row precisely because a procedural-only test reads as a method-pointer bug. Here the procedural-ness IS the subject — but check an ordinal row before assuming any fix generalises, and check the owning-class spellings as the control that must stay green.

Not a regression

Both rows fail identically on pin v407 (095ef4811a5b). Long-standing.

CORRECTED AND FIXED — 2026-09-07 (frankS)

The table above is wrong in its first column and the correction is the whole finding. It says the type resolves in every row and only AliasProcSig fails to travel. Measured at HEAD and, identically, on pin v407:

spelling kind type resolves
FAmt: TAmt in the derived body plain alias no — unknown type
FRng: TRng in the derived body subrange no
FSel: TSel in the derived body procedural no
q: TDer.TAmt plain alias no
q: TDer.TSel procedural no
FEn: TEn, q: TDer.TEn enum yes
FRec: TRec, q: TDer.TRec record yes
FIn: TIn, q: TDer.TIn class yes

So this was never a signature-propagation defect. wrong number of parameters in call to TDer.Twice was the SECOND error in the file; the first was unknown type: TSel on the declaration two lines up, and the assignment failed because the field had no type, not because it had one with no signature.

Why the original reading is a class and not a slip

AliasVisibleHere holds three of the seven nested kinds. A nested RECORD and a nested CLASS are UClass registry rows and FindNestedType has walked UClsParent since it was written; a nested ENUM lives in EnumType*, which has no owner column, so it was never scoped at all. The three kinds anybody reaches for when probing "can a derived class see a base's nested type" are exactly the three that do not exercise this table. Three of six passing reads as "already resolved", and the comment in AliasVisibleHere said so in writing — a claim this ticket's own author then had to work around.

A probe that does not name its KIND is answering about a different table more often than not. The fixture names the kind on every row.

The fix

compiler/symtab.inc, AliasVisibleHere: the ParsingClassBodyCi arm gains an AliasOwnsThroughInherit disjunct beside its existing lexical one, and the QualTypeOwnerCi arm becomes AliasOwnsThroughInherit instead of an exact comparison. frankB's recorded reason for keeping the qualifier arm exact is about walking the ENCLOSING chain from a qualifier and is untouched: walking UClsParent from QualTypeOwnerCi answers precisely the question TDer.TAmt asks and cannot reach a class TDer does not descend from.

The stale sentence in AliasOwnsThroughInherit's own header is replaced rather than trimmed, for the reason FindTypeAlias's header gives one screen up: a stale invariant is what the next reader builds on.

Measured and NOT fixed

var q: TDer.TArr — a nested named ARRAY in a qualified declaration — is still refused. Not this defect: var b: TBase.TArr is refused identically, so inheritance has nothing to do with it. Filed separately as bug-p-a-nested-array-type-is-refused-in-a-qualified-declaration. The unqualified body spelling FArr: TArr is in the fixture and passes, which is what separates them.

Also seen and left alone: in {$mode delphi}, @procvar on an uninitialised method-pointer global answers a non-nil address here where fpc answers the variable's (nil) VALUE. A different subject — @ on a procedural variable — and no row in this fixture depends on it.

Log