Implement interface type declarations, class implements, and interface dispatch

Parser: TInterfaceTypeDef AST node; ParseInterfaceDef; ParseClassDef extended
to parse comma-separated implements list (first name = parent class, rest =
interfaces); ParseMethodDecl body remains optional.

Symbol table: tyInterface added to TTypeKind; TInterfaceTypeDesc with unsorted
(declaration-order) method list for correct itab slot indexing; FImplements
non-owning list on TRecordTypeDesc tracks class→interface pairs; TObject
pre-registered as built-in root class.

Semantic: AnalyseTypeDecls handles TInterfaceTypeDef (register, inherit parent
methods); verifies class implements all interface methods; CheckTypesMatch
extended for class→interface assignment; AnalyseMethodCall handles tyInterface
object vars via itab dispatch path; TAssignment carries ResolvedLhsType.

Codegen: EmitInterfaceDefs emits $typeinfo_IFoo and $itab_TFoo_IFoo data blocks;
EmitVarAllocs allocates two-slot fat pointer (_obj + _itab) for interface vars;
EmitAssignment stores both obj pointer and itab address on interface assignment;
EmitMethodCall dispatches via itab on tyInterface receiver.

15 new tests, 484 total, 0 failures.
This commit is contained in:
Graeme Geldenhuys 2026-04-21 18:24:55 +01:00
parent c13901a2e2
commit c371272811
8 changed files with 858 additions and 50 deletions

View file

@ -84,9 +84,10 @@ type
TAssignment = class(TASTStmt)
public
Name: string;
Expr: TASTExpr; { owned }
IsVarParam: Boolean; { set by uSemantic — True if target is a var parameter }
Name: string;
Expr: TASTExpr; { owned }
IsVarParam: Boolean; { set by uSemantic — True if target is a var parameter }
ResolvedLhsType: TTypeDesc; { set by uSemantic — type of the target variable }
destructor Destroy; override;
end;
@ -255,10 +256,19 @@ type
end;
TClassTypeDef = class(TASTTypeDef)
public
ParentName: string;
ImplementsNames: TStringList; { owned — names of implemented interfaces }
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
constructor Create;
destructor Destroy; override;
end;
TInterfaceTypeDef = class(TASTTypeDef)
public
ParentName: string;
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
Methods: TObjectList; { owned TMethodDecl — forward signatures only }
constructor Create;
destructor Destroy; override;
end;
@ -560,14 +570,30 @@ end;
constructor TClassTypeDef.Create;
begin
inherited Create;
Fields := TObjectList.Create(True);
Methods := TObjectList.Create(True);
ImplementsNames := TStringList.Create;
Fields := TObjectList.Create(True);
Methods := TObjectList.Create(True);
end;
destructor TClassTypeDef.Destroy;
begin
Methods.Free;
Fields.Free;
ImplementsNames.Free;
inherited Destroy;
end;
{ TInterfaceTypeDef }
constructor TInterfaceTypeDef.Create;
begin
inherited Create;
Methods := TObjectList.Create(True);
end;
destructor TInterfaceTypeDef.Destroy;
begin
Methods.Free;
inherited Destroy;
end;

View file

