Typed metaclasses: class of TFoo, so a TClass variable can call the class's methods
- Type: feature
- Track: P — Pascal frontend
- Status: done
- Follows: [[feature-pascal-metaclass-self]] (Self in a class method = the runtime class — landed)
- Blocks: [[feature-pascal-corpus-fpcunit]] if fpcunit's
TTestCaseClasspath needs it
What works today
Self in a class method is the metaclass and carries the RUNTIME class, and every
route to a class method passes it (named class, through an instance, bare sibling
call). Class-reference OPERATIONS work on any class reference:
var cr: TClass;
cr := TDerived;
writeln(cr.ClassName); { works }
writeln(cr.InheritsFrom(TBase)); { works }
What does not
Calling a class METHOD through a class-reference variable:
var cr: TClass;
begin
cr := TDerived;
cr.W; { error: member access on a bare object reference }
The value is right (it IS the blob pointer, and the call would pass it as Self
unchanged) — what is missing is knowing WHICH class's methods are in scope. TClass
is untyped: it is class of TObject, so only TObject's class methods should resolve
through it. FPC's answer is the TYPED metaclass:
type
TTestCaseClass = class of TTestCase; { fpcunit declares exactly this }
var
tc: TTestCaseClass;
begin
tc := TMyTest;
tc.Suite; { resolves against TTestCase's class methods }
Shape of the work
class of <ClassName>as a type: a pointer whose PtrElemRec names the class (today a class reference is tyPointer / PtrElemTk=tyClass / PtrElemRec=REC_NONE — the REC_NONE is exactly the "untyped" part).- Member access on a value of that type: look the method up in that class (
FindUMethon the named ci), then call it with the VALUE as Self. The call machinery already does the right thing —GenMakeStaticMethodCall(mpi, selfNode)takes any node for Self — so this is name resolution, not codegen. - A constructor through a typed metaclass (
tc.Create) is the metaclass-construct path that already exists (BuildMetaclassNew); wire it to the typed case. - Assigning a class literal to a typed metaclass var should check the class descends from the named one.
Note the dispatch is already dynamic: because Self is a real argument, a class method called through a metaclass variable runs with whatever class the variable holds. So this ticket is about the FRONTEND's name resolution, not about the runtime.
Gate
make test + self-host byte-identical + cross.
RESOLVED 2026-07-14 — already landed (stale ticket)
Everything in "shape of the work" shipped during the fpcunit/fpjson arcs:
typed metaclass member resolution + virtual class methods (b278), any
constructor through the metaclass (BuildMetaclassNew), descendant checking on
assignment (CheckMetaclassAssign), metaclass ARRAY ELEMENTS as receivers
(b328). Verified tonight: class method via tc: TBaseClass dispatches on the
RUNTIME class, virtual ctor builds the derived instance, ClassName answers.
Pinned by test_typed_metaclass_b278.pas + test_metaclass_array_element_b328.pas.
Log
- 2026-07-14 — resolved, commit HEAD.