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:
Graeme Geldenhuys 2026-04-21 23:15:15 +01:00
parent b34bfc574d
commit 80c2b21d6c
7 changed files with 636 additions and 19 deletions

View file

@ -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;

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View 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.