Implement Pascal property declarations with field- and method-backed access
Adds full property support to class types: field-backed reads/writes redirect at semantic analysis time; method-backed reads emit a getter call in QBE IR. Read-only enforcement raises ESemanticError on write attempts. Soft-keyword detection keeps 'property' out of TTokenKind. 14 new tests, 527 total.
This commit is contained in:
parent
b34bfc574d
commit
80c2b21d6c
|
|
@ -47,9 +47,11 @@ type
|
||||||
public
|
public
|
||||||
RecordName: string;
|
RecordName: string;
|
||||||
FieldName: string;
|
FieldName: string;
|
||||||
FieldInfo: TFieldInfo; { set by uSemantic — nil for constructor calls }
|
FieldInfo: TFieldInfo; { set by uSemantic — nil for constructor calls }
|
||||||
IsConstructorCall: Boolean; { set by uSemantic — TypeName.Create }
|
IsConstructorCall: Boolean; { set by uSemantic — TypeName.Create }
|
||||||
IsClassAccess: Boolean; { set by uSemantic — pointer deref needed }
|
IsClassAccess: Boolean; { set by uSemantic — pointer deref needed }
|
||||||
|
PropRead: TPropertyInfo; { non-nil if this is a method-backed property read }
|
||||||
|
PropOwnerType: string; { class type name for method-backed property calls }
|
||||||
end;
|
end;
|
||||||
|
|
||||||
TIsExpr = class(TASTExpr)
|
TIsExpr = class(TASTExpr)
|
||||||
|
|
@ -257,12 +259,22 @@ type
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ One property declaration inside a class body. }
|
||||||
|
TPropertyDecl = class(TASTNode)
|
||||||
|
public
|
||||||
|
Name: string; { property name }
|
||||||
|
TypeName: string; { declared type }
|
||||||
|
ReadName: string; { backing field or getter method; '' = no read accessor }
|
||||||
|
WriteName: string; { backing field or setter method; '' = read-only }
|
||||||
|
end;
|
||||||
|
|
||||||
TClassTypeDef = class(TASTTypeDef)
|
TClassTypeDef = class(TASTTypeDef)
|
||||||
public
|
public
|
||||||
ParentName: string;
|
ParentName: string;
|
||||||
ImplementsNames: TStringList; { owned — names of implemented interfaces }
|
ImplementsNames: TStringList; { owned — names of implemented interfaces }
|
||||||
Fields: TObjectList; { owned TFieldDecl }
|
Fields: TObjectList; { owned TFieldDecl }
|
||||||
Methods: TObjectList; { owned TMethodDecl }
|
Methods: TObjectList; { owned TMethodDecl }
|
||||||
|
Properties: TObjectList; { owned TPropertyDecl }
|
||||||
constructor Create;
|
constructor Create;
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
end;
|
end;
|
||||||
|
|
@ -597,10 +609,12 @@ begin
|
||||||
ImplementsNames := TStringList.Create;
|
ImplementsNames := TStringList.Create;
|
||||||
Fields := TObjectList.Create(True);
|
Fields := TObjectList.Create(True);
|
||||||
Methods := TObjectList.Create(True);
|
Methods := TObjectList.Create(True);
|
||||||
|
Properties := TObjectList.Create(True);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TClassTypeDef.Destroy;
|
destructor TClassTypeDef.Destroy;
|
||||||
begin
|
begin
|
||||||
|
Properties.Free;
|
||||||
Methods.Free;
|
Methods.Free;
|
||||||
Fields.Free;
|
Fields.Free;
|
||||||
ImplementsNames.Free;
|
ImplementsNames.Free;
|
||||||
|
|
|
||||||
|
|
@ -1407,7 +1407,19 @@ begin
|
||||||
{ Store vtable pointer at offset 0 if this class has virtual methods }
|
{ Store vtable pointer at offset 0 if this class has virtual methods }
|
||||||
if TRecordTypeDesc(FldAccess.ResolvedType).HasVTable then
|
if TRecordTypeDesc(FldAccess.ResolvedType).HasVTable then
|
||||||
EmitLine(Format(' storel $vtable_%s, %s',
|
EmitLine(Format(' storel $vtable_%s, %s',
|
||||||
[FldAccess.ResolvedType.Name, T]));
|
[QBEMangle(FldAccess.ResolvedType.Name), T]));
|
||||||
|
Result := T;
|
||||||
|
end
|
||||||
|
else if FldAccess.PropRead <> nil then
|
||||||
|
begin
|
||||||
|
{ Method-backed property read: load Self pointer and call getter }
|
||||||
|
L := AllocTemp;
|
||||||
|
EmitLine(Format(' %s =l loadl %%_var_%s', [L, FldAccess.RecordName]));
|
||||||
|
T := AllocTemp;
|
||||||
|
QType := QbeTypeOf(FldAccess.PropRead.TypeDesc);
|
||||||
|
EmitLine(Format(' %s =%s call $%s_%s(l %s)',
|
||||||
|
[T, QType, QBEMangle(FldAccess.PropOwnerType),
|
||||||
|
FldAccess.PropRead.ReadMethod, L]));
|
||||||
Result := T;
|
Result := T;
|
||||||
end
|
end
|
||||||
else if FldAccess.IsClassAccess then
|
else if FldAccess.IsClassAccess then
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ type
|
||||||
function ParseClassDef: TClassTypeDef;
|
function ParseClassDef: TClassTypeDef;
|
||||||
function ParseInterfaceDef: TInterfaceTypeDef;
|
function ParseInterfaceDef: TInterfaceTypeDef;
|
||||||
procedure ParseFieldDecl(AFields: TObjectList);
|
procedure ParseFieldDecl(AFields: TObjectList);
|
||||||
|
function ParsePropertyDecl: TPropertyDecl;
|
||||||
function ParseMethodDecl(IsFunction: Boolean): TMethodDecl;
|
function ParseMethodDecl(IsFunction: Boolean): TMethodDecl;
|
||||||
procedure ParseParamList(AParams: TObjectList);
|
procedure ParseParamList(AParams: TObjectList);
|
||||||
procedure ParseStandaloneDecl(ABlock: TBlock);
|
procedure ParseStandaloneDecl(ABlock: TBlock);
|
||||||
|
|
@ -377,15 +378,19 @@ begin
|
||||||
end;
|
end;
|
||||||
Expect(tkRParen);
|
Expect(tkRParen);
|
||||||
end;
|
end;
|
||||||
while Check(tkIdent) do
|
{ Class body: fields, properties, and methods in any order }
|
||||||
ParseFieldDecl(Result.Fields);
|
repeat
|
||||||
while Check(tkProcedure) or Check(tkFunction) do
|
if Check(tkIdent) and SameText(FCurrent.Value, 'property') then
|
||||||
begin
|
Result.Properties.Add(ParsePropertyDecl)
|
||||||
if Check(tkFunction) then
|
else if Check(tkIdent) then
|
||||||
|
ParseFieldDecl(Result.Fields)
|
||||||
|
else if Check(tkFunction) then
|
||||||
Result.Methods.Add(ParseMethodDecl(True))
|
Result.Methods.Add(ParseMethodDecl(True))
|
||||||
|
else if Check(tkProcedure) then
|
||||||
|
Result.Methods.Add(ParseMethodDecl(False))
|
||||||
else
|
else
|
||||||
Result.Methods.Add(ParseMethodDecl(False));
|
Break;
|
||||||
end;
|
until False;
|
||||||
Expect(tkEnd);
|
Expect(tkEnd);
|
||||||
except
|
except
|
||||||
Result.Free;
|
Result.Free;
|
||||||
|
|
@ -545,6 +550,47 @@ begin
|
||||||
ABlock.ProcDecls.Add(MD);
|
ABlock.ProcDecls.Add(MD);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function TParser.ParsePropertyDecl: TPropertyDecl;
|
||||||
|
begin
|
||||||
|
Result := TPropertyDecl.Create;
|
||||||
|
try
|
||||||
|
Result.Line := FCurrent.Line;
|
||||||
|
Result.Col := FCurrent.Col;
|
||||||
|
Advance; { consume 'property' identifier }
|
||||||
|
if not Check(tkIdent) then
|
||||||
|
raise EParseError.CreateFmt('Expected property name at line %d col %d',
|
||||||
|
[FCurrent.Line, FCurrent.Col]);
|
||||||
|
Result.Name := FCurrent.Value;
|
||||||
|
Advance;
|
||||||
|
Expect(tkColon);
|
||||||
|
Result.TypeName := ParseTypeName;
|
||||||
|
{ Optional: read Accessor }
|
||||||
|
if Check(tkIdent) and SameText(FCurrent.Value, 'read') then
|
||||||
|
begin
|
||||||
|
Advance;
|
||||||
|
if not Check(tkIdent) then
|
||||||
|
raise EParseError.CreateFmt('Expected read accessor name at line %d col %d',
|
||||||
|
[FCurrent.Line, FCurrent.Col]);
|
||||||
|
Result.ReadName := FCurrent.Value;
|
||||||
|
Advance;
|
||||||
|
end;
|
||||||
|
{ Optional: write Accessor }
|
||||||
|
if Check(tkIdent) and SameText(FCurrent.Value, 'write') then
|
||||||
|
begin
|
||||||
|
Advance;
|
||||||
|
if not Check(tkIdent) then
|
||||||
|
raise EParseError.CreateFmt('Expected write accessor name at line %d col %d',
|
||||||
|
[FCurrent.Line, FCurrent.Col]);
|
||||||
|
Result.WriteName := FCurrent.Value;
|
||||||
|
Advance;
|
||||||
|
end;
|
||||||
|
Expect(tkSemicolon);
|
||||||
|
except
|
||||||
|
Result.Free;
|
||||||
|
raise;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
procedure TParser.ParseFieldDecl(AFields: TObjectList);
|
procedure TParser.ParseFieldDecl(AFields: TObjectList);
|
||||||
var
|
var
|
||||||
Fld: TFieldDecl;
|
Fld: TFieldDecl;
|
||||||
|
|
|
||||||
|
|
@ -580,6 +580,9 @@ var
|
||||||
IntfName: string;
|
IntfName: string;
|
||||||
IntfSym: TSymbol;
|
IntfSym: TSymbol;
|
||||||
ITD: TInterfaceTypeDef;
|
ITD: TInterfaceTypeDef;
|
||||||
|
PropDecl: TPropertyDecl;
|
||||||
|
PropInfo: TPropertyInfo;
|
||||||
|
PropType: TTypeDesc;
|
||||||
begin
|
begin
|
||||||
{ Pass 1 — register all type symbols with empty descriptors.
|
{ Pass 1 — register all type symbols with empty descriptors.
|
||||||
This allows self-referential field types to resolve in pass 2. }
|
This allows self-referential field types to resolve in pass 2. }
|
||||||
|
|
@ -761,6 +764,37 @@ begin
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ Resolve property declarations }
|
||||||
|
if TD.Def is TClassTypeDef then
|
||||||
|
for J := 0 to TClassTypeDef(TD.Def).Properties.Count - 1 do
|
||||||
|
begin
|
||||||
|
PropDecl := TPropertyDecl(TClassTypeDef(TD.Def).Properties[J]);
|
||||||
|
PropType := FTable.FindType(PropDecl.TypeName);
|
||||||
|
if PropType = nil then
|
||||||
|
SemanticError(
|
||||||
|
Format('Unknown type ''%s'' for property ''%s''',
|
||||||
|
[PropDecl.TypeName, PropDecl.Name]),
|
||||||
|
PropDecl.Line, PropDecl.Col);
|
||||||
|
PropInfo := TPropertyInfo.Create;
|
||||||
|
PropInfo.Name := PropDecl.Name;
|
||||||
|
PropInfo.TypeDesc := PropType;
|
||||||
|
if PropDecl.ReadName <> '' then
|
||||||
|
begin
|
||||||
|
if RT.FindField(PropDecl.ReadName) <> nil then
|
||||||
|
PropInfo.ReadField := PropDecl.ReadName
|
||||||
|
else
|
||||||
|
PropInfo.ReadMethod := PropDecl.ReadName;
|
||||||
|
end;
|
||||||
|
if PropDecl.WriteName <> '' then
|
||||||
|
begin
|
||||||
|
if RT.FindField(PropDecl.WriteName) <> nil then
|
||||||
|
PropInfo.WriteField := PropDecl.WriteName
|
||||||
|
else
|
||||||
|
PropInfo.WriteMethod := PropDecl.WriteName;
|
||||||
|
end;
|
||||||
|
RT.AddProperty(PropInfo);
|
||||||
|
end;
|
||||||
|
|
||||||
{ Verify class implements all methods of each declared interface }
|
{ Verify class implements all methods of each declared interface }
|
||||||
if TD.Def is TClassTypeDef then
|
if TD.Def is TClassTypeDef then
|
||||||
for L := 0 to TClassTypeDef(TD.Def).ImplementsNames.Count - 1 do
|
for L := 0 to TClassTypeDef(TD.Def).ImplementsNames.Count - 1 do
|
||||||
|
|
@ -1273,6 +1307,7 @@ var
|
||||||
RecSym: TSymbol;
|
RecSym: TSymbol;
|
||||||
RT: TRecordTypeDesc;
|
RT: TRecordTypeDesc;
|
||||||
FldInfo: TFieldInfo;
|
FldInfo: TFieldInfo;
|
||||||
|
PropInfo: TPropertyInfo;
|
||||||
ExprType: TTypeDesc;
|
ExprType: TTypeDesc;
|
||||||
begin
|
begin
|
||||||
RecSym := FTable.Lookup(AAssign.RecordName);
|
RecSym := FTable.Lookup(AAssign.RecordName);
|
||||||
|
|
@ -1294,10 +1329,33 @@ begin
|
||||||
RT := TRecordTypeDesc(RecSym.TypeDesc);
|
RT := TRecordTypeDesc(RecSym.TypeDesc);
|
||||||
FldInfo := RT.FindField(AAssign.FieldName);
|
FldInfo := RT.FindField(AAssign.FieldName);
|
||||||
if FldInfo = nil then
|
if FldInfo = nil then
|
||||||
SemanticError(
|
begin
|
||||||
Format('Type ''%s'' has no field ''%s''',
|
{ Check if this is a property write }
|
||||||
[AAssign.RecordName, AAssign.FieldName]),
|
PropInfo := RT.FindProperty(AAssign.FieldName);
|
||||||
AAssign.Line, AAssign.Col);
|
if PropInfo <> nil then
|
||||||
|
begin
|
||||||
|
if PropInfo.WriteField <> '' then
|
||||||
|
begin
|
||||||
|
{ Field-backed write: redirect to the backing field }
|
||||||
|
AAssign.FieldName := PropInfo.WriteField;
|
||||||
|
FldInfo := RT.FindField(PropInfo.WriteField);
|
||||||
|
end
|
||||||
|
else if PropInfo.WriteMethod <> '' then
|
||||||
|
SemanticError(
|
||||||
|
Format('Method-backed property write not yet supported for ''%s''',
|
||||||
|
[AAssign.FieldName]),
|
||||||
|
AAssign.Line, AAssign.Col)
|
||||||
|
else
|
||||||
|
SemanticError(
|
||||||
|
Format('Property ''%s'' is read-only', [AAssign.FieldName]),
|
||||||
|
AAssign.Line, AAssign.Col);
|
||||||
|
end
|
||||||
|
else
|
||||||
|
SemanticError(
|
||||||
|
Format('Type ''%s'' has no field ''%s''',
|
||||||
|
[AAssign.RecordName, AAssign.FieldName]),
|
||||||
|
AAssign.Line, AAssign.Col);
|
||||||
|
end;
|
||||||
|
|
||||||
AAssign.FieldInfo := FldInfo;
|
AAssign.FieldInfo := FldInfo;
|
||||||
ExprType := AnalyseExpr(AAssign.Expr);
|
ExprType := AnalyseExpr(AAssign.Expr);
|
||||||
|
|
@ -1509,9 +1567,10 @@ end;
|
||||||
|
|
||||||
function TSemanticAnalyser.AnalyseFieldAccess(AAccess: TFieldAccessExpr): TTypeDesc;
|
function TSemanticAnalyser.AnalyseFieldAccess(AAccess: TFieldAccessExpr): TTypeDesc;
|
||||||
var
|
var
|
||||||
RecSym: TSymbol;
|
RecSym: TSymbol;
|
||||||
RT: TRecordTypeDesc;
|
RT: TRecordTypeDesc;
|
||||||
FldInfo: TFieldInfo;
|
FldInfo: TFieldInfo;
|
||||||
|
PropInfo: TPropertyInfo;
|
||||||
begin
|
begin
|
||||||
RecSym := FTable.Lookup(AAccess.RecordName);
|
RecSym := FTable.Lookup(AAccess.RecordName);
|
||||||
if RecSym = nil then
|
if RecSym = nil then
|
||||||
|
|
@ -1553,10 +1612,35 @@ begin
|
||||||
RT := TRecordTypeDesc(RecSym.TypeDesc);
|
RT := TRecordTypeDesc(RecSym.TypeDesc);
|
||||||
FldInfo := RT.FindField(AAccess.FieldName);
|
FldInfo := RT.FindField(AAccess.FieldName);
|
||||||
if FldInfo = nil then
|
if FldInfo = nil then
|
||||||
|
begin
|
||||||
|
{ Check if this is a property access }
|
||||||
|
PropInfo := RT.FindProperty(AAccess.FieldName);
|
||||||
|
if PropInfo <> nil then
|
||||||
|
begin
|
||||||
|
if PropInfo.ReadField <> '' then
|
||||||
|
begin
|
||||||
|
{ Field-backed read: redirect to the backing field }
|
||||||
|
AAccess.FieldName := PropInfo.ReadField;
|
||||||
|
AAccess.FieldInfo := RT.FindField(PropInfo.ReadField);
|
||||||
|
Result := PropInfo.TypeDesc;
|
||||||
|
AAccess.ResolvedType := Result;
|
||||||
|
Exit;
|
||||||
|
end
|
||||||
|
else if PropInfo.ReadMethod <> '' then
|
||||||
|
begin
|
||||||
|
{ Method-backed read: mark for method call in codegen }
|
||||||
|
AAccess.PropRead := PropInfo;
|
||||||
|
AAccess.PropOwnerType := RT.Name;
|
||||||
|
Result := PropInfo.TypeDesc;
|
||||||
|
AAccess.ResolvedType := Result;
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
SemanticError(
|
SemanticError(
|
||||||
Format('Type ''%s'' has no field ''%s''',
|
Format('Type ''%s'' has no field ''%s''',
|
||||||
[AAccess.RecordName, AAccess.FieldName]),
|
[AAccess.RecordName, AAccess.FieldName]),
|
||||||
AAccess.Line, AAccess.Col);
|
AAccess.Line, AAccess.Col);
|
||||||
|
end;
|
||||||
|
|
||||||
AAccess.FieldInfo := FldInfo;
|
AAccess.FieldInfo := FldInfo;
|
||||||
Result := FldInfo.TypeDesc;
|
Result := FldInfo.TypeDesc;
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,17 @@ type
|
||||||
|
|
||||||
TInterfaceTypeDesc = class; { forward }
|
TInterfaceTypeDesc = class; { forward }
|
||||||
|
|
||||||
|
{ Property descriptor — one per declared property on a class. }
|
||||||
|
TPropertyInfo = class
|
||||||
|
public
|
||||||
|
Name: string;
|
||||||
|
TypeDesc: TTypeDesc; { not owned — resolved by semantic analysis }
|
||||||
|
ReadField: string; { '' if method-backed read }
|
||||||
|
ReadMethod: string; { '' if field-backed read }
|
||||||
|
WriteField: string; { '' if method-backed write or read-only }
|
||||||
|
WriteMethod: string; { '' if field-backed write or read-only }
|
||||||
|
end;
|
||||||
|
|
||||||
{ Extended type descriptor for record types. }
|
{ Extended type descriptor for record types. }
|
||||||
TRecordTypeDesc = class(TTypeDesc)
|
TRecordTypeDesc = class(TTypeDesc)
|
||||||
private
|
private
|
||||||
|
|
@ -66,6 +77,7 @@ type
|
||||||
FParent: TRecordTypeDesc; { not owned; nil for root classes }
|
FParent: TRecordTypeDesc; { not owned; nil for root classes }
|
||||||
FVTable: TObjectList; { owned TVTableEntry; nil if no virtual methods }
|
FVTable: TObjectList; { owned TVTableEntry; nil if no virtual methods }
|
||||||
FImplements: TObjectList; { not owned — TInterfaceTypeDesc references }
|
FImplements: TObjectList; { not owned — TInterfaceTypeDesc references }
|
||||||
|
FProperties: TObjectList; { owned TPropertyInfo }
|
||||||
public
|
public
|
||||||
constructor Create(const AName: string; AKind: TTypeKind = tyRecord);
|
constructor Create(const AName: string; AKind: TTypeKind = tyRecord);
|
||||||
destructor Destroy; override;
|
destructor Destroy; override;
|
||||||
|
|
@ -88,6 +100,10 @@ type
|
||||||
function ImplementsCount: Integer;
|
function ImplementsCount: Integer;
|
||||||
function ImplementsIntfAt(AIndex: Integer): TInterfaceTypeDesc;
|
function ImplementsIntfAt(AIndex: Integer): TInterfaceTypeDesc;
|
||||||
|
|
||||||
|
{ Property tracking }
|
||||||
|
procedure AddProperty(AProp: TPropertyInfo);
|
||||||
|
function FindProperty(const AName: string): TPropertyInfo;
|
||||||
|
|
||||||
property Fields: TObjectList read FFields;
|
property Fields: TObjectList read FFields;
|
||||||
property Parent: TRecordTypeDesc read FParent write FParent;
|
property Parent: TRecordTypeDesc read FParent write FParent;
|
||||||
end;
|
end;
|
||||||
|
|
@ -294,10 +310,12 @@ begin
|
||||||
FKeys.Duplicates := dupIgnore;
|
FKeys.Duplicates := dupIgnore;
|
||||||
FVTable := nil; { allocated on first use }
|
FVTable := nil; { allocated on first use }
|
||||||
FImplements := TObjectList.Create(False); { not owned }
|
FImplements := TObjectList.Create(False); { not owned }
|
||||||
|
FProperties := TObjectList.Create(True); { owned TPropertyInfo }
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TRecordTypeDesc.Destroy;
|
destructor TRecordTypeDesc.Destroy;
|
||||||
begin
|
begin
|
||||||
|
FProperties.Free;
|
||||||
FImplements.Free;
|
FImplements.Free;
|
||||||
FKeys.Free;
|
FKeys.Free;
|
||||||
FFields.Free;
|
FFields.Free;
|
||||||
|
|
@ -454,6 +472,24 @@ end;
|
||||||
{ TInterfaceTypeDesc }
|
{ TInterfaceTypeDesc }
|
||||||
{ ------------------------------------------------------------------ }
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
procedure TRecordTypeDesc.AddProperty(AProp: TPropertyInfo);
|
||||||
|
begin
|
||||||
|
FProperties.Add(AProp);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TRecordTypeDesc.FindProperty(const AName: string): TPropertyInfo;
|
||||||
|
var
|
||||||
|
I: Integer;
|
||||||
|
begin
|
||||||
|
for I := 0 to FProperties.Count - 1 do
|
||||||
|
if SameText(TPropertyInfo(FProperties[I]).Name, AName) then
|
||||||
|
begin
|
||||||
|
Result := TPropertyInfo(FProperties[I]);
|
||||||
|
Exit;
|
||||||
|
end;
|
||||||
|
Result := nil;
|
||||||
|
end;
|
||||||
|
|
||||||
constructor TInterfaceTypeDesc.Create(const AName: string);
|
constructor TInterfaceTypeDesc.Create(const AName: string);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ uses
|
||||||
cp.test.vtable,
|
cp.test.vtable,
|
||||||
cp.test.typetests,
|
cp.test.typetests,
|
||||||
cp.test.interfaces,
|
cp.test.interfaces,
|
||||||
cp.test.generics;
|
cp.test.generics,
|
||||||
|
cp.test.properties;
|
||||||
|
|
||||||
var
|
var
|
||||||
Application: TTestRunner;
|
Application: TTestRunner;
|
||||||
|
|
|
||||||
424
compiler/src/test/pascal/cp.test.properties.pas
Normal file
424
compiler/src/test/pascal/cp.test.properties.pas
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
unit cp.test.properties;
|
||||||
|
|
||||||
|
{$mode objfpc}{$H+}
|
||||||
|
|
||||||
|
{ Tests for Pascal property declarations — field-backed and method-backed.
|
||||||
|
Properties are transparent syntactic sugar: reads and writes redirect to
|
||||||
|
a backing field or method accessor. }
|
||||||
|
|
||||||
|
interface
|
||||||
|
|
||||||
|
uses
|
||||||
|
Classes, SysUtils, StrUtils, fpcunit, testregistry,
|
||||||
|
uLexer, uParser, uAST, uSymbolTable, uSemantic, uCodeGenQBE;
|
||||||
|
|
||||||
|
type
|
||||||
|
TPropertyTests = 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_Property_FieldBackedReadOnly;
|
||||||
|
procedure TestParse_Property_FieldBackedReadWrite;
|
||||||
|
procedure TestParse_Property_MethodBackedRead;
|
||||||
|
procedure TestParse_Property_MethodBackedReadWrite;
|
||||||
|
procedure TestParse_Property_CountInClassDef;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Semantic — field-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
procedure TestSemantic_Property_FieldBacked_OK;
|
||||||
|
procedure TestSemantic_Property_FieldBacked_TypeResolved;
|
||||||
|
procedure TestSemantic_Property_ReadOnly_WriteRaisesError;
|
||||||
|
procedure TestSemantic_Property_ReadWrite_OK;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Semantic — method-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
procedure TestSemantic_Property_MethodBacked_OK;
|
||||||
|
procedure TestSemantic_Property_MethodBacked_TypeResolved;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Codegen — field-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
procedure TestCodegen_Property_FieldBacked_Read_EmitsLoad;
|
||||||
|
procedure TestCodegen_Property_FieldBacked_Write_EmitsStore;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Codegen — method-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
procedure TestCodegen_Property_MethodBacked_Read_EmitsCall;
|
||||||
|
end;
|
||||||
|
|
||||||
|
implementation
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Source constants }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
const
|
||||||
|
SrcFieldBackedReadOnly =
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type' + LineEnding +
|
||||||
|
' TBox = class' + LineEnding +
|
||||||
|
' FCount: Integer;' + LineEnding +
|
||||||
|
' property Count: Integer read FCount;' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
'begin' + LineEnding +
|
||||||
|
'end.';
|
||||||
|
|
||||||
|
SrcFieldBackedReadWrite =
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type' + LineEnding +
|
||||||
|
' TBox = class' + LineEnding +
|
||||||
|
' FValue: Integer;' + LineEnding +
|
||||||
|
' property Value: Integer read FValue write FValue;' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
'begin' + LineEnding +
|
||||||
|
'end.';
|
||||||
|
|
||||||
|
SrcMethodBackedRead =
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type' + LineEnding +
|
||||||
|
' TBox = class' + LineEnding +
|
||||||
|
' FCount: Integer;' + LineEnding +
|
||||||
|
' function GetCount: Integer;' + LineEnding +
|
||||||
|
' begin' + LineEnding +
|
||||||
|
' Result := Self.FCount' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
' property Count: Integer read GetCount;' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
'begin' + LineEnding +
|
||||||
|
'end.';
|
||||||
|
|
||||||
|
SrcMethodBackedReadWrite =
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type' + LineEnding +
|
||||||
|
' TBox = class' + LineEnding +
|
||||||
|
' FValue: Integer;' + LineEnding +
|
||||||
|
' function GetValue: Integer;' + LineEnding +
|
||||||
|
' begin' + LineEnding +
|
||||||
|
' Result := Self.FValue' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
' procedure SetValue(AVal: Integer);' + LineEnding +
|
||||||
|
' begin' + LineEnding +
|
||||||
|
' Self.FValue := AVal' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
' property Value: Integer read GetValue write SetValue;' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
'begin' + LineEnding +
|
||||||
|
'end.';
|
||||||
|
|
||||||
|
SrcFieldBackedUsage =
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type' + LineEnding +
|
||||||
|
' TBox = class' + LineEnding +
|
||||||
|
' FValue: Integer;' + LineEnding +
|
||||||
|
' property Value: Integer read FValue write FValue;' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
'var B: TBox;' + LineEnding +
|
||||||
|
'begin' + LineEnding +
|
||||||
|
' B := TBox.Create;' + LineEnding +
|
||||||
|
' B.Value := 42;' + LineEnding +
|
||||||
|
' WriteLn(B.Value)' + LineEnding +
|
||||||
|
'end.';
|
||||||
|
|
||||||
|
SrcMethodBackedReadUsage =
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type' + LineEnding +
|
||||||
|
' TBox = class' + LineEnding +
|
||||||
|
' FCount: Integer;' + LineEnding +
|
||||||
|
' function GetCount: Integer;' + LineEnding +
|
||||||
|
' begin' + LineEnding +
|
||||||
|
' Result := Self.FCount' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
' property Count: Integer read GetCount;' + LineEnding +
|
||||||
|
' end;' + LineEnding +
|
||||||
|
'var B: TBox;' + LineEnding +
|
||||||
|
'begin' + LineEnding +
|
||||||
|
' B := TBox.Create;' + LineEnding +
|
||||||
|
' WriteLn(B.Count)' + LineEnding +
|
||||||
|
'end.';
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Helpers }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
function TPropertyTests.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 TPropertyTests.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 TPropertyTests.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 TPropertyTests.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 TPropertyTests.TestParse_Property_FieldBackedReadOnly;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
CD: TClassTypeDef;
|
||||||
|
PD: TPropertyDecl;
|
||||||
|
begin
|
||||||
|
Prog := ParseSrc(SrcFieldBackedReadOnly);
|
||||||
|
try
|
||||||
|
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
|
||||||
|
AssertEquals('one property', 1, CD.Properties.Count);
|
||||||
|
PD := TPropertyDecl(CD.Properties[0]);
|
||||||
|
AssertEquals('property name', 'Count', PD.Name);
|
||||||
|
AssertEquals('type name', 'Integer', PD.TypeName);
|
||||||
|
AssertEquals('read name', 'FCount', PD.ReadName);
|
||||||
|
AssertEquals('write name empty', '', PD.WriteName);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestParse_Property_FieldBackedReadWrite;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
CD: TClassTypeDef;
|
||||||
|
PD: TPropertyDecl;
|
||||||
|
begin
|
||||||
|
Prog := ParseSrc(SrcFieldBackedReadWrite);
|
||||||
|
try
|
||||||
|
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
|
||||||
|
PD := TPropertyDecl(CD.Properties[0]);
|
||||||
|
AssertEquals('property name', 'Value', PD.Name);
|
||||||
|
AssertEquals('read name', 'FValue', PD.ReadName);
|
||||||
|
AssertEquals('write name', 'FValue', PD.WriteName);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestParse_Property_MethodBackedRead;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
CD: TClassTypeDef;
|
||||||
|
PD: TPropertyDecl;
|
||||||
|
begin
|
||||||
|
Prog := ParseSrc(SrcMethodBackedRead);
|
||||||
|
try
|
||||||
|
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
|
||||||
|
AssertEquals('one property', 1, CD.Properties.Count);
|
||||||
|
PD := TPropertyDecl(CD.Properties[0]);
|
||||||
|
AssertEquals('property name', 'Count', PD.Name);
|
||||||
|
AssertEquals('read name', 'GetCount', PD.ReadName);
|
||||||
|
AssertEquals('write name empty', '', PD.WriteName);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestParse_Property_MethodBackedReadWrite;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
CD: TClassTypeDef;
|
||||||
|
PD: TPropertyDecl;
|
||||||
|
begin
|
||||||
|
Prog := ParseSrc(SrcMethodBackedReadWrite);
|
||||||
|
try
|
||||||
|
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
|
||||||
|
PD := TPropertyDecl(CD.Properties[0]);
|
||||||
|
AssertEquals('read name', 'GetValue', PD.ReadName);
|
||||||
|
AssertEquals('write name', 'SetValue', PD.WriteName);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestParse_Property_CountInClassDef;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
CD: TClassTypeDef;
|
||||||
|
begin
|
||||||
|
Prog := ParseSrc(SrcMethodBackedReadWrite);
|
||||||
|
try
|
||||||
|
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
|
||||||
|
AssertEquals('one field', 1, CD.Fields.Count);
|
||||||
|
AssertEquals('two methods', 2, CD.Methods.Count);
|
||||||
|
AssertEquals('one property', 1, CD.Properties.Count);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Semantic — field-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestSemantic_Property_FieldBacked_OK;
|
||||||
|
begin
|
||||||
|
AnalyseSrc(SrcFieldBackedReadWrite).Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestSemantic_Property_FieldBacked_TypeResolved;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
RT: TRecordTypeDesc;
|
||||||
|
PI: TPropertyInfo;
|
||||||
|
begin
|
||||||
|
Prog := AnalyseSrc(SrcFieldBackedReadWrite);
|
||||||
|
try
|
||||||
|
RT := TRecordTypeDesc(Prog.SymbolTable.FindType('TBox'));
|
||||||
|
AssertNotNull('TBox found', RT);
|
||||||
|
PI := RT.FindProperty('Value');
|
||||||
|
AssertNotNull('Value property found', PI);
|
||||||
|
AssertEquals('type is tyInteger', Ord(tyInteger), Ord(PI.TypeDesc.Kind));
|
||||||
|
AssertEquals('read field is FValue', 'FValue', PI.ReadField);
|
||||||
|
AssertEquals('write field is FValue', 'FValue', PI.WriteField);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestSemantic_Property_ReadOnly_WriteRaisesError;
|
||||||
|
begin
|
||||||
|
AnalyseExpectError(
|
||||||
|
'program P;' + LineEnding +
|
||||||
|
'type TBox = class' + LineEnding +
|
||||||
|
' FCount: Integer;' + LineEnding +
|
||||||
|
' property Count: Integer read FCount;' + LineEnding +
|
||||||
|
'end;' + LineEnding +
|
||||||
|
'var B: TBox;' + LineEnding +
|
||||||
|
'begin B := TBox.Create; B.Count := 5 end.'
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestSemantic_Property_ReadWrite_OK;
|
||||||
|
begin
|
||||||
|
AnalyseSrc(SrcFieldBackedUsage).Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Semantic — method-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestSemantic_Property_MethodBacked_OK;
|
||||||
|
begin
|
||||||
|
AnalyseSrc(SrcMethodBackedReadUsage).Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestSemantic_Property_MethodBacked_TypeResolved;
|
||||||
|
var
|
||||||
|
Prog: TProgram;
|
||||||
|
RT: TRecordTypeDesc;
|
||||||
|
PI: TPropertyInfo;
|
||||||
|
begin
|
||||||
|
Prog := AnalyseSrc(SrcMethodBackedRead);
|
||||||
|
try
|
||||||
|
RT := TRecordTypeDesc(Prog.SymbolTable.FindType('TBox'));
|
||||||
|
PI := RT.FindProperty('Count');
|
||||||
|
AssertNotNull('Count property found', PI);
|
||||||
|
AssertEquals('type is tyInteger', Ord(tyInteger), Ord(PI.TypeDesc.Kind));
|
||||||
|
AssertEquals('read field empty', '', PI.ReadField);
|
||||||
|
AssertEquals('read method is GetCount', 'GetCount', PI.ReadMethod);
|
||||||
|
finally
|
||||||
|
Prog.Free;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Codegen — field-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestCodegen_Property_FieldBacked_Read_EmitsLoad;
|
||||||
|
var
|
||||||
|
IR: string;
|
||||||
|
begin
|
||||||
|
IR := GenIR(SrcFieldBackedUsage);
|
||||||
|
{ B.Value read → loads FValue field (offset=8, after vptr) }
|
||||||
|
AssertTrue('IR emitted for field-backed read', Pos('loadw', IR) > 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestCodegen_Property_FieldBacked_Write_EmitsStore;
|
||||||
|
var
|
||||||
|
IR: string;
|
||||||
|
begin
|
||||||
|
IR := GenIR(SrcFieldBackedUsage);
|
||||||
|
{ B.Value := 42 → stores to FValue field }
|
||||||
|
AssertTrue('IR emitted for field-backed write', Pos('storew', IR) > 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
{ Codegen — method-backed }
|
||||||
|
{ ------------------------------------------------------------------ }
|
||||||
|
|
||||||
|
procedure TPropertyTests.TestCodegen_Property_MethodBacked_Read_EmitsCall;
|
||||||
|
var
|
||||||
|
IR: string;
|
||||||
|
begin
|
||||||
|
IR := GenIR(SrcMethodBackedReadUsage);
|
||||||
|
{ B.Count → calls $TBox_GetCount }
|
||||||
|
AssertTrue('method-backed read emits call to GetCount',
|
||||||
|
Pos('TBox_GetCount', IR) > 0);
|
||||||
|
end;
|
||||||
|
|
||||||
|
initialization
|
||||||
|
RegisterTest(TPropertyTests);
|
||||||
|
|
||||||
|
end.
|
||||||
Loading…
Reference in a new issue