feat(codegen): implicit-virtual constructor dispatch via metaclass

Constructors are now auto-slotted into the vtable by the semantic pass.
When a constructor is called through a metaclass-typed variable
(C.Create(args)), the compiler emits _ClassCreate for allocation
followed by a vtable-indirect call to the most-derived constructor
body. Direct calls (TFoo.Create) remain fully static.

Both backends (QBE and native x86-64) emit correct dispatch for:
- MetaclassVar.Create(args) syntax
- ClassCreate(MetaclassVar, args) builtin
- Zero-arg and multi-arg constructor signatures

This is a layout-changing commit (adds constructor vtable slots).
Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK,
3370 tests pass.
This commit is contained in:
Graeme Geldenhuys 2026-06-17 10:07:47 +01:00
parent 1753b5d2a8
commit 215082afc3
9 changed files with 521 additions and 90 deletions

View file

@ -5385,9 +5385,17 @@ begin
for SetI := FC.Args.Count - 1 downto 1 do
Self.Emit(Format(#9'popq %s', [SysVArg64(SetI)]));
Self.Emit(#9'movq %rbx, %rdi');
Self.Emit(Format(#9'callq %s',
[MethodEmitNameNative(TMethodDecl(FC.ResolvedDecl),
TMethodDecl(FC.ResolvedDecl).OwnerTypeName, 'Create')]));
if TMethodDecl(FC.ResolvedDecl).VTableSlot >= 0 then
begin
Self.Emit(#9'movq (%rdi), %rax');
Self.Emit(Format(#9'movq %d(%%rax), %%rax',
[(TMethodDecl(FC.ResolvedDecl).VTableSlot + 1) * 8]));
Self.Emit(#9'callq *%rax');
end
else
Self.Emit(Format(#9'callq %s',
[MethodEmitNameNative(TMethodDecl(FC.ResolvedDecl),
TMethodDecl(FC.ResolvedDecl).OwnerTypeName, 'Create')]));
Self.Emit(#9'movq %rbx, %rax');
Self.Emit(#9'popq %rbx');
end
@ -5396,9 +5404,17 @@ begin
Self.Emit(#9'pushq %rbx');
Self.Emit(#9'movq %rax, %rbx');
Self.Emit(#9'movq %rax, %rdi');
Self.Emit(Format(#9'callq %s',
[MethodEmitNameNative(TMethodDecl(FC.ResolvedDecl),
TMethodDecl(FC.ResolvedDecl).OwnerTypeName, 'Create')]));
if TMethodDecl(FC.ResolvedDecl).VTableSlot >= 0 then
begin
Self.Emit(#9'movq (%rdi), %rax');
Self.Emit(Format(#9'movq %d(%%rax), %%rax',
[(TMethodDecl(FC.ResolvedDecl).VTableSlot + 1) * 8]));
Self.Emit(#9'callq *%rax');
end
else
Self.Emit(Format(#9'callq %s',
[MethodEmitNameNative(TMethodDecl(FC.ResolvedDecl),
TMethodDecl(FC.ResolvedDecl).OwnerTypeName, 'Create')]));
Self.Emit(#9'movq %rbx, %rax');
Self.Emit(#9'popq %rbx');
end;
@ -7329,13 +7345,21 @@ begin
if ACall.IsConstructorCall then
begin
RT := TRecordTypeDesc(ACall.ResolvedClassType);
Self.Emit(Format(#9'movq $%d, %%rdi', [RT.TotalSize()]));
Self.Emit(Format(#9'leaq _FieldCleanup_%s(%%rip), %%rsi', [Self.ClassSymName(RT.Name)]));
Self.Emit(#9'callq _ClassAlloc');
if RT.HasVTable() then
if ACall.IsMetaclassDispatch then
begin
Self.Emit(Format(#9'leaq vtable_%s(%%rip), %%rcx', [Self.ClassSymName(RT.Name)]));
Self.Emit(#9'movq %rcx, (%rax)');
Self.Emit(Format(#9'movq %s, %%rdi', [Self.VarOperand(ACall.ObjectName)]));
Self.Emit(#9'callq _ClassCreate');
end
else
begin
Self.Emit(Format(#9'movq $%d, %%rdi', [RT.TotalSize()]));
Self.Emit(Format(#9'leaq _FieldCleanup_%s(%%rip), %%rsi', [Self.ClassSymName(RT.Name)]));
Self.Emit(#9'callq _ClassAlloc');
if RT.HasVTable() then
begin
Self.Emit(Format(#9'leaq vtable_%s(%%rip), %%rcx', [Self.ClassSymName(RT.Name)]));
Self.Emit(#9'movq %rcx, (%rax)');
end;
end;
if FDebugMode then
begin
@ -7370,7 +7394,14 @@ begin
for I := UserSlots - 1 downto 0 do
Self.Emit(#9'popq ' + SysVArg64(I + 1));
Self.Emit(#9'movq %rbx, %rdi');
Self.Emit(#9'callq ' + Sym);
if ACall.IsMetaclassDispatch and (MD.VTableSlot >= 0) then
begin
Self.Emit(#9'movq (%rdi), %rax');
Self.Emit(Format(#9'movq %d(%%rax), %%rax', [(MD.VTableSlot + 1) * 8]));
Self.Emit(#9'callq *%rax');
end
else
Self.Emit(#9'callq ' + Sym);
Self.EndCallArgs();
Self.Emit(#9'movq %rbx, %rax');
Self.Emit(#9'popq %rbx');
@ -7406,7 +7437,14 @@ begin
for I := 0 to 5 do
Self.Emit(Format(#9'movq %d(%%rsp), %s', [I * 8, SysVArg64(I)]));
Self.Emit(Format(#9'addq $%d, %%rsp', [6 * 8]));
Self.Emit(#9'callq ' + Sym);
if ACall.IsMetaclassDispatch and (MD.VTableSlot >= 0) then
begin
Self.Emit(#9'movq (%rdi), %rax');
Self.Emit(Format(#9'movq %d(%%rax), %%rax', [(MD.VTableSlot + 1) * 8]));
Self.Emit(#9'callq *%rax');
end
else
Self.Emit(#9'callq ' + Sym);
Self.EmitHoistEpilogue(ACall.Args, HD, HK, HTotal, CleanUp, True);
HD.Free();
HK.Free();

View file

@ -9698,8 +9698,20 @@ begin
ArgLine := ArgLine + Format(', %s %s',
[QbeParamTypeOf(Par.ResolvedType), ArgTemp]);
end;
EmitLine(Format(' call $%s(%s)',
[MethodEmitName(MDecl, MDecl.OwnerTypeName, 'Create'), ArgLine]));
if MDecl.VTableSlot >= 0 then
begin
VTblTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [VTblTemp, T]));
FPtrTemp := AllocTemp();
SlotOff := (MDecl.VTableSlot + 1) * 8;
ArgTemp := AllocTemp();
EmitLine(Format(' %s =l add %s, %d', [ArgTemp, VTblTemp, SlotOff]));
EmitLine(Format(' %s =l loadl %s', [FPtrTemp, ArgTemp]));
EmitLine(Format(' call %s(%s)', [FPtrTemp, ArgLine]));
end
else
EmitLine(Format(' call $%s(%s)',
[MethodEmitName(MDecl, MDecl.OwnerTypeName, 'Create'), ArgLine]));
end;
Exit(T);
end;
@ -10484,15 +10496,24 @@ begin
if MCallExpr.IsConstructorCall then
begin
SelfTemp := AllocTemp();
EmitLine(Format(' %s =l call $_ClassAlloc(l %d, l $_FieldCleanup_%s)',
[SelfTemp, RT.TotalSize(), ClassSymName(QBEMangle(RT.Name))]));
if RT.HasVTable() then
EmitLine(Format(' storel $vtable_%s, %s',
[ClassSymName(QBEMangle(RT.Name)), SelfTemp]));
{ No _ClassAddRef here — the assignment site (EmitAssignment) is
responsible for the retain on the receiving slot. Adding it here
as well produces a double-AddRef that prevents the object from
ever reaching refcount zero. }
if MCallExpr.IsMetaclassDispatch then
begin
{ Metaclass-var dispatch: C.Create(args). Load the metaclass value
(a typeinfo pointer) and call _ClassCreate which reads size/cleanup/
vtable from the typeinfo at runtime. }
L := AllocTemp();
EmitLine(Format(' %s =l loadl %s',
[L, VarRef(MCallExpr.ObjectName, MCallExpr.IsGlobal)]));
EmitLine(Format(' %s =l call $_ClassCreate(l %s)', [SelfTemp, L]));
end
else
begin
EmitLine(Format(' %s =l call $_ClassAlloc(l %d, l $_FieldCleanup_%s)',
[SelfTemp, RT.TotalSize(), ClassSymName(QBEMangle(RT.Name))]));
if RT.HasVTable() then
EmitLine(Format(' storel $vtable_%s, %s',
[ClassSymName(QBEMangle(RT.Name)), SelfTemp]));
end;
if FDebugMode then
begin
L := AllocTemp();
@ -10529,8 +10550,6 @@ begin
else if (Par.ResolvedType <> nil) and (Par.ResolvedType.Kind = tyInterface) then
begin
ArgTemps.Add('');
{ Class expression passed to an interface param: emit obj and
look up the static itab using the known target interface name. }
if TASTExpr(MCallExpr.Args.Items[I]).ResolvedType.Kind = tyClass then
begin
ArgTemp := EmitExpr(TASTExpr(MCallExpr.Args.Items[I]));
@ -10553,11 +10572,27 @@ begin
[QbeParamTypeOf(Par.ResolvedType), ArgTemp]);
end;
end;
if MDecl.OwnerTypeName <> '' then
FuncName := '$' + MethodEmitName(MDecl, MDecl.OwnerTypeName, MCallExpr.Name)
if MCallExpr.IsMetaclassDispatch and (MDecl.VTableSlot >= 0) then
begin
{ Indirect ctor call via vtable: load vptr from instance[0],
then load ctor address from vtable[ctorSlot]. }
VTblTemp := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [VTblTemp, SelfTemp]));
FPtrTemp := AllocTemp();
SlotOff := (MDecl.VTableSlot + 1) * 8;
ArgTemp := AllocTemp();
EmitLine(Format(' %s =l add %s, %d', [ArgTemp, VTblTemp, SlotOff]));
EmitLine(Format(' %s =l loadl %s', [FPtrTemp, ArgTemp]));
EmitLine(Format(' call %s(%s)', [FPtrTemp, ArgLine]));
end
else
FuncName := '$' + MethodEmitName(MDecl, RT.Name, MCallExpr.Name);
EmitLine(Format(' call %s(%s)', [FuncName, ArgLine]));
begin
if MDecl.OwnerTypeName <> '' then
FuncName := '$' + MethodEmitName(MDecl, MDecl.OwnerTypeName, MCallExpr.Name)
else
FuncName := '$' + MethodEmitName(MDecl, RT.Name, MCallExpr.Name);
EmitLine(Format(' call %s(%s)', [FuncName, ArgLine]));
end;
FlushPendingReleases(PMark);
EmitOwnedArgReleases(MCallExpr.Args, ArgTemps, MDecl.Params);
ReleaseConstStringArgs(MCallExpr.Args, ArgTemps, MDecl.Params);

View file

@ -536,6 +536,8 @@ type
IsGlobal: Boolean; { set by uSemantic — ObjectName is a program-level global }
IsVarParam: Boolean; { set by uSemantic — ObjectName is a var/out parameter }
IsBuiltinToString: Boolean; { set by uSemantic — built-in TObject.ToString virtual dispatch }
IsConstructorCall: Boolean; { set by uSemantic — metaclass-var Create in stmt position }
IsMetaclassDispatch: Boolean; { set by uSemantic — receiver is a metaclass var }
IsProcFieldCall: Boolean; { set by uSemantic — Name is a procedural-typed field of
the receiver, invoked directly (F.Handler;). Codegen
loads the (Code, Data) pair from the field slot and
@ -833,6 +835,7 @@ type
[Unretained] ResolvedClassType: TTypeDesc; { not owned; set by uSemantic }
[Unretained] ResolvedMethod: TObject; { TMethodDecl — not owned }
IsConstructorCall: Boolean; { set by uSemantic — TypeName.Create(args) }
IsMetaclassDispatch: Boolean; { set by uSemantic — receiver is a metaclass var }
IsGlobal: Boolean; { set by uSemantic — ObjectName is a program-level global }
IsVarParam: Boolean; { set by uSemantic — ObjectName is a var/out parameter }
IsBuiltinToString: Boolean; { set by uSemantic — built-in TObject.ToString virtual dispatch }

View file

@ -2465,7 +2465,17 @@ begin
else if NewMDecl.IsOverride then
RT.OverrideVTableSlot(
RT.FindVTableSlot(NewMDecl.Name),
'$' + CurrentUnitPrefix() + ATypeName + '_' + NewMDecl.Name);
'$' + CurrentUnitPrefix() + ATypeName + '_' + NewMDecl.Name)
else if (SameText(NewMDecl.Name, 'Create') or
(StrPos('Create', NewMDecl.Name) = 0)) and
(not NewMDecl.IsVirtual) and (not NewMDecl.IsOverride) then
begin
if RT.FindVTableSlot(NewMDecl.Name) >= 0 then
RT.OverrideVTableSlot(RT.FindVTableSlot(NewMDecl.Name),
'$' + CurrentUnitPrefix() + ATypeName + '_' + NewMDecl.Name)
else
RT.AddVTableSlot(NewMDecl.Name, '$' + CurrentUnitPrefix() + ATypeName + '_' + NewMDecl.Name);
end;
end;
{ Resolve fields }
@ -2513,7 +2523,8 @@ begin
RT.DestroyResolvedQbeName := NewMDecl.ResolvedQbeName;
end;
if NewMDecl.IsVirtual or NewMDecl.IsOverride then
if NewMDecl.IsVirtual or NewMDecl.IsOverride or
(SameText(NewMDecl.Name, 'Create') or (StrPos('Create', NewMDecl.Name) = 0)) then
NewMDecl.VTableSlot := RT.FindVTableSlot(NewMDecl.Name);
for K := 0 to NewMDecl.Params.Count - 1 do
@ -4317,6 +4328,16 @@ begin
{ Override clears the abstract flag on the inherited slot }
if Slot >= 0 then
RT.VTableEntryAt(Slot).IsAbstract := False;
end
else if (SameText(MDecl.Name, 'Create') or
(StrPos('Create', MDecl.Name) = 0)) and
(not MDecl.IsVirtual) and (not MDecl.IsOverride) then
begin
Slot := RT.FindVTableSlot(MangledKey);
if Slot >= 0 then
RT.OverrideVTableSlot(Slot, '$' + CurrentUnitPrefix() + TD.Name + '_' + MangledKey)
else
RT.AddVTableSlot(MangledKey, '$' + CurrentUnitPrefix() + TD.Name + '_' + MangledKey);
end;
end;
end;
@ -4425,8 +4446,10 @@ begin
RT.DestroyResolvedQbeName := MDecl.ResolvedQbeName;
end;
{ Retrieve the vtable slot assigned in the pre-pass above. }
if MDecl.IsVirtual or MDecl.IsOverride then
{ Retrieve the vtable slot assigned in the pre-pass above.
Constructors get implicit vtable slots (metaclass dispatch). }
if MDecl.IsVirtual or MDecl.IsOverride or
(SameText(MDecl.Name, 'Create') or (StrPos('Create', MDecl.Name) = 0)) then
begin
MDecl.VTableSlot := RT.FindVTableSlot(MangledKey);
if MDecl.IsOverride and (MDecl.VTableSlot < 0) then
@ -6451,6 +6474,26 @@ begin
SemanticError(
Format('''%s'' is not a variable', [ACall.ObjectName]),
ACall.Line, ACall.Col);
{ Metaclass-var constructor dispatch in statement position. }
if (ObjSym.TypeDesc.Kind = tyMetaClass) and
(SameText(ACall.Name, 'Create') or (StrPos('Create', ACall.Name) = 0)) then
begin
RT := TRecordTypeDesc(TMetaClassTypeDesc(ObjSym.TypeDesc).BaseClass);
for I := 0 to ACall.Args.Count - 1 do
AnalyseExpr(TASTExpr(ACall.Args.Items[I]));
MDecl := ResolveMethodOverload(RT.Name, ACall.Name, ACall.Args,
ACall.Line, ACall.Col);
if MDecl = nil then
MDecl := FindMethodDecl(RT.Name, ACall.Name);
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := MDecl;
ACall.IsConstructorCall := True;
ACall.IsMetaclassDispatch := True;
ACall.IsGlobal := ObjSym.IsGlobal;
ACall.IsVarParam := (ObjSym.Kind = skVarParameter);
Exit;
end;
if not (ObjSym.TypeDesc.Kind in [tyClass, tyInterface, tyRecord]) then
SemanticError(
Format('''%s'' is not a class, interface, or record variable', [ACall.ObjectName]),
@ -9062,6 +9105,30 @@ begin
Exit;
end;
{ Metaclass-var constructor dispatch: C.Create(args) where C is a
metaclass variable. Resolve against the BaseClass and flag for
indirect ctor dispatch via vtable at codegen time. }
if (ObjSym.TypeDesc.Kind = tyMetaClass) and
(SameText(AExpr.Name, 'Create') or (StrPos('Create', AExpr.Name) = 0)) then
begin
RT := TRecordTypeDesc(TMetaClassTypeDesc(ObjSym.TypeDesc).BaseClass);
for I := 0 to AExpr.Args.Count - 1 do
AnalyseExpr(TASTExpr(AExpr.Args.Items[I]));
MDecl := ResolveMethodOverload(RT.Name, AExpr.Name,
AExpr.Args, AExpr.Line, AExpr.Col);
if MDecl = nil then
MDecl := FindMethodDecl(RT.Name, AExpr.Name);
if MDecl <> nil then
AppendDefaultArgs(AExpr.Args, MDecl, AExpr.Name, AExpr.Line, AExpr.Col);
AExpr.ResolvedMethod := MDecl;
AExpr.ResolvedClassType := RT;
AExpr.IsConstructorCall := True;
AExpr.IsMetaclassDispatch := True;
AExpr.IsGlobal := ObjSym.IsGlobal;
AExpr.IsVarParam := (ObjSym.Kind = skVarParameter);
Exit(RT);
end;
if not (ObjSym.TypeDesc.Kind in [tyClass, tyInterface, tyRecord]) then
SemanticError(
Format('''%s'' is not a class, interface, or record variable', [AExpr.ObjectName]),

View file

@ -50,6 +50,12 @@ type
procedure TestSemantic_ClassCreate_RejectsNonMetaclassFirstArg;
procedure TestCodegen_ClassCreate_EmitsAllocAndCtorCall;
procedure TestCodegen_ClassCreate_NoCtor_OnlyAllocCalled;
{ Metaclass-var dispatch: C.Create() emits _ClassCreate + indirect
ctor call via vtable, not a static call to the base Create. }
procedure TestSemantic_MetaclassVar_Create_Accepted;
procedure TestCodegen_MetaclassVar_Create_IndirectCtor;
procedure TestCodegen_MetaclassVar_Create_NoCtor_NoIndirectCall;
end;
implementation
@ -454,8 +460,8 @@ var IR: string;
begin
IR := GenIR(Src);
AssertTrue('emits call to $_ClassCreate', Pos('call $_ClassCreate(', IR) > 0);
AssertTrue('emits static call to TFoo_Create after alloc',
Pos('call $TFoo_Create(', IR) > 0);
AssertTrue('emits indirect ctor call via vtable (no static TFoo_Create call)',
Pos('call $TFoo_Create(', IR) < 0);
end;
procedure TClassOfTests.TestCodegen_ClassCreate_NoCtor_OnlyAllocCalled;
@ -478,6 +484,70 @@ begin
Pos('TFoo_Create', IR) < 0);
end;
{ ---------- Metaclass-var C.Create() dispatch tests ---------- }
procedure TClassOfTests.TestSemantic_MetaclassVar_Create_Accepted;
const Src = '''
program P;
type
TFoo = class(TObject)
constructor Create;
end;
constructor TFoo.Create; begin end;
var C: class of TFoo; F: TFoo;
begin
C := TFoo;
F := C.Create()
end.
''';
begin
AnalyseSrc(Src);
end;
procedure TClassOfTests.TestCodegen_MetaclassVar_Create_IndirectCtor;
const Src = '''
program P;
type
TFoo = class(TObject)
constructor Create;
end;
constructor TFoo.Create; begin end;
var C: class of TFoo; F: TFoo;
begin
C := TFoo;
F := C.Create()
end.
''';
var IR: string;
begin
IR := GenIR(Src);
AssertTrue('emits _ClassCreate for runtime alloc',
Pos('call $_ClassCreate(', IR) > 0);
AssertTrue('emits indirect ctor call via vtable (loadl from vtable slot)',
Pos('loadl', IR) > 0);
AssertTrue('does NOT emit a static TFoo_Create call',
Pos('call $TFoo_Create(', IR) < 0);
end;
procedure TClassOfTests.TestCodegen_MetaclassVar_Create_NoCtor_NoIndirectCall;
const Src = '''
program P;
type TFoo = class(TObject) end;
var C: class of TFoo; F: TFoo;
begin
C := TFoo;
F := C.Create()
end.
''';
var IR: string;
begin
IR := GenIR(Src);
AssertTrue('emits _ClassCreate for runtime alloc',
Pos('call $_ClassCreate(', IR) > 0);
AssertTrue('no ctor call when class declares no Create',
Pos('TFoo_Create', IR) < 0);
end;
initialization
RegisterTest(TClassOfTests);
end.

View file

@ -117,6 +117,17 @@ type
{ var-param interface: method dispatch and reassignment through the
var param must reach the caller's variable. }
procedure TestRun_VarParamInterface_DispatchAndReassign;
{ Metaclass-var constructor dispatch: cls.Create() runs the most-derived
ctor body via vtable (implicitly virtual), not the base ctor statically. }
procedure TestRun_MetaclassCreate_DispatchesToDerived;
{ Metaclass-var ctor with args: cls.Create(N) passes args correctly. }
procedure TestRun_MetaclassCreate_WithArgs;
{ Direct TFoo.Create stays static (no vtable dispatch). }
procedure TestRun_DirectCreate_StaysStatic;
{ ClassCreate builtin still works (backwards compat) and now dispatches
the ctor body via vtable too. }
procedure TestRun_ClassCreate_StillWorks;
end;
implementation
@ -1743,6 +1754,131 @@ begin
'hello 1' + LE + 'hello 2' + LE + '2' + LE + 'hello 101' + LE + '101' + LE, 0);
end;
{ ---------- Metaclass-var constructor dispatch tests ---------- }
const SrcMetaclassCreateDerived = '''
program Prg;
type
TAnimal = class(TObject)
constructor Create;
end;
TDog = class(TAnimal)
constructor Create;
end;
TAnimalClass = class of TAnimal;
constructor TAnimal.Create;
begin WriteLn('animal') end;
constructor TDog.Create;
begin WriteLn('dog') end;
procedure MakeAndGreet(C: TAnimalClass);
var A: TAnimal;
begin
A := C.Create();
A.Free()
end;
begin
MakeAndGreet(TAnimal);
MakeAndGreet(TDog)
end.
''';
const SrcMetaclassCreateArgs = '''
program Prg;
type
TBase = class(TObject)
FVal: Integer;
constructor Create(N: Integer);
end;
TChild = class(TBase)
constructor Create(N: Integer);
end;
TBaseClass = class of TBase;
constructor TBase.Create(N: Integer);
begin FVal := N end;
constructor TChild.Create(N: Integer);
begin FVal := N * 10 end;
procedure Run(C: TBaseClass; N: Integer);
var B: TBase;
begin
B := C.Create(N);
WriteLn(B.FVal);
B.Free()
end;
begin
Run(TBase, 3);
Run(TChild, 3)
end.
''';
const SrcDirectCreateStatic = '''
program Prg;
type
TBase = class(TObject)
constructor Create;
end;
TChild = class(TBase)
constructor Create;
end;
constructor TBase.Create;
begin WriteLn('base') end;
constructor TChild.Create;
begin WriteLn('child') end;
begin
TBase.Create().Free();
TChild.Create().Free()
end.
''';
const SrcClassCreateStillWorks = '''
program Prg;
type
TFoo = class(TObject)
constructor Create;
end;
TBar = class(TFoo)
constructor Create;
end;
TFooClass = class of TFoo;
constructor TFoo.Create;
begin WriteLn('foo') end;
constructor TBar.Create;
begin WriteLn('bar') end;
var C: TFooClass;
begin
C := TBar;
ClassCreate(C).Free();
ClassCreate(TFoo).Free()
end.
''';
procedure TE2EClasses2Tests.TestRun_MetaclassCreate_DispatchesToDerived;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcMetaclassCreateDerived,
'animal' + LE + 'dog' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_MetaclassCreate_WithArgs;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcMetaclassCreateArgs,
'3' + LE + '30' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_DirectCreate_StaysStatic;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcDirectCreateStatic,
'base' + LE + 'child' + LE, 0);
end;
procedure TE2EClasses2Tests.TestRun_ClassCreate_StillWorks;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcClassCreateStillWorks,
'bar' + LE + 'foo' + LE, 0);
end;
initialization
RegisterTest(TE2EClasses2Tests);

View file

@ -1004,8 +1004,8 @@ ArrayLiteral
* ClassCreate ( Cls, ...args ) : Cls.BaseClass
* — runtime construction from a metaclass
* value; lowers to _ClassCreate(Cls)
* followed by a static call to the
* resolved constructor on Cls.BaseClass.
* followed by a vtable-indirect call to
* the constructor on Cls.BaseClass.
* Low ( Ordinal | Array | String ) : T — for ordinal type/expression
* returns lower bound of T;
* 0 for open/dynamic arrays and
@ -1061,6 +1061,9 @@ ArrayLiteral
*
* IDENT DOT IDENT in Factor:
* - Left IDENT resolves to a type symbol → constructor call (TypeName.Create)
* - Left IDENT resolves to a metaclass variable and right IDENT is a
* constructor name → metaclass constructor dispatch (_ClassCreate +
* vtable-indirect call to the most-derived Create body)
* - Left IDENT resolves to a class variable → class field read
* - Left IDENT resolves to a record variable → record field read
*

View file

@ -2198,7 +2198,8 @@ if A.ClassType = EError then … // identity match
The compiler exposes a built-in function `ClassCreate(Cls, ...args)` that
allocates an instance of the class denoted by `Cls` (a value of type `class
of T`) and invokes the resolved constructor on that class's `BaseClass`.
of T`) and dispatches the constructor body via vtable, so the most-derived
override runs.
[source,pascal]
----
@ -2207,7 +2208,7 @@ var
Inst: TTestCase;
begin
Cls := TFooTests; // metaclass = typeinfo pointer
Inst := ClassCreate(Cls, 'TestSomething'); // calls TTestCase.Create(string)
Inst := ClassCreate(Cls, 'TestSomething'); // vtable dispatch → TFooTests.Create
end;
----
@ -2216,6 +2217,9 @@ remaining arguments are passed to the constructor on `Cls.BaseClass`.
The result type is `Cls.BaseClass`, so the call expression assigns
cleanly to a variable of the base class type.
`ClassCreate` is the lower-level primitive; for most code the
`MetaclassVar.Create(args)` syntax (see next section) is preferred.
=== Rationale
The xUnit pattern needs to instantiate one test fixture per published
@ -2226,10 +2230,11 @@ the concrete TotalSize, vtable label, and field-cleanup label into the
emitted IR, all of which must come from the runtime class value.
The split design — a generic RTL helper `_ClassCreate(TInfo)` that
allocates and installs the vtable, plus a static call to the
constructor — avoids stuffing every constructor signature through the
RTL. The helper reads the three additional typeinfo slots (totalsize,
fieldcleanup, vtable) added in Step 11e for exactly this purpose.
allocates and installs the vtable, plus a vtable-indirect call to the
constructor body — dispatches to the most-derived constructor without
the caller knowing the concrete class. The helper reads the three
additional typeinfo slots (totalsize, fieldcleanup, vtable) added in
Step 11e for exactly this purpose.
The constructor lookup is by name (`Create`). Overload resolution on
the constructor signature is not yet implemented; in practice the only
@ -2245,17 +2250,88 @@ ClassCreate surface.
`RegisterTest` call site, polluting the cp.test.* migration
mechanically required by Step 11f. The runtime construction path
exists either way; this just moves where the boilerplate lives.
* *Make `metaclassVar.Create(args)` a special method-call shape in the
parser.* Requires the metaclass to know its own constructor symbol
table, which doesn't fit the current `class of T` lowering (a value
of `class of T` is just a typeinfo pointer; the constructor symbols
belong to T, not to the metaclass type). A dedicated builtin is
honest about what's actually a runtime dispatch.
* *Add a new vtable slot for the constructor and dispatch through
it.* Constructors in Blaise are not virtual; introducing a virtual
constructor would break the static `TFoo.Create` codegen invariant.
Keeping ClassCreate as a separate path leaves the existing
constructor lowering untouched.
---
== Implicit-Virtual Constructor Dispatch via Metaclass
=== Decision
When a constructor is called through a metaclass-typed variable
(`C.Create(args)` where `C: class of TBase`), the compiler emits
runtime vtable dispatch so the most-derived constructor body runs.
Direct calls (`TFoo.Create(args)` where `TFoo` is a concrete type
name) remain fully static.
Constructors are auto-slotted into the vtable by the semantic pass.
A constructor name that matches an ancestor's constructor overrides
the ancestor's vtable slot; new constructor names get a fresh slot.
No `virtual` or `override` keywords are required on constructors —
the dispatch is implicit and transparent.
[source,pascal]
----
type
TAnimal = class(TObject)
constructor Create;
end;
TDog = class(TAnimal)
constructor Create; // implicitly overrides TAnimal.Create's vtable slot
end;
var
C: class of TAnimal;
begin
C := TDog;
C.Create(); // dispatches to TDog.Create via vtable
TAnimal.Create(); // static call to TAnimal.Create (no dispatch)
end;
----
The lowering is:
1. `_ClassCreate(C)` — reads totalsize, fieldcleanup, and vtable from the
typeinfo pointer `C`; allocates and zeroes the instance; installs the
vtable; calls `_ClassAddRef`.
2. Load the vtable pointer from the new instance (`[instance+0]`), index
into the constructor's slot, and call indirectly.
Both the `C.Create(args)` syntax and `ClassCreate(C, args)` use this path.
=== Rationale
The xUnit test runner instantiates fixtures through `class of TTestCase`
and must run the derived class's constructor, not the base. Without
virtual dispatch, `ClassCreate` called the base constructor statically —
a derived class with initialisation in its own `Create` would silently
skip it.
Making constructors implicitly virtual only through metaclass-typed
receivers preserves the zero-overhead guarantee for direct
`TFoo.Create` calls (the overwhelming majority) while giving metaclass
dispatch the polymorphic behaviour users expect. Requiring explicit
`virtual`/`override` on constructors was considered and rejected: it
would force boilerplate on every class in the hierarchy, and the mental
model of "metaclass dispatch = polymorphic" is intuitive without it.
The vtable slot is assigned by constructor name (same name = same slot,
new name = new slot), matching the existing virtual-method slot
assignment algorithm.
=== Alternatives rejected
* *Require `virtual`/`override` on constructors.* Imposes annotation
burden on every class for a behaviour that only manifests through
metaclass dispatch. Direct `TFoo.Create` does not benefit from the
annotation. The implicit model is simpler and matches Delphi's
`virtual constructor` behaviour without the keyword noise.
* *Dispatch constructors through a separate RTTI table instead of the
vtable.* Adds a parallel lookup mechanism that must be kept in sync.
Constructors are regular methods with a `Self` parameter; the vtable
is the natural dispatch vector.
* *Keep constructors static even through metaclass dispatch.* This was
the original `ClassCreate` implementation. It fails the test-runner
use case: the derived fixture's constructor never runs, so the test
fixture is uninitialised.
---

View file

@ -69,29 +69,33 @@ TFieldAccessExpr.PropIndexExpr serialise
TFieldAccessExpr.IsCharAccess safe
TFieldAccessExpr.IsArrayAccess safe
# TIsExpr (uAST.pas:150)
# TIsExpr (uAST.pas:151)
TIsExpr.Obj serialise
TIsExpr.TypeName serialise
# TAsExpr (uAST.pas:158)
# TAsExpr (uAST.pas:159)
TAsExpr.Obj serialise
TAsExpr.TypeName serialise
# TSupportsExpr (uAST.pas:167)
# TSupportsExpr (uAST.pas:168)
TSupportsExpr.Obj serialise
TSupportsExpr.IntfTypeName serialise
TSupportsExpr.OutVarName serialise
TSupportsExpr.OutVarIsGlobal safe
# TBinaryExpr (uAST.pas:183)
# TInheritedCallExpr (uAST.pas:182)
TInheritedCallExpr.Name serialise
TInheritedCallExpr.Args serialise
# TBinaryExpr (uAST.pas:199)
TBinaryExpr.Op serialise
TBinaryExpr.Left serialise
TBinaryExpr.Right serialise
# TNotExpr (uAST.pas:191)
# TNotExpr (uAST.pas:207)
TNotExpr.Expr serialise
# TAssignment (uAST.pas:203)
# TAssignment (uAST.pas:219)
TAssignment.Name serialise
TAssignment.Expr serialise
TAssignment.IsVarParam safe
@ -100,23 +104,23 @@ TAssignment.IsThreadVar safe
TAssignment.IsWeakLhs safe
TAssignment.ImplicitSelfField safe
# TIfStmt (uAST.pas:219)
# TIfStmt (uAST.pas:235)
TIfStmt.Condition serialise
TIfStmt.ThenStmt serialise
TIfStmt.ElseStmt serialise
# TCompoundStmt (uAST.pas:227)
# TCompoundStmt (uAST.pas:243)
TCompoundStmt.Stmts serialise
# TWhileStmt (uAST.pas:234)
# TWhileStmt (uAST.pas:250)
TWhileStmt.Condition serialise
TWhileStmt.Body serialise
# TRepeatStmt (uAST.pas:241)
# TRepeatStmt (uAST.pas:257)
TRepeatStmt.Body serialise
TRepeatStmt.Condition serialise
# TForStmt (uAST.pas:248)
# TForStmt (uAST.pas:264)
TForStmt.VarName serialise
TForStmt.IsGlobal safe
TForStmt.StartExpr serialise
@ -124,7 +128,7 @@ TForStmt.EndExpr serialise
TForStmt.IsDownTo serialise
TForStmt.Body serialise
# TForInStmt (uAST.pas:259)
# TForInStmt (uAST.pas:275)
TForInStmt.VarName serialise
TForInStmt.VarIsGlobal safe
TForInStmt.CollExpr serialise
@ -144,34 +148,34 @@ TForInStmt.SetBitCount safe
TForInStmt.SetMaskVarName safe
TForInStmt.SetIsJumbo safe
# TTryFinallyStmt (uAST.pas:296)
# TTryFinallyStmt (uAST.pas:312)
TTryFinallyStmt.TryBody serialise
TTryFinallyStmt.FinallyBody serialise
# TTryExceptStmt (uAST.pas:313)
# TTryExceptStmt (uAST.pas:329)
TTryExceptStmt.TryBody serialise
TTryExceptStmt.Handlers serialise
TTryExceptStmt.ElseBody serialise
TTryExceptStmt.ExceptBody serialise
# TRaiseStmt (uAST.pas:326)
# TRaiseStmt (uAST.pas:342)
TRaiseStmt.Expr serialise
# TExitStmt (uAST.pas:337)
# TExitStmt (uAST.pas:353)
TExitStmt.Value safe
TExitStmt.ResultAssign safe
# TBreakStmt (uAST.pas:345)
# TBreakStmt (uAST.pas:361)
# TContinueStmt (uAST.pas:348)
# TContinueStmt (uAST.pas:364)
# TCaseStmt (uAST.pas:359)
# TCaseStmt (uAST.pas:375)
TCaseStmt.Selector serialise
TCaseStmt.Branches serialise
TCaseStmt.ElseStmt serialise
TCaseStmt.IsStringCase safe
# TFieldAssignment (uAST.pas:370)
# TFieldAssignment (uAST.pas:386)
TFieldAssignment.RecordName serialise
TFieldAssignment.FieldName serialise
TFieldAssignment.Expr serialise
@ -186,7 +190,7 @@ TFieldAssignment.PropOwnerType safe
TFieldAssignment.PropAccessorVSlot safe
TFieldAssignment.IsElemWrite safe
# TStaticSubscriptAssign (uAST.pas:397)
# TStaticSubscriptAssign (uAST.pas:414)
TStaticSubscriptAssign.ArrayName serialise
TStaticSubscriptAssign.IndexExpr serialise
TStaticSubscriptAssign.ValueExpr serialise
@ -197,18 +201,18 @@ TStaticSubscriptAssign.IsImplicitSelf safe
TStaticSubscriptAssign.PropOwnerType safe
TStaticSubscriptAssign.PropAccessorVSlot safe
# TPointerWriteStmt (uAST.pas:420)
# TPointerWriteStmt (uAST.pas:444)
TPointerWriteStmt.PtrExpr serialise
TPointerWriteStmt.ValExpr serialise
# TProcCall (uAST.pas:428)
# TProcCall (uAST.pas:452)
TProcCall.Name serialise
TProcCall.Args serialise
TProcCall.IsImplicitSelfMethod safe
TProcCall.IsIndirectCall safe
TProcCall.IndirectCallIsGlobal safe
# TFuncCallExpr (uAST.pas:441)
# TFuncCallExpr (uAST.pas:465)
TFuncCallExpr.Name serialise
TFuncCallExpr.Args serialise
TFuncCallExpr.IsImplicitSelfMethod safe
@ -218,18 +222,18 @@ TFuncCallExpr.IsBuiltinHasClassAttr safe
TFuncCallExpr.HasClassAttrClass safe
TFuncCallExpr.HasClassAttrAttr safe
# TIndirectFuncCallExpr (uAST.pas:467)
# TIndirectFuncCallExpr (uAST.pas:491)
TIndirectFuncCallExpr.CalleeExpr serialise
TIndirectFuncCallExpr.Args serialise
# TDerefExpr (uAST.pas:477)
# TDerefExpr (uAST.pas:501)
TDerefExpr.Expr serialise
# TAddrOfExpr (uAST.pas:484)
# TAddrOfExpr (uAST.pas:508)
TAddrOfExpr.Expr serialise
TAddrOfExpr.ResolvedFreeRoutine safe
# TMethodCallStmt (uAST.pas:496)
# TMethodCallStmt (uAST.pas:520)
TMethodCallStmt.ObjectName serialise
TMethodCallStmt.Name serialise
TMethodCallStmt.Args serialise
@ -238,22 +242,21 @@ TMethodCallStmt.IsImplicitSelf safe
TMethodCallStmt.IsGlobal safe
TMethodCallStmt.IsVarParam safe
TMethodCallStmt.IsBuiltinToString safe
TMethodCallStmt.IsConstructorCall safe
TMethodCallStmt.IsMetaclassDispatch safe
TMethodCallStmt.IsProcFieldCall safe
# TInheritedCallStmt (uAST.pas:528)
# TInheritedCallStmt (uAST.pas:554)
TInheritedCallStmt.Name serialise
TInheritedCallStmt.Args serialise
# TInheritedCallExpr (uAST.pas:183)
TInheritedCallExpr.Name serialise
TInheritedCallExpr.Args serialise
# TMethodCallExpr (uAST.pas:803)
# TMethodCallExpr (uAST.pas:829)
TMethodCallExpr.ObjectName serialise
TMethodCallExpr.Name serialise
TMethodCallExpr.Args serialise
TMethodCallExpr.ObjExpr serialise
TMethodCallExpr.IsConstructorCall safe
TMethodCallExpr.IsMetaclassDispatch safe
TMethodCallExpr.IsGlobal safe
TMethodCallExpr.IsVarParam safe
TMethodCallExpr.IsBuiltinToString safe