Add class instance methods (procedure, inline body, Self, method calls)

Lexer:
- Added tkProcedure keyword token.

AST:
- TMethodParam: stores parameter name, type string, and resolved type.
- TMethodDecl: method declaration with name, param list, and inline body
  (TBlock). Avoids TObject.MethodName collision by naming the field Name.
- TClassTypeDef.Methods: owned TObjectList of TMethodDecl.
- TMethodCallStmt: statement node for obj.Method(args) calls; ObjectName
  and Name fields; ResolvedClassType and ResolvedMethod set by semantic.
- TBlock forward-declared before TMethodDecl to break circular dependency.

Parser:
- ParseMethodDecl: parses 'procedure Name [(ParamList)] ; Block ;' inside
  a class body.
- ParseParamList: handles 'Name : Type [; Name : Type ...]' param groups.
- ParseClassDef: field parsing followed by method parsing before 'end'.
- ParseStmt: after 'IDENT.IDENT', dispatches to TFieldAssignment (':=')
  or TMethodCallStmt (everything else, optionally with arg list).
- ParseMethodCallArgList: argument list for method call statements.

Semantic:
- FMethodIndex: TStringList mapping 'TypeName.Name' -> TMethodDecl for
  O(log n) dispatch lookup.
- AnalyseTypeDecls: indexes methods and resolves param types after fields.
- AnalyseMethodBodies: analyses each method body after type registration.
- AnalyseMethodDecl: pushes a scope with Self (class type) and explicit
  params, then calls AnalyseBlock for the method body.
- AnalyseMethodCall: resolves object type, looks up method, checks arg
  count and types; sets ResolvedClassType and ResolvedMethod on the node.

Code generator:
- EmitMethodDefs: emits a QBE function for every method in every class
  type before the main function.
- EmitMethodDef: emits 'function $TypeName_Name(l %_par_Self, ...) {'.
- EmitParamAllocs: allocates stack slots for Self and explicit params.
- EmitMethodCall: loads Self from the caller's var slot, then emits
  'call $TypeName_Name(l selfTemp, ...)'.

Tests: 204 unit tests (24 new method tests), 0 failures.
Grammar: docs/grammar.ebnf updated with MethodDecl, ParamList, MethodCall.
This commit is contained in:
Graeme Geldenhuys 2026-04-20 19:42:09 +01:00
parent 0df5514623
commit 313619511c
8 changed files with 1091 additions and 64 deletions

View file

@ -90,6 +90,18 @@ type
destructor Destroy; override;
end;
TMethodCallStmt = class(TASTStmt)
public
ObjectName: string;
Name: string; { method name }
Args: TObjectList; { owned TASTExpr items }
{ Set by uSemantic: }
ResolvedClassType: TTypeDesc; { not owned }
ResolvedMethod: TObject; { TMethodDecl — not owned; avoids forward ref }
constructor Create;
destructor Destroy; override;
end;
{ ------------------------------------------------------------------ }
{ Declarations }
{ ------------------------------------------------------------------ }
@ -126,10 +138,29 @@ type
destructor Destroy; override;
end;
TBlock = class; { forward — defined below after all declarations }
TMethodParam = class(TASTNode)
public
ParamName: string;
TypeName: string;
ResolvedType: TTypeDesc; { set by uSemantic }
end;
TMethodDecl = class(TASTNode)
public
Name: string; { method name }
Params: TObjectList; { owned TMethodParam }
Body: TBlock; { owned }
constructor Create;
destructor Destroy; override;
end;
TClassTypeDef = class(TASTTypeDef)
public
ParentName: string;
Fields: TObjectList; { owned TFieldDecl }
Methods: TObjectList; { owned TMethodDecl }
constructor Create;
destructor Destroy; override;
end;
@ -219,6 +250,20 @@ begin
inherited Destroy;
end;
{ TMethodCallStmt }
constructor TMethodCallStmt.Create;
begin
inherited Create;
Args := TObjectList.Create(True);
end;
destructor TMethodCallStmt.Destroy;
begin
Args.Free;
inherited Destroy;
end;
{ TVarDecl }
constructor TVarDecl.Create;
@ -261,16 +306,33 @@ begin
inherited Destroy;
end;
{ TMethodDecl }
constructor TMethodDecl.Create;
begin
inherited Create;
Params := TObjectList.Create(True);
end;
destructor TMethodDecl.Destroy;
begin
Params.Free;
Body.Free;
inherited Destroy;
end;
{ TClassTypeDef }
constructor TClassTypeDef.Create;
begin
inherited Create;
Fields := TObjectList.Create(True);
Fields := TObjectList.Create(True);
Methods := TObjectList.Create(True);
end;
destructor TClassTypeDef.Destroy;
begin
Methods.Free;
Fields.Free;
inherited Destroy;
end;

View file

