← board

bug: metaclass construction via GetClass stamps a non-canonical VMT

Resolution (2026-06-24)

Root cause was NOT a wrong VMT offset — it was a missed parse route. The inline cast form TFooClass(expr).Create(args) (the streamer's TComponentClass(GetClass(name)).Create(owner)) has an AN_PTR_CAST base, but AN_METACLASS_NEW detection only matched an AN_IDENT (a class of variable). So the cast form fell through to the field-selector path → no real construction → garbage / non-canonical [inst+0]. The variable form was correct all along but had only ever been checked with tag= (ctor ran), never VMT identity.

Fix: detection now also fires for a metaclass-alias AN_PTR_CAST (AliasElemTk = tyClass); construction factored into BuildMetaclassNew shared by the var path (ParseLValueAST) and the cast path (ParseFactor). Both stamp the canonical class VMT ([cref+24] = cls^.VMTPtr) — virtual dispatch + RTTI identity hold.

Verified: TBaseClass(GetClass('TBase')).Create(3) → VMT == clsB^.VMTPtr; TBaseClass(GetClass('TDer')).Create(4) dispatches TDer.Kind ('der'), VMT == clsD^.VMTPtr. Regression test test/test_metaclass_getclass.pas. Self-host byte-identical; make test green. Track B can drop the four constructor-skip stopgaps and stream via TComponentClass(childCls).Create(parent).


Summary

TComponentClass(GetClass(name)).Create(args) — the metaclass-construction bridge that done/feature-metaclass-construct-dispatch advertised for the streamer — allocates an object whose stamped VMT is not the canonical class VMT. The virtual constructor dispatches and runs (so simple tag= tests pass), but the resulting instance fails VMT-identity: GetInstanceClassName returns '', and RTTI/virtual paths that rely on identity break. Streaming a component tree this way crashes.

Repro (lib/pcl available)

uses gtk3, controls, stdctrls, forms, classes_lite, typinfo;
var cls: PClassRTTI; c1: Pointer; c2: TComponent;
begin
  Application := TApplication.Create; Application.Initialize;
  cls := GetClass('TButton');
  c1 := CreateInstance(cls);
  writeln(GetInstanceClassName(c1));          { 'TButton'  — correct }
  c2 := TComponentClass(cls).Create(nil);
  writeln(GetInstanceClassName(Pointer(c2))); { ''         — WRONG, expect 'TButton' }
end.

CreateInstance (the old RTTI alloc) produces an instance whose VMT matches the registry; the metaclass path does not. Both produce a usable object for direct virtual calls (e.g. CreateHandle runs), so [cref+24] is a functional dispatch table — but it is a different pointer than RTTIPtr^.VMTPtr, the value GetInstanceClassName compares against.

Why this blocks Track B

done/feature-metaclass-construct-dispatch was verified only with tag= assertions (ctor ran); it never checked GetInstanceClassName / is / as / RTTI identity on the constructed object. The streamer (classes_lite.TReader) and is/as/RegisterClass-style code need the canonical VMT. With this open, the PCL streamer cannot adopt virtual constructors and must keep the four done/bug-lfm-streaming-skips-constructors stopgaps (CreateInstance + Canvas / array guards).

Likely cause

AN_METACLASS_NEW stamps [inst+0] := [cref+24]. When cref is a GetClass result (PClassRTTI from typinfo) rather than a compile-time class literal, the offset that holds "the VMT" for stamping differs from the offset/pointer the RTTI registry exposes as VMTPtr. Either the metaclass-new must stamp the same VMT pointer the registry records, or PClassRTTI and the class-literal class-ref must agree on the VMT field.

Acceptance

The repro prints TButton for both paths. A streamed PCL form constructs its widgets via their real Create(AOwner) (Canvas/arrays present, correct VMT), and test_pcl_lfm / test_pcl_event_rtti pass with the streamer calling TComponentClass(childCls).Create(parent) and the four constructor-skip stopgaps removed.

Log