@ -33,6 +33,7 @@ type
procedure EmitTypeInfoDefs(AProg: TProgram);
procedure EmitVTableDefs(AProg: TProgram);
procedure EmitMethodDefs(AProg: TProgram);
procedure EmitInterfaceDefs(AProg: TProgram);
procedure EmitMethodDef(const ATypeName: string; AMethod: TMethodDecl);
procedure EmitStandaloneDefs(AProg: TProgram);
procedure EmitStandaloneDef(ADecl: TMethodDecl);
@ -212,6 +213,15 @@ begin
EmitLine(Format(' storel 0, %%_var_%s', [VarName]));
end;
tyInterface:
begin
{ Interface var = fat pointer: obj slot + itab slot, both nil-init }
EmitLine(Format(' %%_var_%s_obj =l alloc8 1', [VarName]));
EmitLine(Format(' storel 0, %%_var_%s_obj', [VarName]));
EmitLine(Format(' %%_var_%s_itab =l alloc8 1', [VarName]));
EmitLine(Format(' storel 0, %%_var_%s_itab', [VarName]));
end;
else
raise ECodeGenError.CreateFmt(
'Unsupported type kind %d for variable ''%s''',
@ -539,11 +549,28 @@ end;
procedure TCodeGenQBE.EmitAssignment(AAssign: TAssignment);
var
ValTemp, OldTemp, QType, StoreInstr, PtrTemp: string;
IntfDesc: TInterfaceTypeDesc;
ClassRT: TRecordTypeDesc;
ItabName: string;
begin
if AAssign.Expr.ResolvedType = nil then
raise ECodeGenError.CreateFmt(
'Expression in assignment to ''%s'' has no resolved type', [AAssign.Name]);
{ Interface assignment: store obj pointer + itab pointer }
if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyInterface) and
(AAssign.Expr.ResolvedType.Kind = tyClass) then
begin
IntfDesc := TInterfaceTypeDesc(AAssign.ResolvedLhsType);
ClassRT := TRecordTypeDesc(AAssign.Expr.ResolvedType);
ItabName := '$itab_' + ClassRT.Name + '_' + IntfDesc.Name;
ValTemp := EmitExpr(AAssign.Expr);
EmitLine(Format(' storel %s, %%_var_%s_obj', [ValTemp, AAssign.Name]));
EmitLine(Format(' storel %s, %%_var_%s_itab', [ItabName, AAssign.Name]));
Exit;
end;
if AAssign.IsVarParam then
begin
{ Var param: load the stored pointer, then store the value through it }
@ -638,7 +665,31 @@ var
VTblTemp: string;
FPtrTemp: string;
SlotOff: Integer;
IntfDesc: TInterfaceTypeDesc;
begin
{ Interface method dispatch: load obj + itab, index by method slot }
if (ACall.ResolvedClassType <> nil) and
(ACall.ResolvedClassType.Kind = tyInterface) then
begin
IntfDesc := TInterfaceTypeDesc(ACall.ResolvedClassType);
SelfTemp := AllocTemp;
EmitLine(Format(' %s =l loadl %%_var_%s_obj', [SelfTemp, ACall.ObjectName]));
VTblTemp := AllocTemp;
EmitLine(Format(' %s =l loadl %%_var_%s_itab', [VTblTemp, ACall.ObjectName]));
SlotOff := IntfDesc.MethodIndex(ACall.Name) * 8;
FPtrTemp := AllocTemp;
if SlotOff = 0 then
EmitLine(Format(' %s =l loadl %s', [FPtrTemp, VTblTemp]))
else
begin
ArgTemp := AllocTemp;
EmitLine(Format(' %s =l add %s, %d', [ArgTemp, VTblTemp, SlotOff]));
EmitLine(Format(' %s =l loadl %s', [FPtrTemp, ArgTemp]));
end;
EmitLine(Format(' call %%%s(l %s)', [FPtrTemp, SelfTemp]));
Exit;
end;
{ Built-in Free: load Self pointer and call C free() }
if (ACall.ResolvedMethod = nil) and SameText(ACall.Name, 'Free') then
begin
@ -849,6 +900,55 @@ begin
EmitLine('');
end;
procedure TCodeGenQBE.EmitInterfaceDefs(AProg: TProgram);
{ Emit typeinfo blocks for interfaces and itab blocks for class-interface pairs.
Interface typeinfo: data $typeinfo_IFoo = { l 0 } (address IS the identity)
Itab: data $itab_TFoo_IFoo = { l $TFoo_DoIt, l $TFoo_GetVal }
Methods appear in declaration order (itab slot N = interface method N). }
var
I, J, K: Integer;
TD: TTypeDecl;
TDesc: TTypeDesc;
IntfDesc: TInterfaceTypeDesc;
ClassRT: TRecordTypeDesc;
ItabLine: string;
MethName: string;
begin
{ Typeinfo blocks for every interface }
for I := 0 to AProg.Block.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(AProg.Block.TypeDecls[I]);
if not (TD.Def is TInterfaceTypeDef) then Continue;
EmitLine('data $typeinfo_' + TD.Name + ' = { l 0 }');
end;
{ Itab blocks for each (class, interface) pair }
for I := 0 to AProg.Block.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(AProg.Block.TypeDecls[I]);
if not (TD.Def is TClassTypeDef) then Continue;
TDesc := AProg.SymbolTable.FindType(TD.Name);
if (TDesc = nil) or not (TDesc is TRecordTypeDesc) then Continue;
ClassRT := TRecordTypeDesc(TDesc);
for J := 0 to ClassRT.ImplementsCount - 1 do
begin
IntfDesc := ClassRT.ImplementsIntfAt(J);
ItabLine := 'data $itab_' + TD.Name + '_' + IntfDesc.Name + ' = {';
for K := 0 to IntfDesc.MethodCount - 1 do
begin
MethName := IntfDesc.MethodName(K);
if K = 0 then
ItabLine := ItabLine + ' l $' + TD.Name + '_' + MethName
else
ItabLine := ItabLine + ', l $' + TD.Name + '_' + MethName;
end;
ItabLine := ItabLine + ' }';
EmitLine(ItabLine);
end;
end;
EmitLine('');
end;
procedure TCodeGenQBE.EmitMethodDefs(AProg: TProgram);
var
I, J: Integer;
@ -1409,6 +1509,7 @@ begin
EmitLine('# Source: ' + AProg.Name);
EmitLine('');
EmitDataSection;
EmitInterfaceDefs(AProg);
EmitTypeInfoDefs(AProg);
EmitVTableDefs(AProg);
FOutput.AddStrings(Body);

View file

@ -54,6 +54,7 @@ type
procedure ParseTypeDecl(ABlock: TBlock);
function ParseRecordDef: TRecordTypeDef;
function ParseClassDef: TClassTypeDef;
function ParseInterfaceDef: TInterfaceTypeDef;
procedure ParseFieldDecl(AFields: TObjectList);
function ParseMethodDecl(IsFunction: Boolean): TMethodDecl;
procedure ParseParamList(AParams: TObjectList);
@ -228,9 +229,11 @@ begin
TD.Def := ParseRecordDef
else if Check(tkClass) then
TD.Def := ParseClassDef
else if Check(tkIntf) then
TD.Def := ParseInterfaceDef
else
raise EParseError.CreateFmt(
'Expected ''record'' or ''class'' at line %d col %d',
'Expected ''record'', ''class'', or ''interface'' at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Expect(tkSemicolon);
ABlock.TypeDecls.Add(TD);
@ -271,6 +274,16 @@ begin
[FCurrent.Line, FCurrent.Col]);
Result.ParentName := FCurrent.Value;
Advance;
{ Additional names after a comma are implemented interface names }
while Check(tkComma) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected interface name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Result.ImplementsNames.Add(FCurrent.Value);
Advance;
end;
Expect(tkRParen);
end;
while Check(tkIdent) do
@ -289,6 +302,37 @@ begin
end;
end;
function TParser.ParseInterfaceDef: TInterfaceTypeDef;
begin
Result := TInterfaceTypeDef.Create;
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
Expect(tkIntf);
if Check(tkLParen) then
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected parent interface name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Result.ParentName := FCurrent.Value;
Advance;
Expect(tkRParen);
end;
while Check(tkProcedure) or Check(tkFunction) do
begin
if Check(tkFunction) then
Result.Methods.Add(ParseMethodDecl(True))
else
Result.Methods.Add(ParseMethodDecl(False));
end;
Expect(tkEnd);
except
Result.Free;
raise;
end;
end;
function TParser.ParseMethodDecl(IsFunction: Boolean): TMethodDecl;
begin
Result := TMethodDecl.Create;