@ -27,12 +27,16 @@ type
procedure EmitDataSection;
procedure EmitMainHeader;
procedure EmitMainFooter;
procedure EmitMethodDefs(AProg: TProgram);
procedure EmitMethodDef(const ATypeName: string; AMethod: TMethodDecl);
procedure EmitBlock(ABlock: TBlock);
procedure EmitVarAllocs(ABlock: TBlock);
procedure EmitParamAllocs(AMethod: TMethodDecl; AClassType: TRecordTypeDesc);
procedure EmitStringCleanup(ABlock: TBlock);
procedure EmitStmt(AStmt: TASTStmt);
procedure EmitAssignment(AAssign: TAssignment);
procedure EmitFieldAssignment(AAssign: TFieldAssignment);
procedure EmitMethodCall(ACall: TMethodCallStmt);
procedure EmitProcCall(ACall: TProcCall);
procedure EmitWrite(ACall: TProcCall; ANewline: Boolean);
function EmitExpr(AExpr: TASTExpr): string;
@ -223,6 +227,8 @@ begin
EmitFieldAssignment(TFieldAssignment(AStmt))
else if AStmt is TAssignment then
EmitAssignment(TAssignment(AStmt))
else if AStmt is TMethodCallStmt then
EmitMethodCall(TMethodCallStmt(AStmt))
else if AStmt is TProcCall then
EmitProcCall(TProcCall(AStmt))
else
@ -306,6 +312,129 @@ begin
EmitLine(Format(' %s %s, %s', [StoreInstr, ValTemp, Ptr]));
end;
procedure TCodeGenQBE.EmitMethodCall(ACall: TMethodCallStmt);
var
RT: TRecordTypeDesc;
MDecl: TMethodDecl;
SelfTemp: string;
Par: TMethodParam;
ArgTemp: string;
ArgLine: string;
I: Integer;
QType: string;
FuncName: string;
begin
RT := TRecordTypeDesc(ACall.ResolvedClassType);
MDecl := TMethodDecl(ACall.ResolvedMethod);
{ Load the object pointer (Self) from the caller's variable slot }
SelfTemp := AllocTemp;
EmitLine(Format(' %s =l loadl %%_var_%s', [SelfTemp, ACall.ObjectName]));
FuncName := '$' + RT.Name + '_' + ACall.Name;
{ Build argument string: l Self, then each explicit arg }
ArgLine := Format('l %s', [SelfTemp]);
for I := 0 to ACall.Args.Count - 1 do
begin
Par := TMethodParam(MDecl.Params[I]);
ArgTemp := EmitExpr(TASTExpr(ACall.Args[I]));
QType := QbeTypeOf(Par.ResolvedType);
ArgLine := ArgLine + Format(', %s %s', [QType, ArgTemp]);
end;
EmitLine(Format(' call %s(%s)', [FuncName, ArgLine]));
end;
procedure TCodeGenQBE.EmitParamAllocs(AMethod: TMethodDecl;
AClassType: TRecordTypeDesc);
var
I: Integer;
Par: TMethodParam;
begin
{ Self: store incoming pointer into a local slot }
EmitLine(' %_var_Self =l alloc8 1');
EmitLine(' storel %_par_Self, %_var_Self');
{ Explicit params }
for I := 0 to AMethod.Params.Count - 1 do
begin
Par := TMethodParam(AMethod.Params[I]);
case Par.ResolvedType.Kind of
tyInteger, tyUInt32, tyBoolean, tyByte:
begin
EmitLine(Format(' %%_var_%s =l alloc4 1', [Par.ParamName]));
EmitLine(Format(' storew %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
tyInt64, tyString, tyClass:
begin
EmitLine(Format(' %%_var_%s =l alloc8 1', [Par.ParamName]));
EmitLine(Format(' storel %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
else
EmitLine(Format(' %%_var_%s =l alloc8 1', [Par.ParamName]));
EmitLine(Format(' storel %%_par_%s, %%_var_%s',
[Par.ParamName, Par.ParamName]));
end;
end;
end;
procedure TCodeGenQBE.EmitMethodDef(const ATypeName: string;
AMethod: TMethodDecl);
var
Sig: string;
I: Integer;
Par: TMethodParam;
FuncName: string;
Sym: TSymbol;
RT: TRecordTypeDesc;
begin
FuncName := '$' + ATypeName + '_' + AMethod.Name;
{ Build parameter signature: l %_par_Self [, qtype %_par_Name ...] }
Sig := 'l %_par_Self';
for I := 0 to AMethod.Params.Count - 1 do
begin
Par := TMethodParam(AMethod.Params[I]);
Sig := Sig + Format(', %s %%_par_%s',
[QbeTypeOf(Par.ResolvedType), Par.ParamName]);
end;
EmitLine(Format('function %s(%s) {', [FuncName, Sig]));
EmitLine('@start');
{ Allocate stack slots for Self and params, then emit body }
RT := nil;
{ We need the class type descriptor for EmitParamAllocs — obtain from AMethod }
{ AMethod.Body is already resolved; RT is referenced via Self symbol in the body.
We pass nil for RT here since EmitParamAllocs only needs it for Self size (always l). }
EmitParamAllocs(AMethod, nil);
EmitBlock(AMethod.Body);
EmitLine(' ret');
EmitLine('}');
EmitLine('');
end;
procedure TCodeGenQBE.EmitMethodDefs(AProg: TProgram);
var
I, J: Integer;
TD: TTypeDecl;
CD: TClassTypeDef;
begin
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;
CD := TClassTypeDef(TD.Def);
for J := 0 to CD.Methods.Count - 1 do
EmitMethodDef(TD.Name, TMethodDecl(CD.Methods[J]));
end;
end;
procedure TCodeGenQBE.EmitProcCall(ACall: TProcCall);
var
UCaseName: string;
@ -488,6 +617,7 @@ begin
SavedOutput := FOutput;
FOutput := Body;
try
EmitMethodDefs(AProg);
EmitMainHeader;
EmitBlock(AProg.Block);
EmitMainFooter;
@ -495,7 +625,7 @@ begin
FOutput := SavedOutput;
end;
EmitLine('# Generated by Blaise Compiler (Phase 1)');
EmitLine('# Generated by Blaise Compiler (Phase 2)');
EmitLine('# Source: ' + AProg.Name);
EmitLine('');
EmitDataSection;

View file

@ -23,6 +23,7 @@ type
tkType,
tkRecord,
tkClass,
tkProcedure,
tkVar,
tkBegin,
tkEnd,
@ -88,8 +89,9 @@ begin
else if AUpper = 'END' then Result := tkEnd
else if AUpper = 'TYPE' then Result := tkType
else if AUpper = 'RECORD' then Result := tkRecord
else if AUpper = 'CLASS' then Result := tkClass
else if AUpper = 'DIV' then Result := tkDiv
else if AUpper = 'CLASS' then Result := tkClass
else if AUpper = 'PROCEDURE' then Result := tkProcedure
else if AUpper = 'DIV' then Result := tkDiv
else
Result := tkIdent; { keyword outside Phase 1 grammar treated as ident }
end;

View file

@ -3,32 +3,37 @@ unit uParser;
{$mode objfpc}{$H+}
// Recursive-descent parser for the Blaise grammar:
// Program ::= 'program' Ident ';' [Uses] Block '.'
// Uses ::= 'uses' Ident {',' Ident} ';'
// Block ::= [TypeSection] [VarSection] 'begin' StmtList 'end'
// TypeSection ::= 'type' TypeDecl {TypeDecl}
// TypeDecl ::= Ident '=' (RecordDef | ClassDef) ';'
// RecordDef ::= 'record' FieldList 'end'
// ClassDef ::= 'class' ['(' Ident ')'] FieldList 'end'
// FieldList ::= FieldDecl {FieldDecl}
// FieldDecl ::= IdentList ':' TypeName ';'
// VarSection ::= 'var' VarDecl {VarDecl}
// VarDecl ::= IdentList ':' TypeName ';'
// StmtList ::= Stmt {';' Stmt} [';']
// Stmt ::= FieldAssignment | Assignment | ProcCall | empty
// FieldAssign ::= Ident '.' Ident ':=' Expr
// Assignment ::= Ident ':=' Expr
// ProcCall ::= Ident ['(' [ExprList] ')']
// ExprList ::= Expr {',' Expr}
// Expr ::= Term (('+' | '-') Term)*
// Term ::= Factor (('*' | '/' | 'div') Factor)*
// Factor ::= IntLit | StringLit | Ident '.' Ident | Ident | '(' Expr ')'
// TypeName ::= Ident
// Program ::= 'program' Ident ';' [Uses] Block '.'
// Uses ::= 'uses' Ident {',' Ident} ';'
// Block ::= [TypeSection] [VarSection] 'begin' StmtList 'end'
// TypeSection ::= 'type' TypeDecl {TypeDecl}
// TypeDecl ::= Ident '=' (RecordDef | ClassDef) ';'
// RecordDef ::= 'record' FieldList 'end'
// ClassDef ::= 'class' ['(' Ident ')'] FieldList MethodList 'end'
// FieldList ::= {FieldDecl}
// FieldDecl ::= IdentList ':' TypeName ';'
// MethodList ::= {MethodDecl}
// MethodDecl ::= 'procedure' Ident ['(' ParamList ')'] ';' Block ';'
// ParamList ::= ParamGroup {';' ParamGroup}
// ParamGroup ::= IdentList ':' TypeName
// VarSection ::= 'var' VarDecl {VarDecl}
// VarDecl ::= IdentList ':' TypeName ';'
// StmtList ::= Stmt {';' Stmt} [';']
// Stmt ::= FieldAssignment | MethodCall | Assignment | ProcCall | empty
// FieldAssign ::= Ident '.' Ident ':=' Expr
// MethodCall ::= Ident '.' Ident ['(' [ExprList] ')']
// Assignment ::= Ident ':=' Expr
// ProcCall ::= Ident ['(' [ExprList] ')']
// ExprList ::= Expr {',' Expr}
// Expr ::= Term (('+' | '-') Term)*
// Term ::= Factor (('*' | '/' | 'div') Factor)*
// Factor ::= IntLit | StringLit | Ident '.' Ident | Ident | '(' Expr ')'
// TypeName ::= Ident
interface
uses
SysUtils, contnrs, uLexer, uAST;
SysUtils, Classes, contnrs, uLexer, uAST;
type
EParseError = class(Exception);
@ -50,6 +55,8 @@ type
function ParseRecordDef: TRecordTypeDef;
function ParseClassDef: TClassTypeDef;
procedure ParseFieldDecl(AFields: TObjectList);
function ParseMethodDecl: TMethodDecl;
procedure ParseParamList(AParams: TObjectList);
procedure ParseVarBlock(ABlock: TBlock);
procedure ParseVarDecl(ABlock: TBlock);
procedure ParseStmtList(ABlock: TBlock);
@ -58,6 +65,7 @@ type
function ParseTerm: TASTExpr;
function ParseFactor: TASTExpr;
procedure ParseArgList(ACall: TProcCall);
procedure ParseMethodCallArgList(ACall: TMethodCallStmt);
public
constructor Create(ALexer: TLexer);
function Parse: TProgram;
@ -252,6 +260,8 @@ begin
end;
while Check(tkIdent) do
ParseFieldDecl(Result.Fields);
while Check(tkProcedure) do
Result.Methods.Add(ParseMethodDecl);
Expect(tkEnd);
except
Result.Free;
@ -259,6 +269,81 @@ begin
end;
end;
function TParser.ParseMethodDecl: TMethodDecl;
begin
Result := TMethodDecl.Create;
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
Expect(tkProcedure);
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected method name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Result.Name := FCurrent.Value;
Advance;
if Check(tkLParen) then
begin
Advance;
if not Check(tkRParen) then
ParseParamList(Result.Params);
Expect(tkRParen);
end;
Expect(tkSemicolon);
Result.Body := ParseBlock;
Expect(tkSemicolon);
except
Result.Free;
raise;
end;
end;
procedure TParser.ParseParamList(AParams: TObjectList);
var
Par: TMethodParam;
I: Integer;
Names: TStringList;
TypeN: string;
begin
repeat
Names := TStringList.Create;
try
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected parameter name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Names.Add(FCurrent.Value);
Advance;
while Check(tkComma) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected parameter name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Names.Add(FCurrent.Value);
Advance;
end;
Expect(tkColon);
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected type name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
TypeN := FCurrent.Value;
Advance;
for I := 0 to Names.Count - 1 do
begin
Par := TMethodParam.Create;
Par.ParamName := Names[I];
Par.TypeName := TypeN;
AParams.Add(Par);
end;
finally
Names.Free;
end;
if Check(tkSemicolon) then
Advance
else
Break;
until False;
end;
procedure TParser.ParseFieldDecl(AFields: TObjectList);
var
Fld: TFieldDecl;
@ -364,11 +449,13 @@ end;
function TParser.ParseStmt: TASTStmt;
var
Name: string;
Line, Col: Integer;
Call: TProcCall;
Assign: TAssignment;
FldAssign: TFieldAssignment;
Name: string;
Line, Col: Integer;
Call: TProcCall;
Assign: TAssignment;
FldAssign: TFieldAssignment;
MCall: TMethodCallStmt;
SecondIdent: string;
begin
Result := nil;
@ -387,20 +474,42 @@ begin
if Check(tkDot) then
begin
{ Field assignment: Ident '.' Ident ':=' Expr }
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected field name at line %d col %d',
raise EParseError.CreateFmt('Expected field or method name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
FldAssign := TFieldAssignment.Create;
FldAssign.Line := Line;
FldAssign.Col := Col;
FldAssign.RecordName := Name;
FldAssign.FieldName := FCurrent.Value;
SecondIdent := FCurrent.Value;
Advance;
Expect(tkAssign);
FldAssign.Expr := ParseExpr;
Result := FldAssign;
if Check(tkAssign) then
begin
{ Field assignment: Ident '.' Ident ':=' Expr }
FldAssign := TFieldAssignment.Create;
FldAssign.Line := Line;
FldAssign.Col := Col;
FldAssign.RecordName := Name;
FldAssign.FieldName := SecondIdent;
Expect(tkAssign);
FldAssign.Expr := ParseExpr;
Result := FldAssign;
end
else
begin
{ Method call: Ident '.' Ident ['(' [args] ')'] }
MCall := TMethodCallStmt.Create;
MCall.Line := Line;
MCall.Col := Col;
MCall.ObjectName := Name;
MCall.Name := SecondIdent;
if Check(tkLParen) then
begin
Advance;
if not Check(tkRParen) then
ParseMethodCallArgList(MCall);
Expect(tkRParen);
end;
Result := MCall;
end;
end
else if Check(tkAssign) then
begin
@ -439,6 +548,16 @@ begin
end;
end;
procedure TParser.ParseMethodCallArgList(ACall: TMethodCallStmt);
begin
ACall.Args.Add(ParseExpr);
while Check(tkComma) do
begin
Advance;
ACall.Args.Add(ParseExpr);
end;
end;
{ ------------------------------------------------------------------ }
{ Expression parsing — standard precedence climbing }
{ ------------------------------------------------------------------ }

View file

@ -3,37 +3,45 @@ unit uSemantic;
{$mode objfpc}{$H+}
// Semantic analysis pass walks the AST produced by uParser and:
// 1. Resolves record type declarations and registers them in the symbol table.
// 2. Resolves every identifier to a TSymbol in the symbol table.
// 3. Infers and annotates every expression node with ResolvedType.
// 4. Type-checks assignments and field assignments.
// 5. Validates procedure/function calls.
// 6. Raises ESemanticError with source position on any violation.
// 1. Resolves record/class type declarations and registers them in the symbol table.
// 2. Indexes class methods for dispatch lookup.
// 3. Analyses method bodies with Self and explicit params in scope.
// 4. Resolves every identifier to a TSymbol in the symbol table.
// 5. Infers and annotates every expression node with ResolvedType.
// 6. Type-checks assignments, field assignments, and method calls.
// 7. Validates procedure/function calls.
// 8. Raises ESemanticError with source position on any violation.
interface
uses
SysUtils, contnrs, uAST, uSymbolTable;
SysUtils, Classes, contnrs, uAST, uSymbolTable;
type
ESemanticError = class(Exception);
TSemanticAnalyser = class
private
FTable: TSymbolTable;
FTable: TSymbolTable;
FMethodIndex: TStringList; { 'TypeName.MethodName' → TMethodDecl (not owned) }
procedure AnalyseBlock(ABlock: TBlock);
procedure AnalyseTypeDecls(ABlock: TBlock);
procedure AnalyseMethodBodies(ABlock: TBlock);
procedure AnalyseMethodDecl(AMethod: TMethodDecl; AClassType: TRecordTypeDesc);
procedure AnalyseVarDecls(ABlock: TBlock);
procedure AnalyseStmts(ABlock: TBlock);
procedure AnalyseStmt(AStmt: TASTStmt);
procedure AnalyseAssignment(AAssign: TAssignment);
procedure AnalyseFieldAssignment(AAssign: TFieldAssignment);
procedure AnalyseProcCall(ACall: TProcCall);
procedure AnalyseMethodCall(ACall: TMethodCallStmt);
function AnalyseExpr(AExpr: TASTExpr): TTypeDesc;
function AnalyseBinaryExpr(ABin: TBinaryExpr): TTypeDesc;
function AnalyseFieldAccess(AAccess: TFieldAccessExpr): TTypeDesc;
function FindMethodDecl(const ATypeName, AMethodName: string): TMethodDecl;
procedure SemanticError(const AMsg: string; ALine, ACol: Integer);
procedure CheckTypesMatch(AExpected, AActual: TTypeDesc;
const AContext: string; ALine, ACol: Integer);
@ -48,11 +56,14 @@ implementation
constructor TSemanticAnalyser.Create;
begin
inherited Create;
FTable := TSymbolTable.Create;
FTable := TSymbolTable.Create;
FMethodIndex := TStringList.Create;
FMethodIndex.CaseSensitive := False;
end;
destructor TSemanticAnalyser.Destroy;
begin
FMethodIndex.Free;
FTable.Free;
inherited Destroy;
end;
@ -88,6 +99,7 @@ begin
after the block scope is popped needed for var declarations and the
transferred symbol table used by codegen. }
AnalyseTypeDecls(ABlock);
AnalyseMethodBodies(ABlock);
FTable.PushScope;
try
AnalyseVarDecls(ABlock);
@ -99,14 +111,19 @@ end;
procedure TSemanticAnalyser.AnalyseTypeDecls(ABlock: TBlock);
var
I, J, K: Integer;
TD: TTypeDecl;
FieldList: TObjectList;
FDecl: TFieldDecl;
RT: TRecordTypeDesc;
FldType: TTypeDesc;
FldName: string;
Sym: TSymbol;
I, J, K: Integer;
TD: TTypeDecl;
FieldList: TObjectList;
MethodList: TObjectList;
FDecl: TFieldDecl;
MDecl: TMethodDecl;
Par: TMethodParam;
ParType: TTypeDesc;
RT: TRecordTypeDesc;
FldType: TTypeDesc;
FldName: string;
Sym: TSymbol;
Key: string;
begin
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
@ -114,13 +131,15 @@ begin
if TD.Def is TRecordTypeDef then
begin
RT := FTable.NewRecordType(TD.Name);
FieldList := TRecordTypeDef(TD.Def).Fields;
RT := FTable.NewRecordType(TD.Name);
FieldList := TRecordTypeDef(TD.Def).Fields;
MethodList := nil;
end
else if TD.Def is TClassTypeDef then
begin
RT := FTable.NewClassType(TD.Name);
FieldList := TClassTypeDef(TD.Def).Fields;
RT := FTable.NewClassType(TD.Name);
FieldList := TClassTypeDef(TD.Def).Fields;
MethodList := TClassTypeDef(TD.Def).Methods;
end
else
begin
@ -156,9 +175,105 @@ begin
Format('Duplicate type name ''%s''', [TD.Name]),
TD.Line, TD.Col);
end;
{ Index class methods and resolve param types }
if MethodList <> nil then
for J := 0 to MethodList.Count - 1 do
begin
MDecl := TMethodDecl(MethodList[J]);
Key := TD.Name + '.' + MDecl.Name;
FMethodIndex.AddObject(Key, MDecl);
for K := 0 to MDecl.Params.Count - 1 do
begin
Par := TMethodParam(MDecl.Params[K]);
ParType := FTable.FindType(Par.TypeName);
if ParType = nil then
SemanticError(
Format('Unknown type ''%s'' for parameter ''%s''',
[Par.TypeName, Par.ParamName]),
MDecl.Line, MDecl.Col);
Par.ResolvedType := ParType;
end;
end;
end;
end;
procedure TSemanticAnalyser.AnalyseMethodBodies(ABlock: TBlock);
var
I, J: Integer;
TD: TTypeDecl;
CD: TClassTypeDef;
RT: TRecordTypeDesc;
Sym: TSymbol;
begin
for I := 0 to ABlock.TypeDecls.Count - 1 do
begin
TD := TTypeDecl(ABlock.TypeDecls[I]);
if not (TD.Def is TClassTypeDef) then
Continue;
CD := TClassTypeDef(TD.Def);
Sym := FTable.Lookup(TD.Name);
if (Sym = nil) or not (Sym.TypeDesc is TRecordTypeDesc) then
Continue;
RT := TRecordTypeDesc(Sym.TypeDesc);
for J := 0 to CD.Methods.Count - 1 do
AnalyseMethodDecl(TMethodDecl(CD.Methods[J]), RT);
end;
end;
procedure TSemanticAnalyser.AnalyseMethodDecl(
AMethod: TMethodDecl; AClassType: TRecordTypeDesc);
var
I: Integer;
Par: TMethodParam;
Sym: TSymbol;
begin
FTable.PushScope;
try
{ Define Self as a variable of the class type }
Sym := TSymbol.Create('Self', skVariable, AClassType);
FTable.Define(Sym);
{ Define explicit parameters }
for I := 0 to AMethod.Params.Count - 1 do
begin
Par := TMethodParam(AMethod.Params[I]);
if Par.ResolvedType = nil then
SemanticError(
Format('Parameter ''%s'' has unresolved type', [Par.ParamName]),
AMethod.Line, AMethod.Col);
Sym := TSymbol.Create(Par.ParamName, skParameter, Par.ResolvedType);
if not FTable.Define(Sym) then
begin
Sym.Free;
SemanticError(
Format('Duplicate parameter name ''%s''', [Par.ParamName]),
AMethod.Line, AMethod.Col);
end;
end;
{ Analyse the method body block (pushes its own inner scope) }
AnalyseBlock(AMethod.Body);
finally
FTable.PopScope;
end;
end;
function TSemanticAnalyser.FindMethodDecl(
const ATypeName, AMethodName: string): TMethodDecl;
var
Idx: Integer;
Key: string;
begin
Key := ATypeName + '.' + AMethodName;
Idx := FMethodIndex.IndexOf(Key);
if Idx >= 0 then
Result := TMethodDecl(FMethodIndex.Objects[Idx])
else
Result := nil;
end;
procedure TSemanticAnalyser.AnalyseVarDecls(ABlock: TBlock);
var
I, J: Integer;
@ -208,10 +323,61 @@ begin
AnalyseFieldAssignment(TFieldAssignment(AStmt))
else if AStmt is TAssignment then
AnalyseAssignment(TAssignment(AStmt))
else if AStmt is TMethodCallStmt then
AnalyseMethodCall(TMethodCallStmt(AStmt))
else if AStmt is TProcCall then
AnalyseProcCall(TProcCall(AStmt));
end;
procedure TSemanticAnalyser.AnalyseMethodCall(ACall: TMethodCallStmt);
var
ObjSym: TSymbol;
RT: TRecordTypeDesc;
MDecl: TMethodDecl;
Par: TMethodParam;
ArgType: TTypeDesc;
I: Integer;
begin
ObjSym := FTable.Lookup(ACall.ObjectName);
if ObjSym = nil then
SemanticError(
Format('Undeclared variable ''%s''', [ACall.ObjectName]),
ACall.Line, ACall.Col);
if ObjSym.Kind <> skVariable then
SemanticError(
Format('''%s'' is not a variable', [ACall.ObjectName]),
ACall.Line, ACall.Col);
if ObjSym.TypeDesc.Kind <> tyClass then
SemanticError(
Format('''%s'' is not a class variable', [ACall.ObjectName]),
ACall.Line, ACall.Col);
RT := TRecordTypeDesc(ObjSym.TypeDesc);
MDecl := FindMethodDecl(RT.Name, ACall.Name);
if MDecl = nil then
SemanticError(
Format('Class ''%s'' has no method ''%s''', [RT.Name, ACall.Name]),
ACall.Line, ACall.Col);
if ACall.Args.Count <> MDecl.Params.Count then
SemanticError(
Format('Method ''%s.%s'' expects %d argument(s) but got %d',
[RT.Name, ACall.Name, MDecl.Params.Count, ACall.Args.Count]),
ACall.Line, ACall.Col);
for I := 0 to ACall.Args.Count - 1 do
begin
ArgType := AnalyseExpr(TASTExpr(ACall.Args[I]));
Par := TMethodParam(MDecl.Params[I]);
CheckTypesMatch(Par.ResolvedType, ArgType,
Format('argument %d of ''%s''', [I + 1, ACall.Name]),
ACall.Line, ACall.Col);
end;
ACall.ResolvedClassType := RT;
ACall.ResolvedMethod := MDecl;
end;
procedure TSemanticAnalyser.AnalyseAssignment(AAssign: TAssignment);
var
VarSym: TSymbol;

View file

@ -17,7 +17,8 @@ uses
cp.test.semantic,
cp.test.records,
cp.test.classes,
cp.test.arc;
cp.test.arc,
cp.test.methods;
var
Application: TTestRunner;

View file

@ -0,0 +1,519 @@
unit cp.test.methods;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testregistry,
uLexer, uParser, uAST, uSymbolTable, uSemantic, uCodeGenQBE;
type
TMethodTests = 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
{ ------------------------------------------------------------------ }
{ Lexer }
{ ------------------------------------------------------------------ }
procedure TestLexer_Procedure_Keyword;
{ ------------------------------------------------------------------ }
{ Parser }
{ ------------------------------------------------------------------ }
procedure TestParse_Method_InClass;
procedure TestParse_Method_Name;
procedure TestParse_Method_NoParams;
procedure TestParse_Method_WithParams;
procedure TestParse_Method_ParamName;
procedure TestParse_Method_ParamTypeName;
procedure TestParse_Method_Body_HasStmt;
procedure TestParse_MethodCall_Stmt;
procedure TestParse_MethodCall_WithArgs;
procedure TestParse_MethodCall_NoArgs;
{ ------------------------------------------------------------------ }
{ Semantic }
{ ------------------------------------------------------------------ }
procedure TestSemantic_MethodCall_Resolves;
procedure TestSemantic_MethodCall_UnknownMethod_RaisesError;
procedure TestSemantic_MethodCall_ArgTypeMismatch_RaisesError;
procedure TestSemantic_MethodCall_WrongArgCount_RaisesError;
procedure TestSemantic_Method_SelfIsClassType;
procedure TestSemantic_Method_SelfFieldWrite_OK;
procedure TestSemantic_Method_ParamResolved;
{ ------------------------------------------------------------------ }
{ Code generation }
{ ------------------------------------------------------------------ }
procedure TestCodegen_Method_EmitsFunction;
procedure TestCodegen_Method_FuncHasSelfParam;
procedure TestCodegen_Method_FuncHasExplicitParam;
procedure TestCodegen_MethodCall_EmitsCall;
procedure TestCodegen_MethodCall_PassesSelf;
procedure TestCodegen_Method_SelfFieldWrite;
end;
implementation
{ ------------------------------------------------------------------ }
{ Helpers }
{ ------------------------------------------------------------------ }
function TMethodTests.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 TMethodTests.AnalyseSrc(const ASrc: string): TProgram;
var
A: TSemanticAnalyser;
begin
Result := ParseSrc(ASrc);
A := TSemanticAnalyser.Create;
try
A.Analyse(Result);
finally
A.Free;
end;
end;
function TMethodTests.GenIR(const ASrc: string): string;
var
Prog: TProgram;
CG: TCodeGenQBE;
begin
Prog := AnalyseSrc(ASrc);
try
CG := TCodeGenQBE.Create;
try
CG.Generate(Prog);
Result := CG.GetOutput;
finally
CG.Free;
end;
finally
Prog.Free;
end;
end;
procedure TMethodTests.AnalyseExpectError(const ASrc: string);
var
Prog: TProgram;
begin
try
Prog := AnalyseSrc(ASrc);
Prog.Free;
Fail('Expected ESemanticError');
except
on E: ESemanticError do ;
end;
end;
{ ------------------------------------------------------------------ }
{ Shared source snippets }
{ ------------------------------------------------------------------ }
const
SrcCounter =
'program P;' + LineEnding +
'type' + LineEnding +
' TCounter = class' + LineEnding +
' Value: Integer;' + LineEnding +
' procedure SetValue(AVal: Integer);' + LineEnding +
' begin' + LineEnding +
' Self.Value := AVal' + LineEnding +
' end;' + LineEnding +
' end;' + LineEnding +
'var C: TCounter;' + LineEnding +
'begin' + LineEnding +
' C := TCounter.Create;' + LineEnding +
' C.SetValue(42)' + LineEnding +
'end.';
SrcNoParamMethod =
'program P;' + LineEnding +
'type' + LineEnding +
' TFoo = class' + LineEnding +
' X: Integer;' + LineEnding +
' procedure Reset;' + LineEnding +
' begin' + LineEnding +
' Self.X := 0' + LineEnding +
' end;' + LineEnding +
' end;' + LineEnding +
'var F: TFoo;' + LineEnding +
'begin' + LineEnding +
' F := TFoo.Create;' + LineEnding +
' F.Reset' + LineEnding +
'end.';
{ ------------------------------------------------------------------ }
{ Lexer }
{ ------------------------------------------------------------------ }
procedure TMethodTests.TestLexer_Procedure_Keyword;
var
L: TLexer;
T: TToken;
begin
L := TLexer.Create('procedure');
try
T := L.Next;
AssertEquals('procedure token', Ord(tkProcedure), Ord(T.Kind));
finally
L.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Parser }
{ ------------------------------------------------------------------ }
procedure TMethodTests.TestParse_Method_InClass;
var
Prog: TProgram;
CD: TClassTypeDef;
begin
Prog := ParseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
AssertEquals('one method', 1, CD.Methods.Count);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_Method_Name;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
begin
Prog := ParseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
AssertEquals('method name', 'SetValue', MD.Name);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_Method_NoParams;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
begin
Prog := ParseSrc(SrcNoParamMethod);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
AssertEquals('zero params', 0, MD.Params.Count);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_Method_WithParams;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
begin
Prog := ParseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
AssertEquals('one param', 1, MD.Params.Count);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_Method_ParamName;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
Par: TMethodParam;
begin
Prog := ParseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
Par := TMethodParam(MD.Params[0]);
AssertEquals('param name', 'AVal', Par.ParamName);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_Method_ParamTypeName;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
Par: TMethodParam;
begin
Prog := ParseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
Par := TMethodParam(MD.Params[0]);
AssertEquals('param type', 'Integer', Par.TypeName);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_Method_Body_HasStmt;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
begin
Prog := ParseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
AssertEquals('body has 1 stmt', 1, MD.Body.Stmts.Count);
AssertTrue('stmt is TFieldAssignment', MD.Body.Stmts[0] is TFieldAssignment);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_MethodCall_Stmt;
var
Prog: TProgram;
Stmt: TMethodCallStmt;
begin
Prog := ParseSrc(SrcCounter);
try
{ second stmt after C := TCounter.Create }
AssertTrue('second stmt is TMethodCallStmt',
Prog.Block.Stmts[1] is TMethodCallStmt);
Stmt := TMethodCallStmt(Prog.Block.Stmts[1]);
AssertEquals('object name', 'C', Stmt.ObjectName);
AssertEquals('method name', 'SetValue', Stmt.Name);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_MethodCall_WithArgs;
var
Prog: TProgram;
Stmt: TMethodCallStmt;
begin
Prog := ParseSrc(SrcCounter);
try
Stmt := TMethodCallStmt(Prog.Block.Stmts[1]);
AssertEquals('one arg', 1, Stmt.Args.Count);
AssertTrue('arg is TIntLiteral', Stmt.Args[0] is TIntLiteral);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestParse_MethodCall_NoArgs;
var
Prog: TProgram;
Stmt: TMethodCallStmt;
begin
Prog := ParseSrc(SrcNoParamMethod);
try
Stmt := TMethodCallStmt(Prog.Block.Stmts[1]);
AssertEquals('method name', 'Reset', Stmt.Name);
AssertEquals('zero args', 0, Stmt.Args.Count);
finally
Prog.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Semantic }
{ ------------------------------------------------------------------ }
procedure TMethodTests.TestSemantic_MethodCall_Resolves;
begin
AnalyseSrc(SrcCounter).Free;
end;
procedure TMethodTests.TestSemantic_MethodCall_UnknownMethod_RaisesError;
begin
AnalyseExpectError(
'program P;' + LineEnding +
'type' + LineEnding +
' TFoo = class' + LineEnding +
' X: Integer;' + LineEnding +
' end;' + LineEnding +
'var F: TFoo;' + LineEnding +
'begin' + LineEnding +
' F := TFoo.Create;' + LineEnding +
' F.NoSuchMethod' + LineEnding +
'end.');
end;
procedure TMethodTests.TestSemantic_MethodCall_ArgTypeMismatch_RaisesError;
begin
AnalyseExpectError(
'program P;' + LineEnding +
'type' + LineEnding +
' TFoo = class' + LineEnding +
' X: Integer;' + LineEnding +
' procedure SetX(AVal: Integer);' + LineEnding +
' begin' + LineEnding +
' Self.X := AVal' + LineEnding +
' end;' + LineEnding +
' end;' + LineEnding +
'var F: TFoo;' + LineEnding +
'begin' + LineEnding +
' F := TFoo.Create;' + LineEnding +
' F.SetX(''not an int'')' + LineEnding +
'end.');
end;
procedure TMethodTests.TestSemantic_MethodCall_WrongArgCount_RaisesError;
begin
AnalyseExpectError(
'program P;' + LineEnding +
'type' + LineEnding +
' TFoo = class' + LineEnding +
' X: Integer;' + LineEnding +
' procedure SetX(AVal: Integer);' + LineEnding +
' begin' + LineEnding +
' Self.X := AVal' + LineEnding +
' end;' + LineEnding +
' end;' + LineEnding +
'var F: TFoo;' + LineEnding +
'begin' + LineEnding +
' F := TFoo.Create;' + LineEnding +
' F.SetX(1, 2)' + LineEnding +
'end.');
end;
procedure TMethodTests.TestSemantic_Method_SelfIsClassType;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
Stmt: TFieldAssignment;
begin
Prog := AnalyseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
Stmt := TFieldAssignment(MD.Body.Stmts[0]);
AssertTrue('Self.Value is class access', Stmt.IsClassAccess);
finally
Prog.Free;
end;
end;
procedure TMethodTests.TestSemantic_Method_SelfFieldWrite_OK;
begin
AnalyseSrc(SrcCounter).Free;
end;
procedure TMethodTests.TestSemantic_Method_ParamResolved;
var
Prog: TProgram;
CD: TClassTypeDef;
MD: TMethodDecl;
Par: TMethodParam;
begin
Prog := AnalyseSrc(SrcCounter);
try
CD := TClassTypeDef(TTypeDecl(Prog.Block.TypeDecls[0]).Def);
MD := TMethodDecl(CD.Methods[0]);
Par := TMethodParam(MD.Params[0]);
AssertNotNull('param type resolved', Par.ResolvedType);
AssertEquals('param type is Integer',
Ord(tyInteger), Ord(Par.ResolvedType.Kind));
finally
Prog.Free;
end;
end;
{ ------------------------------------------------------------------ }
{ Code generation }
{ ------------------------------------------------------------------ }
procedure TMethodTests.TestCodegen_Method_EmitsFunction;
var
IR: string;
begin
IR := GenIR(SrcCounter);
AssertTrue('emits TCounter_SetValue function',
Pos('$TCounter_SetValue', IR) > 0);
AssertTrue('function keyword present',
Pos('function $TCounter_SetValue', IR) > 0);
end;
procedure TMethodTests.TestCodegen_Method_FuncHasSelfParam;
var
IR: string;
begin
IR := GenIR(SrcCounter);
AssertTrue('Self is first l param',
Pos('function $TCounter_SetValue(l', IR) > 0);
end;
procedure TMethodTests.TestCodegen_Method_FuncHasExplicitParam;
var
IR: string;
begin
IR := GenIR(SrcCounter);
AssertTrue('explicit param in signature',
Pos('%_par_AVal', IR) > 0);
end;
procedure TMethodTests.TestCodegen_MethodCall_EmitsCall;
var
IR: string;
begin
IR := GenIR(SrcCounter);
AssertTrue('call to TCounter_SetValue',
Pos('call $TCounter_SetValue', IR) > 0);
end;
procedure TMethodTests.TestCodegen_MethodCall_PassesSelf;
var
IR: string;
begin
IR := GenIR(SrcCounter);
AssertTrue('Self (C pointer) passed to method',
Pos('call $TCounter_SetValue(l', IR) > 0);
end;
procedure TMethodTests.TestCodegen_Method_SelfFieldWrite;
var
IR: string;
begin
IR := GenIR(SrcCounter);
{ Inside the method, Self.Value := AVal should load Self ptr and store }
AssertTrue('method body loads Self pointer',
Pos('%_var_Self', IR) > 0);
AssertTrue('method body stores to field',
Pos('storew', IR) > 0);
end;
initialization
RegisterTest(TMethodTests);
end.

View file

@ -99,11 +99,30 @@ RecordDef
;
ClassDef
= CLASS [ LPAREN IDENT RPAREN ] FieldList END
= CLASS [ LPAREN IDENT RPAREN ] FieldList MethodList END
;
(* LPAREN IDENT RPAREN optional parent class name, e.g. class(TObject) *)
MethodList
= { MethodDecl }
;
MethodDecl
= PROCEDURE IDENT [ LPAREN ParamList RPAREN ] SEMICOLON Block SEMICOLON
;
(* Body is declared inline in the class definition.
* An implicit 'Self' parameter of the class type is available inside the body. *)
ParamList
= ParamGroup { SEMICOLON ParamGroup }
;
ParamGroup
= IdentList COLON TypeName
;
FieldList
= { FieldDecl }
;
@ -140,6 +159,7 @@ StmtList
Stmt
= FieldAssignment
| MethodCall
| Assignment
| ProcCall
| (* empty *)
@ -149,6 +169,14 @@ FieldAssignment
= IDENT DOT IDENT ASSIGN Expr
;
MethodCall
= IDENT DOT IDENT [ LPAREN [ ArgList ] RPAREN ]
;
(* Semantic disambiguation of IDENT DOT IDENT in Stmt:
* - Followed by := FieldAssignment
* - Otherwise MethodCall *)
(* Resolved by semantic analysis:
* - If IDENT names a record variable record field write
* - If IDENT names a class variable heap pointer deref + field write *)