View file

@ -111,15 +111,26 @@ end;
procedure TSemanticAnalyser.CheckTypesMatch(AExpected, AActual: TTypeDesc;
const AContext: string; ALine, ACol: Integer);
var
RT: TRecordTypeDesc;
I: Integer;
begin
if AExpected = AActual then
Exit;
{ nil is compatible with any class type }
if (AActual.Kind = tyNil) and (AExpected.Kind = tyClass) then
{ nil is compatible with any class or interface type }
if (AActual.Kind = tyNil) and (AExpected.Kind in [tyClass, tyInterface]) then
Exit;
{ subtype assignment: TDerived → TBase is allowed }
if IsSubtypeOf(AActual, AExpected) then
Exit;
{ class → interface: allowed when the class implements that interface }
if (AExpected.Kind = tyInterface) and (AActual.Kind = tyClass) then
begin
RT := TRecordTypeDesc(AActual);
for I := 0 to RT.ImplementsCount - 1 do
if RT.ImplementsIntfAt(I) = AExpected then
Exit;
end;
SemanticError(
Format('Type mismatch in %s: expected ''%s'' but got ''%s''',
[AContext, AExpected.Name, AActual.Name]),
@ -328,6 +339,7 @@ end;
procedure TSemanticAnalyser.AnalyseTypeDecls(ABlock: TBlock);
var
I, J, K: Integer;
L: Integer;
TD: TTypeDecl;
FieldList: TObjectList;
MethodList: TObjectList;
@ -343,6 +355,10 @@ var
Sym: TSymbol;
Key: string;
FldInfo: TFieldInfo;
IntfDesc: TInterfaceTypeDesc;
IntfName: string;
IntfSym: TSymbol;
ITD: TInterfaceTypeDef;
begin
{ Pass 1 register all type symbols with empty descriptors.
This allows self-referential field types to resolve in pass 2. }
@ -353,9 +369,20 @@ begin
RT := FTable.NewRecordType(TD.Name)
else if TD.Def is TClassTypeDef then
RT := FTable.NewClassType(TD.Name)
else if TD.Def is TInterfaceTypeDef then
begin
IntfDesc := FTable.NewInterfaceType(TD.Name);
Sym := TSymbol.Create(TD.Name, skType, IntfDesc);
if not FTable.Define(Sym) then
begin
Sym.Free;
SemanticError(Format('Duplicate type name ''%s''', [TD.Name]), TD.Line, TD.Col);
end;
Continue;
end
else
begin
SemanticError('Only record and class type definitions are supported',
SemanticError('Only record, class, or interface type definitions are supported',
TD.Line, TD.Col);
Continue;
end;
@ -372,6 +399,30 @@ begin
begin
TD := TTypeDecl(ABlock.TypeDecls[I]);
{ Interface types: register methods and resolve optional parent }
if TD.Def is TInterfaceTypeDef then
begin
ITD := TInterfaceTypeDef(TD.Def);
IntfSym := FTable.Lookup(TD.Name);
IntfDesc := TInterfaceTypeDesc(IntfSym.TypeDesc);
if ITD.ParentName <> '' then
begin
Sym := FTable.Lookup(ITD.ParentName);
if (Sym = nil) or not (Sym.TypeDesc is TInterfaceTypeDesc) then
SemanticError(
Format('Unknown parent interface ''%s'' for ''%s''',
[ITD.ParentName, TD.Name]),
TD.Line, TD.Col);
IntfDesc.Parent := TInterfaceTypeDesc(Sym.TypeDesc);
{ Inherit parent methods }
for J := 0 to IntfDesc.Parent.MethodCount - 1 do
IntfDesc.AddMethod(IntfDesc.Parent.MethodName(J));
end;
for J := 0 to ITD.Methods.Count - 1 do
IntfDesc.AddMethod(TMethodDecl(ITD.Methods[J]).Name);
Continue;
end;
Sym := FTable.Lookup(TD.Name);
RT := TRecordTypeDesc(Sym.TypeDesc);
@ -479,6 +530,42 @@ begin
MDecl.ResolvedReturnType := ParType;
end;
end;
{ Verify class implements all methods of each declared interface }
if TD.Def is TClassTypeDef then
for L := 0 to TClassTypeDef(TD.Def).ImplementsNames.Count - 1 do
begin
IntfName := TClassTypeDef(TD.Def).ImplementsNames[L];
IntfSym := FTable.Lookup(IntfName);
if (IntfSym = nil) or not (IntfSym.TypeDesc is TInterfaceTypeDesc) then
SemanticError(
Format('Unknown interface ''%s'' in implements list of ''%s''',
[IntfName, TD.Name]),
TD.Line, TD.Col);
IntfDesc := TInterfaceTypeDesc(IntfSym.TypeDesc);
RT.AddImplements(IntfDesc);
for J := 0 to IntfDesc.MethodCount - 1 do
begin
Key := IntfDesc.MethodName(J);
if RT.FindField(Key) = nil then
begin
{ Check method exists in class — search method list }
MDecl := nil;
if TD.Def is TClassTypeDef then
for K := 0 to TClassTypeDef(TD.Def).Methods.Count - 1 do
if SameText(TMethodDecl(TClassTypeDef(TD.Def).Methods[K]).Name, Key) then
begin
MDecl := TMethodDecl(TClassTypeDef(TD.Def).Methods[K]);
Break;
end;
if MDecl = nil then
SemanticError(
Format('Class ''%s'' does not implement method ''%s'' from interface ''%s''',
[TD.Name, Key, IntfName]),
TD.Line, TD.Col);
end;
end;
end;
end;
end;
@ -875,11 +962,25 @@ begin
SemanticError(
Format('''%s'' is not a variable', [ACall.ObjectName]),
ACall.Line, ACall.Col);
if ObjSym.TypeDesc.Kind <> tyClass then
if not (ObjSym.TypeDesc.Kind in [tyClass, tyInterface]) then
SemanticError(
Format('''%s'' is not a class variable', [ACall.ObjectName]),
Format('''%s'' is not a class or interface variable', [ACall.ObjectName]),
ACall.Line, ACall.Col);
{ Interface method call: look up method in interface type descriptor }
if ObjSym.TypeDesc.Kind = tyInterface then
begin
if not TInterfaceTypeDesc(ObjSym.TypeDesc).HasMethod(ACall.Name) then
SemanticError(
Format('Interface ''%s'' has no method ''%s''',
[ObjSym.TypeDesc.Name, ACall.Name]),
ACall.Line, ACall.Col);
{ Args for interface methods not checked in Phase 3 (no param info stored) }
ACall.ResolvedClassType := ObjSym.TypeDesc;
ACall.ResolvedMethod := nil; { nil = interface dispatch, not class dispatch }
Exit;
end;
RT := TRecordTypeDesc(ObjSym.TypeDesc);
MDecl := FindMethodDecl(RT.Name, ACall.Name);
if MDecl = nil then
@ -930,7 +1031,8 @@ begin
Format('''%s'' is not a variable', [AAssign.Name]),
AAssign.Line, AAssign.Col);
AAssign.IsVarParam := (VarSym.Kind = skVarParameter);
AAssign.IsVarParam := (VarSym.Kind = skVarParameter);
AAssign.ResolvedLhsType := VarSym.TypeDesc;
ExprType := AnalyseExpr(AAssign.Expr);
CheckTypesMatch(VarSym.TypeDesc, ExprType, 'assignment', AAssign.Line, AAssign.Col);

View file

@ -21,6 +21,7 @@ type
tyString, { ARC-managed UTF-8 string }
tyRecord, { Stack-allocated aggregate (Phase 2) }
tyClass, { Heap-allocated, single-inheritance (Phase 2) }
tyInterface, { Zero-GUID interface reference (Phase 3) }
tyVoid, { No value — used as procedure return type }
tyNil { Pseudo-type for the nil literal; compatible with tyClass }
);
@ -55,13 +56,16 @@ type
ImplName: string; { fully-qualified QBE label, e.g. $TDog_Speak }
end;
TInterfaceTypeDesc = class; { forward }
{ Extended type descriptor for record types. }
TRecordTypeDesc = class(TTypeDesc)
private
FFields: TObjectList; { owned TFieldInfo }
FKeys: TStringList; { sorted, case-insensitive; Objects[] = TFieldInfo (not owned) }
FParent: TRecordTypeDesc; { not owned; nil for root classes }
FVTable: TObjectList; { owned TVTableEntry; nil if no virtual methods }
FFields: TObjectList; { owned TFieldInfo }
FKeys: TStringList; { sorted, case-insensitive; Objects[] = TFieldInfo (not owned) }
FParent: TRecordTypeDesc; { not owned; nil for root classes }
FVTable: TObjectList; { owned TVTableEntry; nil if no virtual methods }
FImplements: TObjectList; { not owned — TInterfaceTypeDesc references }
public
constructor Create(const AName: string; AKind: TTypeKind = tyRecord);
destructor Destroy; override;
@ -75,17 +79,35 @@ type
function VTableCount: Integer;
function VTableEntryAt(ASlot: Integer): TVTableEntry;
function FindVTableSlot(const AMethodName: string): Integer;
{ Assigns a new slot for a virtual method; returns the slot index. }
function AddVTableSlot(const AMethodName, AImplName: string): Integer;
{ Overrides an existing slot (for 'override' declarations). }
procedure OverrideVTableSlot(ASlot: Integer; const AImplName: string);
{ Copies all vtable entries from the parent (called during class analysis). }
procedure CopyVTableFrom(AParent: TRecordTypeDesc);
{ Interface implements tracking }
procedure AddImplements(AIntf: TInterfaceTypeDesc);
function ImplementsCount: Integer;
function ImplementsIntfAt(AIndex: Integer): TInterfaceTypeDesc;
property Fields: TObjectList read FFields;
property Parent: TRecordTypeDesc read FParent write FParent;
end;
{ Type descriptor for zero-GUID interface types (Phase 3). }
TInterfaceTypeDesc = class(TTypeDesc)
private
FMethods: TStringList; { method names, case-insensitive sorted }
FParent: TInterfaceTypeDesc; { not owned; nil if no parent }
public
constructor Create(const AName: string);
destructor Destroy; override;
procedure AddMethod(const AName: string);
function HasMethod(const AName: string): Boolean;
function MethodCount: Integer;
function MethodName(AIndex: Integer): string;
function MethodIndex(const AName: string): Integer;
property Parent: TInterfaceTypeDesc read FParent write FParent;
end;
{ ------------------------------------------------------------------ }
{ Symbols }
{ ------------------------------------------------------------------ }
@ -182,6 +204,9 @@ type
{ Creates a new class type descriptor (tyClass, heap-allocated). }
function NewClassType(const AName: string): TRecordTypeDesc;
{ Creates a new interface type descriptor (tyInterface). }
function NewInterfaceType(const AName: string): TInterfaceTypeDesc;
{ Convenience type accessors }
property TypeInteger: TTypeDesc read FTypeInteger;
property TypeInt64: TTypeDesc read FTypeInt64;
@ -252,18 +277,20 @@ end;
constructor TRecordTypeDesc.Create(const AName: string; AKind: TTypeKind = tyRecord);
begin
inherited Create;
Kind := AKind;
Name := AName;
FFields := TObjectList.Create(True);
FKeys := TStringList.Create;
Kind := AKind;
Name := AName;
FFields := TObjectList.Create(True);
FKeys := TStringList.Create;
FKeys.Sorted := True;
FKeys.CaseSensitive := False;
FKeys.Duplicates := dupIgnore;
FVTable := nil; { allocated on first use }
FVTable := nil; { allocated on first use }
FImplements := TObjectList.Create(False); { not owned }
end;
destructor TRecordTypeDesc.Destroy;
begin
FImplements.Free;
FKeys.Free;
FFields.Free;
FVTable.Free;
@ -381,6 +408,21 @@ begin
TVTableEntry(FVTable[ASlot]).ImplName := AImplName;
end;
procedure TRecordTypeDesc.AddImplements(AIntf: TInterfaceTypeDesc);
begin
FImplements.Add(AIntf);
end;
function TRecordTypeDesc.ImplementsCount: Integer;
begin
Result := FImplements.Count;
end;
function TRecordTypeDesc.ImplementsIntfAt(AIndex: Integer): TInterfaceTypeDesc;
begin
Result := TInterfaceTypeDesc(FImplements[AIndex]);
end;
procedure TRecordTypeDesc.CopyVTableFrom(AParent: TRecordTypeDesc);
var
I: Integer;
@ -400,6 +442,51 @@ begin
end;
end;
{ ------------------------------------------------------------------ }
{ TInterfaceTypeDesc }
{ ------------------------------------------------------------------ }
constructor TInterfaceTypeDesc.Create(const AName: string);
begin
inherited Create;
Kind := tyInterface;
Name := AName;
FMethods := TStringList.Create;
FMethods.CaseSensitive := False; { unsorted — preserves declaration order }
FParent := nil;
end;
destructor TInterfaceTypeDesc.Destroy;
begin
FMethods.Free;
inherited Destroy;
end;
procedure TInterfaceTypeDesc.AddMethod(const AName: string);
begin
FMethods.Add(AName);
end;
function TInterfaceTypeDesc.HasMethod(const AName: string): Boolean;
begin
Result := FMethods.IndexOf(AName) >= 0;
end;
function TInterfaceTypeDesc.MethodCount: Integer;
begin
Result := FMethods.Count;
end;
function TInterfaceTypeDesc.MethodName(AIndex: Integer): string;
begin
Result := FMethods[AIndex];
end;
function TInterfaceTypeDesc.MethodIndex(const AName: string): Integer;
begin
Result := FMethods.IndexOf(AName);
end;
{ ------------------------------------------------------------------ }
{ TSymbol }
{ ------------------------------------------------------------------ }
@ -522,6 +609,12 @@ begin
FAllTypes.Add(Result);
end;
function TSymbolTable.NewInterfaceType(const AName: string): TInterfaceTypeDesc;
begin
Result := TInterfaceTypeDesc.Create(AName);
FAllTypes.Add(Result);
end;
procedure TSymbolTable.RegisterBuiltins;
var
Sym: TSymbol;
@ -544,6 +637,9 @@ begin
Define(TSymbol.Create('Boolean', skType, FTypeBoolean));
Define(TSymbol.Create('string', skType, FTypeString));
{ TObject — root of the class hierarchy; no fields, no parent }
Define(TSymbol.Create('TObject', skType, NewClassType('TObject')));
{ Built-in I/O procedures }
Sym := TSymbol.Create('Write', skProcedure, nil);
Define(Sym);

View file

@ -28,7 +28,8 @@ uses
cp.test.units,
cp.test.varparams,
cp.test.vtable,
cp.test.typetests;
cp.test.typetests,
cp.test.interfaces;
var
Application: TTestRunner;

View file

@ -0,0 +1,428 @@
unit cp.test.interfaces;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, StrUtils, fpcunit, testregistry,
uLexer, uParser, uAST, uSymbolTable, uSemantic, uCodeGenQBE;
type
TInterfaceTests = class(TTestCase)
private
function ParseSrc(const ASrc: string): TProgram;
function AnalyseSrc(const ASrc: string): TProgram;
function GenIR(const ASrc: string): string;
procedure AnalyseExpectError(const ASrc: string);
published
{ ------------------------------------------------------------------ }
{ Parser }
{ ------------------------------------------------------------------ }
procedure TestParse_Interface_Empty;
procedure TestParse_Interface_WithMethods;
procedure TestParse_Interface_WithParent;
procedure TestParse_Class_ImplementsInterface;
procedure TestParse_Class_ImplementsMultiple;
{ ------------------------------------------------------------------ }
{ Semantic }
{ ------------------------------------------------------------------ }
procedure TestSemantic_Interface_Registered;
procedure TestSemantic_Interface_IsInterfaceKind;
procedure TestSemantic_Interface_MethodsRegistered;
procedure TestSemantic_ClassImplements_OK;
procedure TestSemantic_ClassImplements_MissingMethod_RaisesError;
{ ------------------------------------------------------------------ }
{ Code generation }
{ ------------------------------------------------------------------ }
procedure TestCodegen_Interface_TypeInfo_Emitted;
procedure TestCodegen_Class_Itab_Emitted;
procedure TestCodegen_Itab_ContainsMethodPointer;
procedure TestCodegen_InterfaceVar_AllocsTwoSlots;
procedure TestCodegen_InterfaceMethodCall_IndirectDispatch;
end;
implementation
const
SrcInterfaceEmpty =
'program P;' + LineEnding +
'type' + LineEnding +
' IFoo = interface' + LineEnding +
' end;' + LineEnding +
'begin' + LineEnding +
'end.';
SrcInterfaceWithMethods =
'program P;' + LineEnding +
'type' + LineEnding +
' IFoo = interface' + LineEnding +
' procedure DoIt;' + LineEnding +
' function GetVal: Integer;' + LineEnding +
' end;' + LineEnding +
'begin' + LineEnding +
'end.';
SrcInterfaceWithParent =
'program P;' + LineEnding +
'type' + LineEnding +
' IBase = interface' + LineEnding +
' procedure Base;' + LineEnding +
' end;' + LineEnding +
' IChild = interface(IBase)' + LineEnding +
' procedure Child;' + LineEnding +
' end;' + LineEnding +
'begin' + LineEnding +
'end.';
SrcClassImplements =
'program P;' + LineEnding +
'type' + LineEnding +
' IFoo = interface' + LineEnding +
' procedure DoIt;' + LineEnding +
' function GetVal: Integer;' + LineEnding +
' end;' + LineEnding +
' TFoo = class(TObject, IFoo)' + LineEnding +
' procedure DoIt;' + LineEnding +
' function GetVal: Integer;' + LineEnding +
' end;' + LineEnding +
'procedure TFoo.DoIt;' + LineEnding +
'begin' + LineEnding +
'end;' + LineEnding +
'function TFoo.GetVal: Integer;' + LineEnding +
'begin' + LineEnding +
' Result := 42' + LineEnding +
'end;' + LineEnding +
'begin' + LineEnding +
'end.';
SrcClassImplementsMultiple =
'program P;' + LineEnding +
'type' + LineEnding +
' IFoo = interface' + LineEnding +
' procedure DoIt;' + LineEnding +
' end;' + LineEnding +
' IBar = interface' + LineEnding +
' procedure DoBar;' + LineEnding +
' end;' + LineEnding +
' TFoo = class(TObject, IFoo, IBar)' + LineEnding +
' procedure DoIt;' + LineEnding +
' procedure DoBar;' + LineEnding +
' end;' + LineEnding +
'procedure TFoo.DoIt;' + LineEnding +
'begin' + LineEnding +
'end;' + LineEnding +
'procedure TFoo.DoBar;' + LineEnding +
'begin' + LineEnding +
'end;' + LineEnding +
'begin' + LineEnding +
'end.';
SrcClassMissingMethod =
'program P;' + LineEnding +
'type' + LineEnding +
' IFoo = interface' + LineEnding +
' procedure DoIt;' + LineEnding +
' end;' + LineEnding +
' TFoo = class(TObject, IFoo)' + LineEnding +
' end;' + LineEnding +
'begin' + LineEnding +
'end.';
SrcInterfaceVar =
'program P;' + LineEnding +
'type' + LineEnding +
' IFoo = interface' + LineEnding +
' procedure DoIt;' + LineEnding +
' end;' + LineEnding +
' TFoo = class(TObject, IFoo)' + LineEnding +
' procedure DoIt;' + LineEnding +
' end;' + LineEnding +
'procedure TFoo.DoIt;' + LineEnding +
'begin' + LineEnding +
'end;' + LineEnding +
'var' + LineEnding +
' F: IFoo;' + LineEnding +
' T: TFoo;' + LineEnding +
'begin' + LineEnding +
' T := TFoo.Create;' + LineEnding +
' F := T;' + LineEnding +
' F.DoIt' + LineEnding +
'end.';
{ ------------------------------------------------------------------ }
{ Helpers }
{ ------------------------------------------------------------------ }
function TInterfaceTests.ParseSrc(const ASrc: string): TProgram;
var
L: TLexer;
P: TParser;
begin
L := TLexer.Create(ASrc);
P := TParser.Create(L);
try
Result := P.Parse;
finally
P.Free;
L.Free;
end;
end;
function TInterfaceTests.AnalyseSrc(const ASrc: string): TProgram;
var
SA: TSemanticAnalyser;
begin
Result := ParseSrc(ASrc);
SA := TSemanticAnalyser.Create;
try
SA.Analyse(Result);
finally
SA.Free;
end;
end;
function TInterfaceTests.GenIR(const ASrc: string): string;
var
CG: TCodeGenQBE;
Prog: TProgram;
begin
Prog := AnalyseSrc(ASrc);
CG := TCodeGenQBE.Create;
try
CG.Generate(Prog);
Result := CG.GetOutput;
finally
CG.Free;
Prog.Free;
end;
end;
procedure TInterfaceTests.AnalyseExpectError(const ASrc: string);
var
Prog: TProgram;
SA: TSemanticAnalyser;
begin
Prog := ParseSrc(ASrc);
SA := TSemanticAnalyser.Create;
try
try
SA.Analyse(Prog);
Fail('Expected ESemanticError but none was raised');
except
on E: ESemanticError do
{ expected };
end;
finally
SA.Free;
Prog.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Parser tests }
{ ------------------------------------------------------------------ }
procedure TInterfaceTests.TestParse_Interface_Empty;
var
Prog: TProgram;
TD: TTypeDecl;
begin
Prog := ParseSrc(SrcInterfaceEmpty);
try
AssertEquals('one type decl', 1, Prog.Block.TypeDecls.Count);
TD := TTypeDecl(Prog.Block.TypeDecls[0]);
AssertEquals('name is IFoo', 'IFoo', TD.Name);
AssertTrue('def is TInterfaceTypeDef', TD.Def is TInterfaceTypeDef);
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestParse_Interface_WithMethods;
var
Prog: TProgram;
ITD: TInterfaceTypeDef;
begin
Prog := ParseSrc(SrcInterfaceWithMethods);
try
ITD := TInterfaceTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
AssertEquals('two methods', 2, ITD.Methods.Count);
AssertEquals('first method DoIt', 'DoIt', TMethodDecl(ITD.Methods[0]).Name);
AssertEquals('second method GetVal','GetVal', TMethodDecl(ITD.Methods[1]).Name);
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestParse_Interface_WithParent;
var
Prog: TProgram;
Child: TInterfaceTypeDef;
begin
Prog := ParseSrc(SrcInterfaceWithParent);
try
Child := TInterfaceTypeDef(TTypeDecl(Prog.Block.TypeDecls[1]).Def);
AssertEquals('parent is IBase', 'IBase', Child.ParentName);
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestParse_Class_ImplementsInterface;
var
Prog: TProgram;
CD: TClassTypeDef;
begin
Prog := ParseSrc(SrcClassImplements);
try
{ type decl index 0 = IFoo, index 1 = TFoo }
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[1]).Def);
AssertEquals('one implements name', 1, CD.ImplementsNames.Count);
AssertEquals('implements IFoo', 'IFoo', CD.ImplementsNames[0]);
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestParse_Class_ImplementsMultiple;
var
Prog: TProgram;
CD: TClassTypeDef;
begin
Prog := ParseSrc(SrcClassImplementsMultiple);
try
{ type decl indices 0=IFoo, 1=IBar, 2=TFoo }
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[2]).Def);
AssertEquals('two implements names', 2, CD.ImplementsNames.Count);
AssertEquals('first is IFoo', 'IFoo', CD.ImplementsNames[0]);
AssertEquals('second is IBar', 'IBar', CD.ImplementsNames[1]);
finally
Prog.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Semantic tests }
{ ------------------------------------------------------------------ }
procedure TInterfaceTests.TestSemantic_Interface_Registered;
var
Prog: TProgram;
Sym: TSymbol;
begin
Prog := AnalyseSrc(SrcInterfaceWithMethods);
try
Sym := Prog.SymbolTable.Lookup('IFoo');
AssertNotNull('IFoo symbol exists', Sym);
AssertEquals('IFoo is skType', Ord(skType), Ord(Sym.Kind));
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestSemantic_Interface_IsInterfaceKind;
var
Prog: TProgram;
TD: TTypeDesc;
begin
Prog := AnalyseSrc(SrcInterfaceWithMethods);
try
TD := Prog.SymbolTable.FindType('IFoo');
AssertNotNull('IFoo type exists', TD);
AssertEquals('kind is tyInterface', Ord(tyInterface), Ord(TD.Kind));
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestSemantic_Interface_MethodsRegistered;
var
Prog: TProgram;
ITD: TInterfaceTypeDesc;
begin
Prog := AnalyseSrc(SrcInterfaceWithMethods);
try
ITD := TInterfaceTypeDesc(Prog.SymbolTable.FindType('IFoo'));
AssertTrue('has DoIt', ITD.HasMethod('DoIt'));
AssertTrue('has GetVal', ITD.HasMethod('GetVal'));
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestSemantic_ClassImplements_OK;
var
Prog: TProgram;
begin
Prog := AnalyseSrc(SrcClassImplements);
try
{ No exception = success }
AssertNotNull('prog not nil', Prog);
finally
Prog.Free;
end;
end;
procedure TInterfaceTests.TestSemantic_ClassImplements_MissingMethod_RaisesError;
begin
AnalyseExpectError(SrcClassMissingMethod);
end;
{ ------------------------------------------------------------------ }
{ Codegen tests }
{ ------------------------------------------------------------------ }
procedure TInterfaceTests.TestCodegen_Interface_TypeInfo_Emitted;
var
IR: string;
begin
IR := GenIR(SrcClassImplements);
AssertTrue('typeinfo_IFoo in IR', Pos('$typeinfo_IFoo', IR) > 0);
end;
procedure TInterfaceTests.TestCodegen_Class_Itab_Emitted;
var
IR: string;
begin
IR := GenIR(SrcClassImplements);
AssertTrue('itab_TFoo_IFoo in IR', Pos('$itab_TFoo_IFoo', IR) > 0);
end;
procedure TInterfaceTests.TestCodegen_Itab_ContainsMethodPointer;
var
IR: string;
ItabPos: Integer;
begin
IR := GenIR(SrcClassImplements);
ItabPos := Pos('$itab_TFoo_IFoo', IR);
AssertTrue('itab present', ItabPos > 0);
AssertTrue('TFoo_DoIt appears after itab label',
PosEx('$TFoo_DoIt', IR, ItabPos) > 0);
end;
procedure TInterfaceTests.TestCodegen_InterfaceVar_AllocsTwoSlots;
var
IR: string;
begin
IR := GenIR(SrcInterfaceVar);
AssertTrue('obj slot for F', Pos('_var_F_obj', IR) > 0);
AssertTrue('itab slot for F', Pos('_var_F_itab', IR) > 0);
end;
procedure TInterfaceTests.TestCodegen_InterfaceMethodCall_IndirectDispatch;
var
IR: string;
begin
IR := GenIR(SrcInterfaceVar);
{ Interface dispatch loads the itab pointer and calls indirectly }
AssertTrue('loads itab pointer', Pos('_var_F_itab', IR) > 0);
AssertTrue('indirect call via register', Pos('call %', IR) > 0);
end;
initialization
RegisterTest(TInterfaceTests);
end.

View file

@ -583,6 +583,9 @@ These are not open for reconsideration:
| Hello World compiles to a native binary via PasBuild
| Phase 1
| A non-trivial Linked-List program using TObject descendants.
| Phase 2
| A real library (generic list, hash map) compiles and passes tests
| Phase 3
@ -715,50 +718,57 @@ are required for correct generic type bounds).
| Item | Status | Notes
| Interface declaration parsing
| Pending
| Done
| `type IFoo = interface ... end;` — methods only, no fields;
`interface(IParent)` for inheritance; `['{GUID}']` attribute silently ignored
(zero-GUID design)
(zero-GUID design); `TInterfaceTypeDef` AST node; `tkIntf` token reused from
unit interface section (context disambiguates)
| TYPEID generation
| Pending
| `TYPEID = CRC32(UnitName + '.' + InterfaceName)` emitted as a data constant;
used for interface identity checks at runtime
| Done
| `data $typeinfo_IFoo = { l 0 }` — address of block IS the identity token;
emitted by `EmitInterfaceDefs`; used for future `is`/`as` interface checks
| Interface type in symbol table
| Pending
| `TInterfaceTypeDesc` alongside `TRecordTypeDesc`; method signature storage;
`implements` relationship on class types
| Done
| `TInterfaceTypeDesc` (tyInterface) alongside `TRecordTypeDesc`; methods stored
in declaration order (unsorted list for correct itab indexing);
`FImplements` non-owning list on `TRecordTypeDesc` tracks class→interface pairs;
`TObject` pre-registered as built-in root class
| Semantic: class implements interface
| Pending
| Verify all interface methods are present on the class with matching signatures;
record `implements` list on `TRecordTypeDesc` for codegen
| Done
| Parser parses `class(TBase, IFoo, IBar)` — first name = parent class, rest =
interfaces; semantic pass verifies all interface methods exist on the class;
`CheckTypesMatch` extended to allow class→interface assignment when class
implements that interface
| Interface vtable (itab) generation
| Pending
| Per `(class, interface)` pair: `data $itab_TFoo_IBar = { l $TFoo_Method, ... }`;
itab pointer stored as a hidden field alongside the object pointer in interface
variable slots
| Done
| `data $itab_TFoo_IFoo = { l $TFoo_DoIt, ... }` emitted by `EmitInterfaceDefs`;
method order matches interface declaration order; one itab per (class, interface)
pair
| Interface variable assignment
| Pending
| Assigning a class instance to an interface variable loads the matching itab;
ARC addref/release on interface variables (same RC string rules)
| Done
| Interface var = fat pointer: `%_var_F_obj` + `%_var_F_itab` (two alloc8 slots);
assignment stores obj pointer and itab address; ARC on interface refs deferred
(Phase 3 follow-up)
| Interface method dispatch
| Pending
| Load itab pointer from interface var slot; index into itab by method slot;
indirect `call %fptr(l obj, ...)` — similar to virtual method dispatch
| Done
| Load obj from `%_var_F_obj`, load itab from `%_var_F_itab`; index by method
slot (slot × 8); indirect `call %fptr(l obj)` — same pattern as virtual dispatch
| `as` cast to interface
| Pending
| Checked downcast: verify class implements interface via TYPEID lookup;
| Checked downcast: verify class implements interface via typeinfo pointer walk;
raise `EInvalidCast` on failure
| `is` test against interface
| Pending
| Runtime check: walk class `implements` list for matching TYPEID; return boolean
| Runtime check: walk class `implements` list for matching typeinfo pointer;
return boolean
| RTL: `IInterface` base
| Pending