Phase 1 bootstrap: Blaise compiler skeleton

Renames the project from 'Clean Pascal' to Blaise (after Blaise Pascal).
Adds the Phase 1 compiler pipeline with TDD test suite:

- uPasTokeniser: general Pascal tokeniser (ported from fpGUI IDE)
- uLexer: compiler-specific adapter — filters whitespace/comments,
  maps to TTokenKind, unescapes string literals
- uAST: typed AST node hierarchy (TProgram, TBlock, TBinaryExpr, etc.)
- uParser: recursive-descent parser for the Phase 1 BNF grammar
- uCodeGenQBE: QBE IR emitter — WriteLn/Write built-ins, integer
  variables, arithmetic, string literals
- Blaise.pas: compiler driver — parses flags, shells to qbe+cc
- FPCUnit test suites for lexer, parser, and code generator
This commit is contained in:
Graeme Geldenhuys 2026-04-20 15:35:50 +01:00
parent fd79a7db98
commit 3fce5f86de
18 changed files with 3098 additions and 15 deletions

View file

@ -1,4 +1,4 @@
= Clean Pascal Compiler
= Blaise
:toc: left
:toclevels: 2
:icons: font
@ -138,23 +138,23 @@ pasbuild test
[source,shell]
----
pasbuild compile -m clean-pascal-compiler
pasbuild compile -m blaise-compiler
----
=== Running the compiler
Once built, the compiler binary is at `compiler/target/cleanpascal`.
Once built, the compiler binary is at `compiler/target/blaise`.
[source,shell]
----
# Compile a single file
compiler/target/cleanpascal --source Hello.pas --output Hello
compiler/target/blaise --source Hello.pas --output Hello
# Compile via project.xml
compiler/target/cleanpascal --project project.xml --config debug --output myapp
compiler/target/blaise --project project.xml --config debug --output myapp
# Emit QBE IR (useful for debugging the compiler itself)
compiler/target/cleanpascal --source Hello.pas --emit-ir
compiler/target/blaise --source Hello.pas --emit-ir
----
== What Is Dropped From Classic Pascal

View file

@ -1,12 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>clean-pascal-compiler</name>
<name>blaise-compiler</name>
<!-- version inherited from root aggregator -->
<build>
<packaging>application</packaging>
<mainSource>CleanPascal.pas</mainSource>
<executableName>cleanpascal</executableName>
<mainSource>Blaise.pas</mainSource>
<executableName>blaise</executableName>
</build>
<test>

View file

@ -0,0 +1,248 @@
program Blaise;
{$mode objfpc}{$H+}
{ Blaise Compiler — main entry point.
Usage:
cleanpascal --source Hello.pas --output hello
cleanpascal --source Hello.pas --emit-ir
cleanpascal --source Hello.pas --output hello --target linux-x86_64
The compiler generates QBE IR, then shells out to qbe + cc to produce
the final binary. With --emit-ir, the IR is written to stdout and no
binary is produced.
}
uses
SysUtils, Classes, Process,
uLexer, uParser, uCodeGenQBE;
const
Version = '0.1.0-alpha';
CompilerName = 'Blaise';
procedure PrintUsage;
begin
WriteLn('Blaise Compiler v', Version);
WriteLn('');
WriteLn('Usage:');
WriteLn(' cleanpascal --source <file.pas> --output <binary>');
WriteLn(' cleanpascal --source <file.pas> --emit-ir');
WriteLn('');
WriteLn('Flags:');
WriteLn(' --source <path> Pascal source file');
WriteLn(' --output <path> Output binary path');
WriteLn(' --target <id> linux-x86_64 (default), macos-arm64');
WriteLn(' --emit-ir Print QBE IR to stdout and exit');
end;
function ParseArgs(
out SourceFile: string;
out OutputFile: string;
out EmitIR: Boolean): Boolean;
var
I: Integer;
Arg: string;
begin
Result := False;
SourceFile := '';
OutputFile := '';
EmitIR := False;
I := 1;
while I <= ParamCount do
begin
Arg := ParamStr(I);
if (Arg = '--source') and (I < ParamCount) then
begin
Inc(I);
SourceFile := ParamStr(I);
end
else if (Arg = '--output') and (I < ParamCount) then
begin
Inc(I);
OutputFile := ParamStr(I);
end
else if Arg = '--emit-ir' then
EmitIR := True
else if Arg = '--target' then
Inc(I) { consume next arg — target is not used in Phase 1 }
else if (Arg = '--help') or (Arg = '-h') then
begin
PrintUsage;
Halt(0);
end
else
begin
WriteLn(StdErr, 'Unknown flag: ', Arg);
Exit;
end;
Inc(I);
end;
if SourceFile = '' then
begin
WriteLn(StdErr, 'Error: --source is required');
Exit;
end;
if (not EmitIR) and (OutputFile = '') then
begin
WriteLn(StdErr, 'Error: --output is required (or use --emit-ir)');
Exit;
end;
Result := True;
end;
function RunProcess(const AExe: string; const AArgs: array of string;
out AOutput: string): Integer;
var
Proc: TProcess;
Lines: TStringList;
I: Integer;
begin
Proc := TProcess.Create(nil);
try
Proc.Executable := AExe;
for I := Low(AArgs) to High(AArgs) do
Proc.Parameters.Add(AArgs[I]);
Proc.Options := [poWaitOnExit, poUsePipes];
Proc.Execute;
Lines := TStringList.Create;
try
Lines.LoadFromStream(Proc.Stderr);
AOutput := Lines.Text;
finally
Lines.Free;
end;
Result := Proc.ExitCode;
finally
Proc.Free;
end;
end;
procedure CompileToNative(const AIRFile, AOutputFile: string);
var
AsmFile: string;
Msg: string;
ExitCode: Integer;
begin
AsmFile := ChangeFileExt(AIRFile, '.s');
ExitCode := RunProcess('qbe', ['-o', AsmFile, AIRFile], Msg);
if ExitCode <> 0 then
begin
WriteLn(StdErr, 'qbe error (exit ', ExitCode, '):');
Write(StdErr, Msg);
Halt(1);
end;
ExitCode := RunProcess('cc', ['-o', AOutputFile, AsmFile], Msg);
if ExitCode <> 0 then
begin
WriteLn(StdErr, 'cc error (exit ', ExitCode, '):');
Write(StdErr, Msg);
Halt(1);
end;
DeleteFile(AsmFile);
end;
var
SourceFile, OutputFile: string;
EmitIR: Boolean;
Source: TStringList;
Lexer: TLexer;
Parser: TParser;
Prog: TProgram;
CG: TCodeGenQBE;
IR: string;
IRFile: string;
begin
if not ParseArgs(SourceFile, OutputFile, EmitIR) then
begin
PrintUsage;
Halt(1);
end;
if not FileExists(SourceFile) then
begin
WriteLn(StdErr, 'Error: source file not found: ', SourceFile);
Halt(1);
end;
Source := TStringList.Create;
try
Source.LoadFromFile(SourceFile);
except
on E: Exception do
begin
WriteLn(StdErr, 'Error reading source: ', E.Message);
Halt(1);
end;
end;
Lexer := nil;
Parser := nil;
Prog := nil;
CG := nil;
try
try
Lexer := TLexer.Create(Source.Text);
Parser := TParser.Create(Lexer);
Prog := Parser.Parse;
except
on E: Exception do
begin
WriteLn(StdErr, 'Parse error: ', E.Message);
Halt(1);
end;
end;
try
CG := TCodeGenQBE.Create;
CG.Generate(Prog);
IR := CG.GetOutput;
except
on E: Exception do
begin
WriteLn(StdErr, 'Code generation error: ', E.Message);
Halt(1);
end;
end;
finally
CG.Free;
Prog.Free;
Parser.Free;
Lexer.Free;
Source.Free;
end;
if EmitIR then
begin
Write(IR);
Halt(0);
end;
IRFile := ChangeFileExt(OutputFile, '.ssa');
try
Source := TStringList.Create;
try
Source.Text := IR;
Source.SaveToFile(IRFile);
finally
Source.Free;
end;
except
on E: Exception do
begin
WriteLn(StdErr, 'Error writing IR: ', E.Message);
Halt(1);
end;
end;
CompileToNative(IRFile, OutputFile);
DeleteFile(IRFile);
end.

View file

@ -0,0 +1,181 @@
unit uAST;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, contnrs;
type
{ Base node — all AST nodes carry source position. }
TASTNode = class
public
Line: Integer;
Col: Integer;
end;
{ ------------------------------------------------------------------ }
{ Expressions }
{ ------------------------------------------------------------------ }
TASTExpr = class(TASTNode);
TIntLiteral = class(TASTExpr)
public
Value: Int64;
end;
TStringLiteral = class(TASTExpr)
public
Value: string;
end;
TIdentExpr = class(TASTExpr)
public
Name: string;
end;
TBinaryOp = (boAdd, boSub, boMul, boDiv);
TBinaryExpr = class(TASTExpr)
public
Op: TBinaryOp;
Left: TASTExpr; { owned }
Right: TASTExpr; { owned }
destructor Destroy; override;
end;
{ ------------------------------------------------------------------ }
{ Statements }
{ ------------------------------------------------------------------ }
TASTStmt = class(TASTNode);
TAssignment = class(TASTStmt)
public
Name: string;
Expr: TASTExpr; { owned }
destructor Destroy; override;
end;
TProcCall = class(TASTStmt)
public
Name: string;
Args: TObjectList; { owned TASTExpr items }
constructor Create;
destructor Destroy; override;
end;
{ ------------------------------------------------------------------ }
{ Declarations }
{ ------------------------------------------------------------------ }
TVarDecl = class(TASTNode)
public
Names: TStringList; { owned — one or more names: x, y: Integer }
TypeName: string;
constructor Create;
destructor Destroy; override;
end;
{ ------------------------------------------------------------------ }
{ Block and Program }
{ ------------------------------------------------------------------ }
TBlock = class(TASTNode)
public
Decls: TObjectList; { owned TVarDecl }
Stmts: TObjectList; { owned TASTStmt }
constructor Create;
destructor Destroy; override;
end;
TProgram = class(TASTNode)
public
Name: string;
Uses: TStringList; { owned — unit names from uses clause }
Block: TBlock; { owned }
constructor Create;
destructor Destroy; override;
end;
implementation
{ TBinaryExpr }
destructor TBinaryExpr.Destroy;
begin
Left.Free;
Right.Free;
inherited Destroy;
end;
{ TAssignment }
destructor TAssignment.Destroy;
begin
Expr.Free;
inherited Destroy;
end;
{ TProcCall }
constructor TProcCall.Create;
begin
inherited Create;
Args := TObjectList.Create(True);
end;
destructor TProcCall.Destroy;
begin
Args.Free;
inherited Destroy;
end;
{ TVarDecl }
constructor TVarDecl.Create;
begin
inherited Create;
Names := TStringList.Create;
end;
destructor TVarDecl.Destroy;
begin
Names.Free;
inherited Destroy;
end;
{ TBlock }
constructor TBlock.Create;
begin
inherited Create;
Decls := TObjectList.Create(True);
Stmts := TObjectList.Create(True);
end;
destructor TBlock.Destroy;
begin
Decls.Free;
Stmts.Free;
inherited Destroy;
end;
{ TProgram }
constructor TProgram.Create;
begin
inherited Create;
Uses := TStringList.Create;
end;
destructor TProgram.Destroy;
begin
Uses.Free;
Block.Free;
inherited Destroy;
end;
end.

View file

@ -0,0 +1,356 @@
unit uCodeGenQBE;
{$mode objfpc}{$H+}
{ Phase 1 QBE IR emitter.
String layout: Phase 1 uses raw NUL-terminated bytes (no ARC header).
The full refcount+length+capacity header is introduced in Phase 2.
WriteLn/Write are built-ins resolved directly to libc printf calls. }
interface
uses
SysUtils, StrUtils, Classes, uAST;
type
ECodeGenError = class(Exception);
TCodeGenQBE = class
private
FOutput: TStringList;
FStrLits: TStringList; { index → raw value; label = $__s<index> }
FTempCount: Integer;
FVarTypes: TStringList; { name → type ('w'=integer, 'l'=string ptr) }
function AllocTemp: string;
function EmitStrLit(const AValue: string): string;
procedure EmitLine(const ALine: string);
procedure EmitDataSection;
procedure EmitMainHeader;
procedure EmitMainFooter;
procedure EmitBlock(ABlock: TBlock);
procedure EmitVarAllocs(ABlock: TBlock);
procedure EmitStmt(AStmt: TASTStmt);
procedure EmitAssignment(AAssign: TAssignment);
procedure EmitProcCall(ACall: TProcCall);
procedure EmitWriteLn(ACall: TProcCall);
procedure EmitWrite(ACall: TProcCall; ANewline: Boolean);
function EmitExpr(AExpr: TASTExpr): string;
function QbeTypeForVar(const AName: string): string;
function QbeEscapeString(const AStr: string): string;
public
constructor Create;
destructor Destroy; override;
procedure Generate(AProg: TProgram);
function GetOutput: string;
end;
implementation
constructor TCodeGenQBE.Create;
begin
inherited Create;
FOutput := TStringList.Create;
FStrLits := TStringList.Create;
FVarTypes := TStringList.Create;
FTempCount := 0;
end;
destructor TCodeGenQBE.Destroy;
begin
FOutput.Free;
FStrLits.Free;
FVarTypes.Free;
inherited Destroy;
end;
function TCodeGenQBE.AllocTemp: string;
begin
Result := Format('%%_t%d', [FTempCount]);
Inc(FTempCount);
end;
function TCodeGenQBE.EmitStrLit(const AValue: string): string;
{ Returns the global label for this string literal, e.g. $__s0 }
var
Idx: Integer;
begin
Idx := FStrLits.IndexOf(AValue);
if Idx < 0 then
Idx := FStrLits.Add(AValue);
Result := Format('$__s%d', [Idx]);
end;
procedure TCodeGenQBE.EmitLine(const ALine: string);
begin
FOutput.Add(ALine);
end;
procedure TCodeGenQBE.EmitDataSection;
var
I: Integer;
begin
if FStrLits.Count = 0 then
Exit;
EmitLine('# String literals');
for I := 0 to FStrLits.Count - 1 do
EmitLine(Format('data $__s%d = { b "%s", b 0 }',
[I, QbeEscapeString(FStrLits[I])]));
EmitLine('data $__fmt_s_nl = { b "%s\n", b 0 }');
EmitLine('data $__fmt_s = { b "%s", b 0 }');
EmitLine('data $__fmt_d_nl = { b "%d\n", b 0 }');
EmitLine('data $__fmt_d = { b "%d", b 0 }');
EmitLine('data $__fmt_nl = { b "\n", b 0 }');
EmitLine('');
end;
procedure TCodeGenQBE.EmitMainHeader;
begin
EmitLine('export function w $main() {');
EmitLine('@start');
end;
procedure TCodeGenQBE.EmitMainFooter;
begin
EmitLine(' ret 0');
EmitLine('}');
end;
procedure TCodeGenQBE.EmitVarAllocs(ABlock: TBlock);
var
I, J: Integer;
Decl: TVarDecl;
VarName, Alloc: string;
begin
for I := 0 to ABlock.Decls.Count - 1 do
begin
Decl := TVarDecl(ABlock.Decls[I]);
for J := 0 to Decl.Names.Count - 1 do
begin
VarName := Decl.Names[J];
if SameText(Decl.TypeName, 'Integer') or
SameText(Decl.TypeName, 'Boolean') then
begin
Alloc := Format(' %%_var_%s =l alloc4 1', [VarName]);
FVarTypes.Values[VarName] := 'w';
end
else if SameText(Decl.TypeName, 'string') then
begin
Alloc := Format(' %%_var_%s =l alloc8 1', [VarName]);
FVarTypes.Values[VarName] := 'l';
{ Initialise string pointer to nil (0) }
EmitLine(Alloc);
EmitLine(Format(' storel 0, %%_var_%s', [VarName]));
Continue;
end
else
raise ECodeGenError.CreateFmt(
'Unknown type ''%s'' for variable ''%s''', [Decl.TypeName, VarName]);
EmitLine(Alloc);
end;
end;
end;
procedure TCodeGenQBE.EmitBlock(ABlock: TBlock);
var
I: Integer;
begin
EmitVarAllocs(ABlock);
for I := 0 to ABlock.Stmts.Count - 1 do
EmitStmt(TASTStmt(ABlock.Stmts[I]));
end;
procedure TCodeGenQBE.EmitStmt(AStmt: TASTStmt);
begin
if AStmt is TAssignment then
EmitAssignment(TAssignment(AStmt))
else if AStmt is TProcCall then
EmitProcCall(TProcCall(AStmt))
else
raise ECodeGenError.Create('Unknown statement node type');
end;
procedure TCodeGenQBE.EmitAssignment(AAssign: TAssignment);
var
ValTemp, QType, StoreInstr: string;
begin
QType := QbeTypeForVar(AAssign.Name);
ValTemp := EmitExpr(AAssign.Expr);
if QType = 'w' then
StoreInstr := 'storew'
else
StoreInstr := 'storel';
EmitLine(Format(' %s %s, %%_var_%s', [StoreInstr, ValTemp, AAssign.Name]));
end;
procedure TCodeGenQBE.EmitProcCall(ACall: TProcCall);
var
UCaseName: string;
begin
UCaseName := UpperCase(ACall.Name);
if (UCaseName = 'WRITELN') then
EmitWriteLn(ACall)
else if UCaseName = 'WRITE' then
EmitWrite(ACall, False)
else
raise ECodeGenError.CreateFmt(
'Unknown procedure ''%s'' at line %d', [ACall.Name, ACall.Line]);
end;
procedure TCodeGenQBE.EmitWriteLn(ACall: TProcCall);
begin
EmitWrite(ACall, True);
end;
procedure TCodeGenQBE.EmitWrite(ACall: TProcCall; ANewline: Boolean);
var
ArgExpr: TASTExpr;
ArgTemp: string;
FmtLabel: string;
IsString: Boolean;
begin
if ACall.Args.Count = 0 then
begin
if ANewline then
EmitLine(' call $printf(l $__fmt_nl)');
Exit;
end;
if ACall.Args.Count > 1 then
raise ECodeGenError.CreateFmt(
'Phase 1: Write/WriteLn takes at most 1 argument (line %d)', [ACall.Line]);
ArgExpr := TASTExpr(ACall.Args[0]);
IsString := (ArgExpr is TStringLiteral) or
((ArgExpr is TIdentExpr) and
(QbeTypeForVar(TIdentExpr(ArgExpr).Name) = 'l'));
ArgTemp := EmitExpr(ArgExpr);
if IsString then
FmtLabel := IfThen(ANewline, '$__fmt_s_nl', '$__fmt_s')
else
FmtLabel := IfThen(ANewline, '$__fmt_d_nl', '$__fmt_d');
if IsString then
EmitLine(Format(' call $printf(l %s, ..., l %s)', [FmtLabel, ArgTemp]))
else
EmitLine(Format(' call $printf(l %s, ..., w %s)', [FmtLabel, ArgTemp]));
end;
function TCodeGenQBE.EmitExpr(AExpr: TASTExpr): string;
var
T, L, R: string;
Op: string;
BinExpr: TBinaryExpr;
QType: string;
begin
if AExpr is TIntLiteral then
begin
T := AllocTemp;
EmitLine(Format(' %s =w copy %d', [T, TIntLiteral(AExpr).Value]));
Result := T;
end
else if AExpr is TStringLiteral then
begin
Result := EmitStrLit(TStringLiteral(AExpr).Value);
end
else if AExpr is TIdentExpr then
begin
T := AllocTemp;
QType := QbeTypeForVar(TIdentExpr(AExpr).Name);
if QType = 'w' then
EmitLine(Format(' %s =w loadw %%_var_%s', [T, TIdentExpr(AExpr).Name]))
else
EmitLine(Format(' %s =l loadl %%_var_%s', [T, TIdentExpr(AExpr).Name]));
Result := T;
end
else if AExpr is TBinaryExpr then
begin
BinExpr := TBinaryExpr(AExpr);
L := EmitExpr(BinExpr.Left);
R := EmitExpr(BinExpr.Right);
T := AllocTemp;
case BinExpr.Op of
boAdd: Op := 'add';
boSub: Op := 'sub';
boMul: Op := 'mul';
boDiv: Op := 'div';
end;
EmitLine(Format(' %s =w %s %s, %s', [T, Op, L, R]));
Result := T;
end
else
raise ECodeGenError.Create('Unknown expression node type');
end;
function TCodeGenQBE.QbeTypeForVar(const AName: string): string;
begin
Result := FVarTypes.Values[AName];
if Result = '' then
Result := 'w'; { default to integer if unknown (e.g. undeclared) }
end;
function TCodeGenQBE.QbeEscapeString(const AStr: string): string;
var
I: Integer;
C: Char;
begin
Result := '';
for I := 1 to Length(AStr) do
begin
C := AStr[I];
case C of
'"': Result := Result + '\"';
'\': Result := Result + '\\';
#10: Result := Result + '\n';
#13: Result := Result + '\r';
#9: Result := Result + '\t';
else if (Ord(C) < 32) or (Ord(C) > 126) then
Result := Result + Format('\%02x', [Ord(C)])
else
Result := Result + C;
end;
end;
end;
procedure TCodeGenQBE.Generate(AProg: TProgram);
var
Body: TStringList;
SavedOutput: TStringList;
begin
FOutput.Clear;
FStrLits.Clear;
FVarTypes.Clear;
FTempCount := 0;
{ Two-pass emit: collect string literals by emitting the body first,
then prepend the data section. }
Body := TStringList.Create;
try
SavedOutput := FOutput;
FOutput := Body;
try
EmitMainHeader;
EmitBlock(AProg.Block);
EmitMainFooter;
finally
FOutput := SavedOutput;
end;
EmitLine('# Generated by Blaise Compiler (Phase 1)');
EmitLine('# Source: ' + AProg.Name);
EmitLine('');
EmitDataSection;
FOutput.AddStrings(Body);
finally
Body.Free;
end;
end;
function TCodeGenQBE.GetOutput: string;
begin
Result := FOutput.Text;
end;
end.

View file

@ -0,0 +1,195 @@
unit uLexer;
{$mode objfpc}{$H+}
{ Compiler lexer — wraps uPasTokeniser and converts its flat token stream
into the compiler's specific token kinds. Skips whitespace, line endings,
comments, and compiler directives. Unescapes string literal values. }
interface
uses
SysUtils, uPasTokeniser;
type
TTokenKind = (
tkEOF,
{ Literals }
tkIntLit,
tkStringLit,
{ Keywords }
tkProgram,
tkUses,
tkVar,
tkBegin,
tkEnd,
{ Identifier }
tkIdent,
{ Arithmetic operators }
tkPlus,
tkMinus,
tkStar,
tkSlash,
{ Assignment and colon }
tkAssign, { := }
tkColon, { : }
{ Grouping and punctuation }
tkLParen,
tkRParen,
tkComma,
tkSemicolon,
tkDot
);
TToken = record
Kind: TTokenKind;
Value: string; { parsed value: ident text, int digits, unescaped string }
Line: Integer;
Col: Integer;
end;
TLexer = class
private
FTok: TFpgPascalTokeniser;
function MapKeyword(const AUpper: string): TTokenKind;
function UnescapeString(const ARaw: string): string;
public
constructor Create(const ASource: string);
destructor Destroy; override;
function Next: TToken;
end;
implementation
constructor TLexer.Create(const ASource: string);
begin
inherited Create;
FTok := TFpgPascalTokeniser.Create;
FTok.SetSource(ASource);
end;
destructor TLexer.Destroy;
begin
FTok.Free;
inherited Destroy;
end;
function TLexer.MapKeyword(const AUpper: string): TTokenKind;
begin
if AUpper = 'PROGRAM' then Result := tkProgram
else if AUpper = 'USES' then Result := tkUses
else if AUpper = 'VAR' then Result := tkVar
else if AUpper = 'BEGIN' then Result := tkBegin
else if AUpper = 'END' then Result := tkEnd
else
Result := tkIdent; { keyword outside Phase 1 grammar treated as ident }
end;
function TLexer.UnescapeString(const ARaw: string): string;
{ ARaw is the full source span including surrounding quotes.
Handles: 'text' with '' → ' escaping.
Phase 1 supports only single-quoted string literals; #nn and ^X forms
are not yet part of the Clean Pascal spec. }
var
I, Len: Integer;
begin
Result := '';
Len := Length(ARaw);
if (Len >= 2) and (ARaw[1] = '''') then
begin
I := 2;
while I <= Len do
begin
if ARaw[I] = '''' then
begin
if (I < Len) and (ARaw[I + 1] = '''') then
begin
Result := Result + '''';
Inc(I, 2);
end
else
Inc(I); { closing quote }
end
else
begin
Result := Result + ARaw[I];
Inc(I);
end;
end;
end;
end;
function TLexer.Next: TToken;
var
raw: TFpgPasToken;
text: string;
begin
repeat
raw := FTok.NextToken;
until not (raw.Kind in [fptkWhitespace, fptkLineEnding,
fptkComment, fptkDirective]);
Result.Line := raw.Line;
Result.Col := raw.Column;
case raw.Kind of
fptkEOF:
begin
Result.Kind := tkEOF;
Result.Value := '';
end;
fptkKeyword:
begin
text := FTok.TokenTextUpper;
Result.Kind := MapKeyword(text);
Result.Value := FTok.TokenText;
end;
fptkIdentifier:
begin
Result.Kind := tkIdent;
Result.Value := FTok.TokenText;
end;
fptkNumber:
begin
Result.Kind := tkIntLit;
Result.Value := FTok.TokenText;
end;
fptkString:
begin
Result.Kind := tkStringLit;
Result.Value := UnescapeString(FTok.TokenText);
end;
fptkSymbol:
begin
text := FTok.TokenText;
if text = ':=' then Result.Kind := tkAssign
else if text = ':' then Result.Kind := tkColon
else if text = '(' then Result.Kind := tkLParen
else if text = ')' then Result.Kind := tkRParen
else if text = ',' then Result.Kind := tkComma
else if text = ';' then Result.Kind := tkSemicolon
else if text = '.' then Result.Kind := tkDot
else if text = '+' then Result.Kind := tkPlus
else if text = '-' then Result.Kind := tkMinus
else if text = '*' then Result.Kind := tkStar
else if text = '/' then Result.Kind := tkSlash
else
raise Exception.CreateFmt(
'Unexpected symbol ''%s'' at line %d col %d',
[text, raw.Line, raw.Column]);
Result.Value := text;
end;
else
raise Exception.CreateFmt(
'Unexpected token kind %d at line %d col %d',
[Ord(raw.Kind), raw.Line, raw.Column]);
end;
end;
end.

View file

@ -0,0 +1,379 @@
unit uParser;
{$mode objfpc}{$H+}
{ Recursive-descent parser for the Phase 1 grammar:
Program ::= 'program' Ident ';' [Uses] Block '.'
Uses ::= 'uses' Ident {',' Ident} ';'
Block ::= ['var' VarBlock] 'begin' StmtList 'end'
VarBlock ::= VarDecl {VarDecl}
VarDecl ::= IdentList ':' TypeName ';'
StmtList ::= Stmt {';' Stmt} [';']
Stmt ::= Assignment | ProcCall | empty
Assignment ::= Ident ':=' Expr
ProcCall ::= Ident ['(' [ExprList] ')']
ExprList ::= Expr {',' Expr}
Expr ::= Term (('+' | '-') Term)*
Term ::= Factor (('*' | '/') Factor)*
Factor ::= IntLit | StringLit | Ident | '(' Expr ')'
TypeName ::= 'Integer' | 'Boolean' | 'string'
}
interface
uses
SysUtils, uLexer, uAST;
type
EParseError = class(Exception);
TParser = class
private
FLexer: TLexer;
FCurrent: TToken;
procedure Advance;
procedure Expect(AKind: TTokenKind);
function Check(AKind: TTokenKind): Boolean;
function ParseProgram: TProgram;
procedure ParseUses(AProg: TProgram);
function ParseBlock: TBlock;
procedure ParseVarBlock(ABlock: TBlock);
procedure ParseVarDecl(ABlock: TBlock);
procedure ParseStmtList(ABlock: TBlock);
function ParseStmt: TASTStmt;
function ParseExpr: TASTExpr;
function ParseTerm: TASTExpr;
function ParseFactor: TASTExpr;
procedure ParseArgList(ACall: TProcCall);
public
constructor Create(ALexer: TLexer);
function Parse: TProgram;
end;
implementation
constructor TParser.Create(ALexer: TLexer);
begin
inherited Create;
FLexer := ALexer;
FCurrent := FLexer.Next;
end;
procedure TParser.Advance;
begin
FCurrent := FLexer.Next;
end;
procedure TParser.Expect(AKind: TTokenKind);
begin
if FCurrent.Kind <> AKind then
raise EParseError.CreateFmt(
'Expected token %d but got %d (''%s'') at line %d col %d',
[Ord(AKind), Ord(FCurrent.Kind), FCurrent.Value,
FCurrent.Line, FCurrent.Col]);
Advance;
end;
function TParser.Check(AKind: TTokenKind): Boolean;
begin
Result := FCurrent.Kind = AKind;
end;
{ ------------------------------------------------------------------ }
function TParser.Parse: TProgram;
begin
Result := ParseProgram;
end;
function TParser.ParseProgram: TProgram;
begin
Result := TProgram.Create;
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
Expect(tkProgram);
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected program name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Result.Name := FCurrent.Value;
Advance;
Expect(tkSemicolon);
if Check(tkUses) then
ParseUses(Result);
Result.Block := ParseBlock;
Expect(tkDot);
if not Check(tkEOF) then
raise EParseError.CreateFmt(
'Unexpected tokens after program end at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
except
Result.Free;
raise;
end;
end;
procedure TParser.ParseUses(AProg: TProgram);
begin
Expect(tkUses);
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected unit name after ''uses'' at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
AProg.Uses.Add(FCurrent.Value);
Advance;
while Check(tkComma) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected unit name after '','' at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
AProg.Uses.Add(FCurrent.Value);
Advance;
end;
Expect(tkSemicolon);
end;
function TParser.ParseBlock: TBlock;
begin
Result := TBlock.Create;
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
if Check(tkVar) then
ParseVarBlock(Result);
Expect(tkBegin);
ParseStmtList(Result);
Expect(tkEnd);
except
Result.Free;
raise;
end;
end;
procedure TParser.ParseVarBlock(ABlock: TBlock);
begin
Expect(tkVar);
{ At least one declaration required after 'var' }
while Check(tkIdent) do
ParseVarDecl(ABlock);
end;
procedure TParser.ParseVarDecl(ABlock: TBlock);
var
Decl: TVarDecl;
begin
Decl := TVarDecl.Create;
Decl.Line := FCurrent.Line;
Decl.Col := FCurrent.Col;
try
{ IdentList: one or more names separated by commas }
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected variable name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Decl.Names.Add(FCurrent.Value);
Advance;
while Check(tkComma) do
begin
Advance;
if not Check(tkIdent) then
raise EParseError.CreateFmt('Expected variable name at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Decl.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]);
Decl.TypeName := FCurrent.Value;
Advance;
Expect(tkSemicolon);
ABlock.Decls.Add(Decl);
except
Decl.Free;
raise;
end;
end;
procedure TParser.ParseStmtList(ABlock: TBlock);
var
Stmt: TASTStmt;
begin
{ Parse zero or more statements separated (or terminated) by semicolons }
while not (Check(tkEnd) or Check(tkEOF)) do
begin
Stmt := ParseStmt;
if Stmt <> nil then
ABlock.Stmts.Add(Stmt);
if Check(tkSemicolon) then
Advance
else
Break;
end;
end;
function TParser.ParseStmt: TASTStmt;
var
Name: string;
Line, Col: Integer;
Call: TProcCall;
Assign: TAssignment;
begin
Result := nil;
{ Empty statement }
if Check(tkEnd) or Check(tkEOF) or Check(tkSemicolon) then
Exit;
if not Check(tkIdent) then
raise EParseError.CreateFmt(
'Expected statement at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
Name := FCurrent.Value;
Line := FCurrent.Line;
Col := FCurrent.Col;
Advance;
if Check(tkAssign) then
begin
{ Assignment }
Advance;
Assign := TAssignment.Create;
Assign.Line := Line;
Assign.Col := Col;
Assign.Name := Name;
Assign.Expr := ParseExpr;
Result := Assign;
end
else
begin
{ Procedure call — parentheses optional for zero-arg calls }
Call := TProcCall.Create;
Call.Line := Line;
Call.Col := Col;
Call.Name := Name;
if Check(tkLParen) then
begin
Advance;
if not Check(tkRParen) then
ParseArgList(Call);
Expect(tkRParen);
end;
Result := Call;
end;
end;
procedure TParser.ParseArgList(ACall: TProcCall);
begin
ACall.Args.Add(ParseExpr);
while Check(tkComma) do
begin
Advance;
ACall.Args.Add(ParseExpr);
end;
end;
{ Expression parsing — standard precedence climbing }
function TParser.ParseExpr: TASTExpr;
var
Right: TASTExpr;
Op: TBinaryOp;
Node: TBinaryExpr;
begin
Result := ParseTerm;
while Check(tkPlus) or Check(tkMinus) do
begin
if Check(tkPlus) then Op := boAdd else Op := boSub;
Advance;
Right := ParseTerm;
Node := TBinaryExpr.Create;
Node.Op := Op;
Node.Left := Result;
Node.Right := Right;
Result := Node;
end;
end;
function TParser.ParseTerm: TASTExpr;
var
Right: TASTExpr;
Op: TBinaryOp;
Node: TBinaryExpr;
begin
Result := ParseFactor;
while Check(tkStar) or Check(tkSlash) do
begin
if Check(tkStar) then Op := boMul else Op := boDiv;
Advance;
Right := ParseFactor;
Node := TBinaryExpr.Create;
Node.Op := Op;
Node.Left := Result;
Node.Right := Right;
Result := Node;
end;
end;
function TParser.ParseFactor: TASTExpr;
var
IntNode: TIntLiteral;
StrNode: TStringLiteral;
IdNode: TIdentExpr;
Inner: TASTExpr;
begin
case FCurrent.Kind of
tkIntLit:
begin
IntNode := TIntLiteral.Create;
IntNode.Line := FCurrent.Line;
IntNode.Col := FCurrent.Col;
IntNode.Value := StrToInt64(FCurrent.Value);
Advance;
Result := IntNode;
end;
tkStringLit:
begin
StrNode := TStringLiteral.Create;
StrNode.Line := FCurrent.Line;
StrNode.Col := FCurrent.Col;
StrNode.Value := FCurrent.Value;
Advance;
Result := StrNode;
end;
tkIdent:
begin
IdNode := TIdentExpr.Create;
IdNode.Line := FCurrent.Line;
IdNode.Col := FCurrent.Col;
IdNode.Name := FCurrent.Value;
Advance;
Result := IdNode;
end;
tkLParen:
begin
Advance;
Inner := ParseExpr;
Expect(tkRParen);
Result := Inner;
end;
else
raise EParseError.CreateFmt(
'Expected expression at line %d col %d',
[FCurrent.Line, FCurrent.Col]);
end;
end;
end.

View file

@ -0,0 +1,586 @@
{
Clean Pascal Compiler — General Pascal Tokeniser
Lightweight Object Pascal tokeniser. Operates on a string buffer and
yields tokens one at a time via NextToken. No exceptions on malformed
source — unrecognised characters produce fptkSymbol tokens of length 1,
and unterminated strings or comments consume to end-of-source.
Does NOT evaluate compiler directives or IFDEF branches — everything
is tokenised literally.
Ported from the fpGUI IDE tokeniser (same author).
}
unit uPasTokeniser;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils;
type
TFpgPasTokenKind = (
fptkEOF,
fptkWhitespace,
fptkLineEnding,
fptkIdentifier,
fptkKeyword,
fptkNumber,
fptkString,
fptkComment,
fptkDirective,
fptkSymbol
);
TFpgPasToken = record
Kind: TFpgPasTokenKind;
Line: Integer; { 1-based line number }
Column: Integer; { 1-based column }
Len: Integer; { character length in source }
TextStart: Integer; { 1-based index into source string }
end;
{ TFpgPascalTokeniser }
TFpgPascalTokeniser = class(TObject)
private
FSource: string;
FPos: Integer;
FLine: Integer;
FLineStart: Integer; { FPos value at start of current line }
FToken: TFpgPasToken;
function Peek: Char; inline;
function PeekAt(AOffset: Integer): Char; inline;
procedure Advance; inline;
procedure AdvanceLine;
procedure ReadWhitespace;
procedure ReadLineEnding;
procedure ReadIdentifierOrKeyword;
procedure ReadNumber;
procedure ReadString;
procedure ReadBraceCommentOrDirective;
procedure ReadParenStarCommentOrDirective;
procedure ReadLineComment;
procedure ReadSymbol;
public
constructor Create;
procedure SetSource(const ASource: string);
function NextToken: TFpgPasToken;
function TokenText: string;
function TokenTextUpper: string;
property Token: TFpgPasToken read FToken;
property Source: string read FSource;
end;
{ Returns True if AText is a Pascal keyword (case-insensitive). }
function PasIsKeyword(const AText: string): Boolean;
implementation
const
{ Sorted keyword list for binary search }
KeywordCount = 79;
Keywords: array[0..KeywordCount - 1] of string = (
'ABSOLUTE', 'AND', 'ARRAY', 'AS', 'ASM',
'BEGIN', 'BITPACKED',
'CASE', 'CLASS', 'CONST', 'CONSTREF', 'CONSTRUCTOR', 'CONTAINS',
'DESTRUCTOR', 'DISPINTERFACE', 'DIV', 'DO', 'DOWNTO',
'ELSE', 'END', 'EXCEPT', 'EXPORTS',
'FALSE', 'FILE', 'FINALIZATION', 'FINALLY', 'FOR', 'FUNCTION',
'GENERIC', 'GOTO',
'IF', 'IMPLEMENTATION', 'IN', 'INHERITED', 'INITIALIZATION',
'INLINE', 'INTERFACE', 'IS',
'LABEL', 'LIBRARY',
'MOD',
'NIL', 'NOT',
'OBJCCATEGORY', 'OBJCCLASS', 'OBJCPROTOCOL', 'OBJECT', 'OF',
'OPERATOR', 'OR', 'OTHERWISE',
'PACKAGE', 'PACKED', 'PROCEDURE', 'PROGRAM', 'PROPERTY',
'RAISE', 'RECORD', 'REPEAT', 'REQUIRES', 'RESOURCESTRING',
'SELF', 'SET', 'SHL', 'SHR', 'SPECIALIZE',
'THEN', 'THREADVAR', 'TO', 'TRUE', 'TRY', 'TYPE',
'UNIT', 'UNTIL', 'USES',
'VAR',
'WHILE', 'WITH',
'XOR'
);
function BinarySearchKeyword(const AText: string): Boolean;
var
Lo, Hi, Mid, Cmp: Integer;
begin
Result := False;
Lo := 0;
Hi := KeywordCount - 1;
while Lo <= Hi do
begin
Mid := (Lo + Hi) shr 1;
Cmp := CompareStr(AText, Keywords[Mid]);
if Cmp < 0 then
Hi := Mid - 1
else if Cmp > 0 then
Lo := Mid + 1
else
Exit(True);
end;
end;
function PasIsKeyword(const AText: string): Boolean;
begin
if AText = '' then
Exit(False);
Result := BinarySearchKeyword(UpCase(AText));
end;
{ TFpgPascalTokeniser }
constructor TFpgPascalTokeniser.Create;
begin
inherited Create;
FSource := '';
FPos := 1;
FLine := 1;
FLineStart := 1;
end;
procedure TFpgPascalTokeniser.SetSource(const ASource: string);
begin
FSource := ASource;
FPos := 1;
FLine := 1;
FLineStart := 1;
FToken.Kind := fptkEOF;
FToken.Line := 1;
FToken.Column := 1;
FToken.Len := 0;
FToken.TextStart := 1;
end;
function TFpgPascalTokeniser.Peek: Char;
begin
if FPos <= Length(FSource) then
Result := FSource[FPos]
else
Result := #0;
end;
function TFpgPascalTokeniser.PeekAt(AOffset: Integer): Char;
var
p: Integer;
begin
p := FPos + AOffset;
if (p >= 1) and (p <= Length(FSource)) then
Result := FSource[p]
else
Result := #0;
end;
procedure TFpgPascalTokeniser.Advance;
begin
Inc(FPos);
end;
procedure TFpgPascalTokeniser.AdvanceLine;
begin
Inc(FLine);
FLineStart := FPos;
end;
procedure TFpgPascalTokeniser.ReadWhitespace;
begin
FToken.Kind := fptkWhitespace;
while (FPos <= Length(FSource)) and (FSource[FPos] in [' ', #9]) do
Advance;
FToken.Len := FPos - FToken.TextStart;
end;
procedure TFpgPascalTokeniser.ReadLineEnding;
begin
FToken.Kind := fptkLineEnding;
if (FSource[FPos] = #13) and (PeekAt(1) = #10) then
Advance; { consume CR of CRLF }
Advance; { consume LF (or lone CR) }
FToken.Len := FPos - FToken.TextStart;
AdvanceLine;
end;
procedure TFpgPascalTokeniser.ReadIdentifierOrKeyword;
begin
while (FPos <= Length(FSource)) and
(FSource[FPos] in ['A'..'Z', 'a'..'z', '0'..'9', '_']) do
Advance;
FToken.Len := FPos - FToken.TextStart;
if BinarySearchKeyword(UpCase(TokenText)) then
FToken.Kind := fptkKeyword
else
FToken.Kind := fptkIdentifier;
end;
procedure TFpgPascalTokeniser.ReadNumber;
var
c: Char;
begin
FToken.Kind := fptkNumber;
c := FSource[FPos];
if c = '$' then
begin
{ Hex: $[0-9A-Fa-f]+ }
Advance;
while (FPos <= Length(FSource)) and
(FSource[FPos] in ['0'..'9', 'A'..'F', 'a'..'f']) do
Advance;
FToken.Len := FPos - FToken.TextStart;
Exit;
end;
if c = '%' then
begin
{ Binary: %[01]+ }
Advance;
while (FPos <= Length(FSource)) and (FSource[FPos] in ['0', '1']) do
Advance;
FToken.Len := FPos - FToken.TextStart;
Exit;
end;
if c = '&' then
begin
{ Octal: &[0-7]+ }
Advance;
while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'7']) do
Advance;
FToken.Len := FPos - FToken.TextStart;
Exit;
end;
{ Decimal integer or float }
while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
Advance;
{ Check for decimal point (but not '..') }
if (FPos <= Length(FSource)) and (FSource[FPos] = '.') and
(PeekAt(1) <> '.') then
begin
Advance; { consume '.' }
while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
Advance;
end;
{ Check for exponent }
if (FPos <= Length(FSource)) and (FSource[FPos] in ['e', 'E']) then
begin
Advance;
if (FPos <= Length(FSource)) and (FSource[FPos] in ['+', '-']) then
Advance;
while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
Advance;
end;
FToken.Len := FPos - FToken.TextStart;
end;
procedure TFpgPascalTokeniser.ReadString;
var
c: Char;
begin
{ Pascal string literals can be composed of:
- 'quoted text' (with '' for embedded quotes)
- #nn (decimal char code)
- #$nn (hex char code)
- ^A (control char)
These can be concatenated without operators: 'abc'#13#10'def' }
FToken.Kind := fptkString;
repeat
c := Peek;
if c = '''' then
begin
Advance; { opening quote }
while FPos <= Length(FSource) do
begin
if FSource[FPos] = '''' then
begin
Advance;
{ Embedded quote? '' }
if (FPos <= Length(FSource)) and (FSource[FPos] = '''') then
Advance
else
Break; { closing quote }
end
else if FSource[FPos] in [#10, #13] then
Break { unterminated string at line end }
else
Advance;
end;
end
else if c = '#' then
begin
Advance; { consume # }
if (FPos <= Length(FSource)) and (FSource[FPos] = '$') then
begin
Advance; { hex char code }
while (FPos <= Length(FSource)) and
(FSource[FPos] in ['0'..'9', 'A'..'F', 'a'..'f']) do
Advance;
end
else
begin
while (FPos <= Length(FSource)) and (FSource[FPos] in ['0'..'9']) do
Advance;
end;
end
else if c = '^' then
begin
Advance; { consume ^ }
if (FPos <= Length(FSource)) and (FSource[FPos] in ['A'..'Z', 'a'..'z']) then
Advance;
end
else
Break; { not a string continuation }
until False;
FToken.Len := FPos - FToken.TextStart;
end;
procedure TFpgPascalTokeniser.ReadBraceCommentOrDirective;
begin
// Already at open-brace. Check next char for '$'.
if PeekAt(1) = '$' then
FToken.Kind := fptkDirective
else
FToken.Kind := fptkComment;
Advance; // consume open-brace
while FPos <= Length(FSource) do
begin
if FSource[FPos] = '}' then
begin
Advance;
Break;
end
else if FSource[FPos] = #13 then
begin
Advance;
if (FPos <= Length(FSource)) and (FSource[FPos] = #10) then
Advance;
AdvanceLine;
end
else if FSource[FPos] = #10 then
begin
Advance;
AdvanceLine;
end
else
Advance;
end;
FToken.Len := FPos - FToken.TextStart;
end;
procedure TFpgPascalTokeniser.ReadParenStarCommentOrDirective;
begin
{ Already at '('. Next is '*'. Check char after '*' for '$'. }
if PeekAt(2) = '$' then
FToken.Kind := fptkDirective
else
FToken.Kind := fptkComment;
Advance; { consume '(' }
Advance; { consume '*' }
while FPos <= Length(FSource) do
begin
if (FSource[FPos] = '*') and (PeekAt(1) = ')') then
begin
Advance; { consume '*' }
Advance; { consume ')' }
Break;
end
else if FSource[FPos] = #13 then
begin
Advance;
if (FPos <= Length(FSource)) and (FSource[FPos] = #10) then
Advance;
AdvanceLine;
end
else if FSource[FPos] = #10 then
begin
Advance;
AdvanceLine;
end
else
Advance;
end;
FToken.Len := FPos - FToken.TextStart;
end;
procedure TFpgPascalTokeniser.ReadLineComment;
begin
FToken.Kind := fptkComment;
{ Consume everything until end of line or end of source }
while (FPos <= Length(FSource)) and
not (FSource[FPos] in [#10, #13]) do
Advance;
FToken.Len := FPos - FToken.TextStart;
end;
procedure TFpgPascalTokeniser.ReadSymbol;
var
c, c2: Char;
begin
FToken.Kind := fptkSymbol;
c := FSource[FPos];
c2 := PeekAt(1);
Advance;
case c of
':': if c2 = '=' then Advance; // :=
'<': if c2 in ['>', '='] then Advance; // <> or <=
'>': if c2 in ['<', '='] then Advance; // >< or >=
'.': if c2 = '.' then Advance; // ..
'*': if c2 = '*' then Advance; // **
'@': if c2 = '@' then Advance; // @@
'+': if c2 = '=' then Advance; // +=
'-': if c2 = '=' then Advance; // -=
'/': if c2 = '=' then Advance; // /= (// handled separately)
end;
// Special: *= (if * was not followed by *)
if (c = '*') and (c2 <> '*') and (c2 = '=') then
Advance;
FToken.Len := FPos - FToken.TextStart;
end;
function TFpgPascalTokeniser.NextToken: TFpgPasToken;
var
c, c2: Char;
begin
if FPos > Length(FSource) then
begin
FToken.Kind := fptkEOF;
FToken.Line := FLine;
FToken.Column := FPos - FLineStart + 1;
FToken.Len := 0;
FToken.TextStart := FPos;
Result := FToken;
Exit;
end;
{ Record token start position }
FToken.TextStart := FPos;
FToken.Line := FLine;
FToken.Column := FPos - FLineStart + 1;
c := FSource[FPos];
{ Whitespace (not line endings) }
if c in [' ', #9] then
begin
ReadWhitespace;
Result := FToken;
Exit;
end;
{ Line endings }
if c in [#13, #10] then
begin
ReadLineEnding;
Result := FToken;
Exit;
end;
{ Identifiers and keywords }
if c in ['A'..'Z', 'a'..'z', '_'] then
begin
ReadIdentifierOrKeyword;
Result := FToken;
Exit;
end;
{ Numbers: digits or $ (hex) or % (binary) or & (octal) }
if c in ['0'..'9'] then
begin
ReadNumber;
Result := FToken;
Exit;
end;
if (c = '$') and (PeekAt(1) in ['0'..'9', 'A'..'F', 'a'..'f']) then
begin
ReadNumber;
Result := FToken;
Exit;
end;
if (c = '%') and (PeekAt(1) in ['0', '1']) then
begin
ReadNumber;
Result := FToken;
Exit;
end;
if (c = '&') and (PeekAt(1) in ['0'..'7']) then
begin
ReadNumber;
Result := FToken;
Exit;
end;
{ Strings: ' or # or ^ }
if c in ['''', '#'] then
begin
ReadString;
Result := FToken;
Exit;
end;
if (c = '^') and (PeekAt(1) in ['A'..'Z', 'a'..'z']) then
begin
ReadString;
Result := FToken;
Exit;
end;
{ Comments and directives }
if c = '{' then
begin
ReadBraceCommentOrDirective;
Result := FToken;
Exit;
end;
c2 := PeekAt(1);
if (c = '(') and (c2 = '*') then
begin
ReadParenStarCommentOrDirective;
Result := FToken;
Exit;
end;
if (c = '/') and (c2 = '/') then
begin
ReadLineComment;
Result := FToken;
Exit;
end;
{ Symbols and operators }
ReadSymbol;
Result := FToken;
end;
function TFpgPascalTokeniser.TokenText: string;
begin
if (FToken.TextStart >= 1) and (FToken.Len > 0) and
(FToken.TextStart + FToken.Len - 1 <= Length(FSource)) then
Result := Copy(FSource, FToken.TextStart, FToken.Len)
else
Result := '';
end;
function TFpgPascalTokeniser.TokenTextUpper: string;
begin
Result := UpCase(TokenText);
end;
end.

View file

@ -0,0 +1,29 @@
program TestRunner;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}
cthreads,
{$ENDIF}
Classes,
fpcunit,
testregistry,
consoletestrunner,
cp.test.lexer,
cp.test.parser,
cp.test.codegen;
var
Application: TTestRunner;
begin
Application := TTestRunner.Create(nil);
try
Application.Initialize;
Application.Title := 'Blaise Compiler Unit Tests';
Application.Run;
finally
Application.Free;
end;
end.

View file

@ -0,0 +1,209 @@
unit cp.test.codegen;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testregistry,
uLexer, uParser, uAST, uCodeGenQBE;
type
TCodeGenTests = class(TTestCase)
private
function GenerateIR(const ASrc: string): string;
function IRContains(const AIR, AFragment: string): Boolean;
published
{ Data sections }
procedure TestHelloWorld_HasStrLitData;
procedure TestHelloWorld_HasFormatString;
{ Main function structure }
procedure TestOutput_HasMainFunction;
procedure TestOutput_HasRetZero;
{ WriteLn }
procedure TestWriteLn_NoArgs_CallsFormatNL;
procedure TestWriteLn_StringLit_CallsPrintf;
procedure TestWriteLn_IntExpr_CallsPrintfInt;
{ Variables and assignment }
procedure TestIntVar_HasAlloc;
procedure TestAssignment_HasStorew;
procedure TestAssignment_LoadAndStore;
{ Arithmetic }
procedure TestAdd_EmitsAddInstruction;
procedure TestMul_EmitsMulInstruction;
{ Header comment }
procedure TestOutput_HasSourceComment;
end;
implementation
function TCodeGenTests.GenerateIR(const ASrc: string): string;
var
L: TLexer;
P: TParser;
Pr: TProgram;
CG: TCodeGenQBE;
begin
L := TLexer.Create(ASrc);
P := TParser.Create(L);
Pr := P.Parse;
CG := TCodeGenQBE.Create;
try
CG.Generate(Pr);
Result := CG.GetOutput;
finally
CG.Free;
Pr.Free;
P.Free;
L.Free;
end;
end;
function TCodeGenTests.IRContains(const AIR, AFragment: string): Boolean;
begin
Result := Pos(AFragment, AIR) > 0;
end;
{ Data sections }
procedure TCodeGenTests.TestHelloWorld_HasStrLitData;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(''Hello'') end.');
AssertTrue('Has str data',
IRContains(IR, 'data $__s0'));
AssertTrue('Contains Hello',
IRContains(IR, '"Hello"'));
end;
procedure TCodeGenTests.TestHelloWorld_HasFormatString;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(''Hello'') end.');
AssertTrue('Has fmt_s_nl',
IRContains(IR, '$__fmt_s_nl'));
end;
{ Main function structure }
procedure TCodeGenTests.TestOutput_HasMainFunction;
var
IR: string;
begin
IR := GenerateIR('program P; begin end.');
AssertTrue('Has export function $main',
IRContains(IR, 'export function w $main()'));
end;
procedure TCodeGenTests.TestOutput_HasRetZero;
var
IR: string;
begin
IR := GenerateIR('program P; begin end.');
AssertTrue('Has ret 0', IRContains(IR, 'ret 0'));
end;
{ WriteLn }
procedure TCodeGenTests.TestWriteLn_NoArgs_CallsFormatNL;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn() end.');
AssertTrue('Calls printf with fmt_nl',
IRContains(IR, 'call $printf(l $__fmt_nl)'));
end;
procedure TCodeGenTests.TestWriteLn_StringLit_CallsPrintf;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(''Hi'') end.');
AssertTrue('Calls printf with s_nl format',
IRContains(IR, '$__fmt_s_nl'));
AssertTrue('Passes string pointer',
IRContains(IR, '..., l $__s0'));
end;
procedure TCodeGenTests.TestWriteLn_IntExpr_CallsPrintfInt;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(42) end.');
AssertTrue('Uses int format',
IRContains(IR, '$__fmt_d_nl'));
end;
{ Variables and assignment }
procedure TCodeGenTests.TestIntVar_HasAlloc;
var
IR: string;
begin
IR := GenerateIR('program P; var x: Integer; begin end.');
AssertTrue('Has alloc for x',
IRContains(IR, '%_var_x =l alloc4 1'));
end;
procedure TCodeGenTests.TestAssignment_HasStorew;
var
IR: string;
begin
IR := GenerateIR(
'program P; var n: Integer; begin n := 7 end.');
AssertTrue('Has storew', IRContains(IR, 'storew'));
AssertTrue('Stores to n', IRContains(IR, 'storew %_t0, %_var_n'));
end;
procedure TCodeGenTests.TestAssignment_LoadAndStore;
var
IR: string;
begin
IR := GenerateIR(
'program P; var x, y: Integer; begin x := 1; y := x end.');
AssertTrue('Loads x', IRContains(IR, 'loadw %_var_x'));
AssertTrue('Stores y', IRContains(IR, '%_var_y'));
end;
{ Arithmetic }
procedure TCodeGenTests.TestAdd_EmitsAddInstruction;
var
IR: string;
begin
IR := GenerateIR(
'program P; var n: Integer; begin n := 3 + 4 end.');
AssertTrue('Has add', IRContains(IR, '=w add'));
end;
procedure TCodeGenTests.TestMul_EmitsMulInstruction;
var
IR: string;
begin
IR := GenerateIR(
'program P; var n: Integer; begin n := 2 * 5 end.');
AssertTrue('Has mul', IRContains(IR, '=w mul'));
end;
{ Header comment }
procedure TCodeGenTests.TestOutput_HasSourceComment;
var
IR: string;
begin
IR := GenerateIR('program MyProg; begin end.');
AssertTrue('Has source comment',
IRContains(IR, '# Generated by Blaise Compiler'));
end;
initialization
RegisterTest(TCodeGenTests);
end.

View file

@ -0,0 +1,475 @@
unit cp.test.lexer;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testregistry,
uLexer;
type
TLexerTests = class(TTestCase)
private
FLexer: TLexer;
procedure SetLexer(const ASource: string);
protected
procedure TearDown; override;
published
{ EOF and whitespace }
procedure TestEmptySource_ReturnsEOF;
procedure TestWhitespaceOnly_ReturnsEOF;
{ Comment skipping }
procedure TestLineComment_Skipped;
procedure TestBlockComment_Skipped;
procedure TestBlockComment_MultiLine_Skipped;
{ Keywords }
procedure TestKeyword_Program;
procedure TestKeyword_Uses;
procedure TestKeyword_Var;
procedure TestKeyword_Begin;
procedure TestKeyword_End;
procedure TestKeywords_CaseInsensitive;
procedure TestIdent_NotKeyword_Prefix;
{ Identifiers }
procedure TestIdent_Simple;
procedure TestIdent_WithUnderscore;
procedure TestIdent_WithDigits;
{ Integer literals }
procedure TestIntLit_SingleDigit;
procedure TestIntLit_MultiDigit;
{ String literals }
procedure TestStringLit_Simple;
procedure TestStringLit_Empty;
procedure TestStringLit_EmbeddedQuote;
{ Operators and punctuation }
procedure TestOp_Plus;
procedure TestOp_Minus;
procedure TestOp_Star;
procedure TestOp_Slash;
procedure TestOp_Assign;
procedure TestOp_Colon;
procedure TestOp_LParen;
procedure TestOp_RParen;
procedure TestOp_Comma;
procedure TestOp_Semicolon;
procedure TestOp_Dot;
{ Position tracking }
procedure TestLineTracking_SecondLine;
procedure TestColTracking_AfterSpaces;
{ Token sequences }
procedure TestSeq_VarDecl;
procedure TestSeq_Assignment;
procedure TestSeq_ProcCall;
end;
implementation
procedure TLexerTests.SetLexer(const ASource: string);
begin
FreeAndNil(FLexer);
FLexer := TLexer.Create(ASource);
end;
procedure TLexerTests.TearDown;
begin
FreeAndNil(FLexer);
end;
{ EOF and whitespace }
procedure TLexerTests.TestEmptySource_ReturnsEOF;
var
tok: TToken;
begin
SetLexer('');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkEOF), Ord(tok.Kind));
end;
procedure TLexerTests.TestWhitespaceOnly_ReturnsEOF;
var
tok: TToken;
begin
SetLexer(' ' + LineEnding + ' ');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkEOF), Ord(tok.Kind));
end;
{ Comment skipping }
procedure TLexerTests.TestLineComment_Skipped;
var
tok: TToken;
begin
SetLexer('// comment' + LineEnding + 'begin');
tok := FLexer.Next;
AssertEquals('Kind after //', Ord(tkBegin), Ord(tok.Kind));
end;
procedure TLexerTests.TestBlockComment_Skipped;
var
tok: TToken;
begin
SetLexer('{ comment } begin');
tok := FLexer.Next;
AssertEquals('Kind after {}', Ord(tkBegin), Ord(tok.Kind));
end;
procedure TLexerTests.TestBlockComment_MultiLine_Skipped;
var
tok: TToken;
begin
SetLexer('{ line one' + LineEnding + ' line two }' + LineEnding + 'end');
tok := FLexer.Next;
AssertEquals('Kind after multiline {}', Ord(tkEnd), Ord(tok.Kind));
end;
{ Keywords }
procedure TLexerTests.TestKeyword_Program;
var
tok: TToken;
begin
SetLexer('program');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkProgram), Ord(tok.Kind));
AssertEquals('Value', 'program', tok.Value);
end;
procedure TLexerTests.TestKeyword_Uses;
var
tok: TToken;
begin
SetLexer('uses');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkUses), Ord(tok.Kind));
end;
procedure TLexerTests.TestKeyword_Var;
var
tok: TToken;
begin
SetLexer('var');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkVar), Ord(tok.Kind));
end;
procedure TLexerTests.TestKeyword_Begin;
var
tok: TToken;
begin
SetLexer('begin');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkBegin), Ord(tok.Kind));
end;
procedure TLexerTests.TestKeyword_End;
var
tok: TToken;
begin
SetLexer('end');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkEnd), Ord(tok.Kind));
end;
procedure TLexerTests.TestKeywords_CaseInsensitive;
var
tok: TToken;
begin
SetLexer('BEGIN');
tok := FLexer.Next;
AssertEquals('Uppercase BEGIN', Ord(tkBegin), Ord(tok.Kind));
SetLexer('End');
tok := FLexer.Next;
AssertEquals('Mixed End', Ord(tkEnd), Ord(tok.Kind));
SetLexer('PROGRAM');
tok := FLexer.Next;
AssertEquals('Uppercase PROGRAM', Ord(tkProgram), Ord(tok.Kind));
end;
procedure TLexerTests.TestIdent_NotKeyword_Prefix;
var
tok: TToken;
begin
{ "beginning" starts with "begin" but is not a keyword }
SetLexer('beginning');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkIdent), Ord(tok.Kind));
AssertEquals('Value', 'beginning', tok.Value);
end;
{ Identifiers }
procedure TLexerTests.TestIdent_Simple;
var
tok: TToken;
begin
SetLexer('Hello');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkIdent), Ord(tok.Kind));
AssertEquals('Value', 'Hello', tok.Value);
end;
procedure TLexerTests.TestIdent_WithUnderscore;
var
tok: TToken;
begin
SetLexer('_myVar');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkIdent), Ord(tok.Kind));
AssertEquals('Value', '_myVar', tok.Value);
end;
procedure TLexerTests.TestIdent_WithDigits;
var
tok: TToken;
begin
SetLexer('item2');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkIdent), Ord(tok.Kind));
AssertEquals('Value', 'item2', tok.Value);
end;
{ Integer literals }
procedure TLexerTests.TestIntLit_SingleDigit;
var
tok: TToken;
begin
SetLexer('7');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkIntLit), Ord(tok.Kind));
AssertEquals('Value', '7', tok.Value);
end;
procedure TLexerTests.TestIntLit_MultiDigit;
var
tok: TToken;
begin
SetLexer('42');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkIntLit), Ord(tok.Kind));
AssertEquals('Value', '42', tok.Value);
end;
{ String literals }
procedure TLexerTests.TestStringLit_Simple;
var
tok: TToken;
begin
SetLexer('''hello''');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkStringLit), Ord(tok.Kind));
AssertEquals('Value', 'hello', tok.Value);
end;
procedure TLexerTests.TestStringLit_Empty;
var
tok: TToken;
begin
SetLexer('''''');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkStringLit), Ord(tok.Kind));
AssertEquals('Value', '', tok.Value);
end;
procedure TLexerTests.TestStringLit_EmbeddedQuote;
var
tok: TToken;
begin
{ Pascal ''it''''s'' represents it's }
SetLexer('''it''''s''');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkStringLit), Ord(tok.Kind));
AssertEquals('Value', 'it''s', tok.Value);
end;
{ Operators and punctuation }
procedure TLexerTests.TestOp_Plus;
var
tok: TToken;
begin
SetLexer('+');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkPlus), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Minus;
var
tok: TToken;
begin
SetLexer('-');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkMinus), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Star;
var
tok: TToken;
begin
SetLexer('*');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkStar), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Slash;
var
tok: TToken;
begin
SetLexer('/');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkSlash), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Assign;
var
tok: TToken;
begin
SetLexer(':=');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkAssign), Ord(tok.Kind));
AssertEquals('Value', ':=', tok.Value);
end;
procedure TLexerTests.TestOp_Colon;
var
tok: TToken;
begin
SetLexer(':');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkColon), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_LParen;
var
tok: TToken;
begin
SetLexer('(');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkLParen), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_RParen;
var
tok: TToken;
begin
SetLexer(')');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkRParen), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Comma;
var
tok: TToken;
begin
SetLexer(',');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkComma), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Semicolon;
var
tok: TToken;
begin
SetLexer(';');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkSemicolon), Ord(tok.Kind));
end;
procedure TLexerTests.TestOp_Dot;
var
tok: TToken;
begin
SetLexer('.');
tok := FLexer.Next;
AssertEquals('Kind', Ord(tkDot), Ord(tok.Kind));
end;
{ Position tracking }
procedure TLexerTests.TestLineTracking_SecondLine;
var
tok: TToken;
begin
SetLexer('begin' + LineEnding + 'end');
tok := FLexer.Next;
AssertEquals('begin line', 1, tok.Line);
tok := FLexer.Next;
AssertEquals('end line', 2, tok.Line);
end;
procedure TLexerTests.TestColTracking_AfterSpaces;
var
tok: TToken;
begin
SetLexer(' begin');
tok := FLexer.Next;
AssertEquals('begin col', 3, tok.Col);
end;
{ Token sequences }
procedure TLexerTests.TestSeq_VarDecl;
var
t: array[0..3] of TToken;
i: Integer;
begin
SetLexer('x : Integer');
for i := 0 to 3 do
t[i] := FLexer.Next;
AssertEquals('x kind', Ord(tkIdent), Ord(t[0].Kind));
AssertEquals('x value', 'x', t[0].Value);
AssertEquals(': kind', Ord(tkColon), Ord(t[1].Kind));
AssertEquals('Integer kind', Ord(tkIdent), Ord(t[2].Kind));
AssertEquals('Integer value', 'Integer', t[2].Value);
AssertEquals('EOF', Ord(tkEOF), Ord(t[3].Kind));
end;
procedure TLexerTests.TestSeq_Assignment;
var
t: array[0..3] of TToken;
i: Integer;
begin
SetLexer('x := 42');
for i := 0 to 3 do
t[i] := FLexer.Next;
AssertEquals('x', Ord(tkIdent), Ord(t[0].Kind));
AssertEquals(':=', Ord(tkAssign), Ord(t[1].Kind));
AssertEquals('42 kind', Ord(tkIntLit), Ord(t[2].Kind));
AssertEquals('42 value', '42', t[2].Value);
AssertEquals('EOF', Ord(tkEOF), Ord(t[3].Kind));
end;
procedure TLexerTests.TestSeq_ProcCall;
var
t: array[0..4] of TToken;
i: Integer;
begin
SetLexer('WriteLn(''hi'')');
for i := 0 to 4 do
t[i] := FLexer.Next;
AssertEquals('WriteLn kind', Ord(tkIdent), Ord(t[0].Kind));
AssertEquals('( kind', Ord(tkLParen), Ord(t[1].Kind));
AssertEquals('str kind', Ord(tkStringLit), Ord(t[2].Kind));
AssertEquals('str value', 'hi', t[2].Value);
AssertEquals(') kind', Ord(tkRParen), Ord(t[3].Kind));
AssertEquals('EOF', Ord(tkEOF), Ord(t[4].Kind));
end;
initialization
RegisterTest(TLexerTests);
end.

View file

@ -0,0 +1,425 @@
unit cp.test.parser;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testregistry,
uLexer, uParser, uAST;
type
TParserTests = class(TTestCase)
private
function ParseSource(const ASrc: string): TProgram;
published
{ Program structure }
procedure TestMinimalProgram;
procedure TestProgramName;
procedure TestProgramWithUses;
procedure TestProgramWithMultipleUses;
{ Var block }
procedure TestSingleVarDecl;
procedure TestMultipleVarDecls;
procedure TestMultiNameVarDecl;
{ Statements }
procedure TestEmptyBeginEnd;
procedure TestAssignment_IntLit;
procedure TestAssignment_StringLit;
procedure TestProcCall_NoArgs;
procedure TestProcCall_NoParens;
procedure TestProcCall_OneStringArg;
procedure TestProcCall_OneIntArg;
{ Expressions }
procedure TestExpr_Addition;
procedure TestExpr_Subtraction;
procedure TestExpr_Multiplication;
procedure TestExpr_Precedence_MulBeforeAdd;
procedure TestExpr_Parenthesised;
procedure TestExpr_IdentInExpr;
{ Error cases }
procedure TestError_MissingProgramKeyword;
procedure TestError_MissingDot;
end;
implementation
function TParserTests.ParseSource(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;
{ Program structure }
procedure TParserTests.TestMinimalProgram;
var
Prog: TProgram;
begin
Prog := ParseSource('program Empty; begin end.');
try
AssertNotNull('TProgram', Prog);
AssertNotNull('Block', Prog.Block);
AssertEquals('No decls', 0, Prog.Block.Decls.Count);
AssertEquals('No stmts', 0, Prog.Block.Stmts.Count);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProgramName;
var
Prog: TProgram;
begin
Prog := ParseSource('program MyApp; begin end.');
try
AssertEquals('Name', 'MyApp', Prog.Name);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProgramWithUses;
var
Prog: TProgram;
begin
Prog := ParseSource('program P; uses System; begin end.');
try
AssertEquals('Uses count', 1, Prog.Uses.Count);
AssertEquals('Unit name', 'System', Prog.Uses[0]);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProgramWithMultipleUses;
var
Prog: TProgram;
begin
Prog := ParseSource('program P; uses System, SysUtils; begin end.');
try
AssertEquals('Uses count', 2, Prog.Uses.Count);
AssertEquals('First', 'System', Prog.Uses[0]);
AssertEquals('Second', 'SysUtils', Prog.Uses[1]);
finally
Prog.Free;
end;
end;
{ Var block }
procedure TParserTests.TestSingleVarDecl;
var
Prog: TProgram;
Decl: TVarDecl;
begin
Prog := ParseSource('program P; var x: Integer; begin end.');
try
AssertEquals('1 decl', 1, Prog.Block.Decls.Count);
Decl := TVarDecl(Prog.Block.Decls[0]);
AssertEquals('1 name', 1, Decl.Names.Count);
AssertEquals('Name', 'x', Decl.Names[0]);
AssertEquals('Type', 'Integer', Decl.TypeName);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestMultipleVarDecls;
var
Prog: TProgram;
begin
Prog := ParseSource(
'program P; var x: Integer; s: string; begin end.');
try
AssertEquals('2 decls', 2, Prog.Block.Decls.Count);
AssertEquals('First type', 'Integer',
TVarDecl(Prog.Block.Decls[0]).TypeName);
AssertEquals('Second type', 'string',
TVarDecl(Prog.Block.Decls[1]).TypeName);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestMultiNameVarDecl;
var
Prog: TProgram;
Decl: TVarDecl;
begin
Prog := ParseSource('program P; var x, y: Integer; begin end.');
try
AssertEquals('1 decl group', 1, Prog.Block.Decls.Count);
Decl := TVarDecl(Prog.Block.Decls[0]);
AssertEquals('2 names', 2, Decl.Names.Count);
AssertEquals('First', 'x', Decl.Names[0]);
AssertEquals('Second', 'y', Decl.Names[1]);
finally
Prog.Free;
end;
end;
{ Statements }
procedure TParserTests.TestEmptyBeginEnd;
var
Prog: TProgram;
begin
Prog := ParseSource('program P; begin end.');
try
AssertEquals('No stmts', 0, Prog.Block.Stmts.Count);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestAssignment_IntLit;
var
Prog: TProgram;
Assign: TAssignment;
Lit: TIntLiteral;
begin
Prog := ParseSource('program P; var n: Integer; begin n := 42 end.');
try
AssertEquals('1 stmt', 1, Prog.Block.Stmts.Count);
AssertTrue('Is TAssignment',
Prog.Block.Stmts[0] is TAssignment);
Assign := TAssignment(Prog.Block.Stmts[0]);
AssertEquals('Name', 'n', Assign.Name);
AssertTrue('Expr is TIntLiteral', Assign.Expr is TIntLiteral);
Lit := TIntLiteral(Assign.Expr);
AssertEquals('Value', 42, Lit.Value);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestAssignment_StringLit;
var
Prog: TProgram;
Assign: TAssignment;
begin
Prog := ParseSource(
'program P; var s: string; begin s := ''hello'' end.');
try
Assign := TAssignment(Prog.Block.Stmts[0]);
AssertTrue('Expr is TStringLiteral', Assign.Expr is TStringLiteral);
AssertEquals('Value', 'hello',
TStringLiteral(Assign.Expr).Value);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProcCall_NoArgs;
var
Prog: TProgram;
Call: TProcCall;
begin
Prog := ParseSource('program P; begin WriteLn() end.');
try
AssertTrue('Is TProcCall',
Prog.Block.Stmts[0] is TProcCall);
Call := TProcCall(Prog.Block.Stmts[0]);
AssertEquals('Name', 'WriteLn', Call.Name);
AssertEquals('0 args', 0, Call.Args.Count);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProcCall_NoParens;
var
Prog: TProgram;
Call: TProcCall;
begin
Prog := ParseSource('program P; begin WriteLn end.');
try
Call := TProcCall(Prog.Block.Stmts[0]);
AssertEquals('Name', 'WriteLn', Call.Name);
AssertEquals('0 args', 0, Call.Args.Count);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProcCall_OneStringArg;
var
Prog: TProgram;
Call: TProcCall;
begin
Prog := ParseSource('program P; begin WriteLn(''Hello'') end.');
try
Call := TProcCall(Prog.Block.Stmts[0]);
AssertEquals('1 arg', 1, Call.Args.Count);
AssertTrue('Arg is TStringLiteral',
Call.Args[0] is TStringLiteral);
AssertEquals('Value', 'Hello',
TStringLiteral(Call.Args[0]).Value);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestProcCall_OneIntArg;
var
Prog: TProgram;
Call: TProcCall;
begin
Prog := ParseSource('program P; begin WriteLn(99) end.');
try
Call := TProcCall(Prog.Block.Stmts[0]);
AssertEquals('1 arg', 1, Call.Args.Count);
AssertTrue('Arg is TIntLiteral', Call.Args[0] is TIntLiteral);
AssertEquals('Value', 99, TIntLiteral(Call.Args[0]).Value);
finally
Prog.Free;
end;
end;
{ Expressions }
procedure TParserTests.TestExpr_Addition;
var
Prog: TProgram;
Bin: TBinaryExpr;
begin
Prog := ParseSource(
'program P; var n: Integer; begin n := 1 + 2 end.');
try
Bin := TBinaryExpr(TAssignment(Prog.Block.Stmts[0]).Expr);
AssertEquals('Op', Ord(boAdd), Ord(Bin.Op));
AssertEquals('Left', 1, TIntLiteral(Bin.Left).Value);
AssertEquals('Right', 2, TIntLiteral(Bin.Right).Value);
finally
Prog.Free;
end;
end;
procedure TParserTests.TestExpr_Subtraction;
var
Prog: TProgram;
Bin: TBinaryExpr;
begin
Prog := ParseSource(
'program P; var n: Integer; begin n := 10 - 3 end.');
try
Bin := TBinaryExpr(TAssignment(Prog.Block.Stmts[0]).Expr);
AssertEquals('Op', Ord(boSub), Ord(Bin.Op));
finally
Prog.Free;
end;
end;
procedure TParserTests.TestExpr_Multiplication;
var
Prog: TProgram;
Bin: TBinaryExpr;
begin
Prog := ParseSource(
'program P; var n: Integer; begin n := 3 * 4 end.');
try
Bin := TBinaryExpr(TAssignment(Prog.Block.Stmts[0]).Expr);
AssertEquals('Op', Ord(boMul), Ord(Bin.Op));
finally
Prog.Free;
end;
end;
procedure TParserTests.TestExpr_Precedence_MulBeforeAdd;
var
Prog: TProgram;
Outer: TBinaryExpr;
Inner: TBinaryExpr;
begin
{ 1 + 2 * 3 should parse as 1 + (2 * 3) }
Prog := ParseSource(
'program P; var n: Integer; begin n := 1 + 2 * 3 end.');
try
Outer := TBinaryExpr(TAssignment(Prog.Block.Stmts[0]).Expr);
AssertEquals('Outer op is Add', Ord(boAdd), Ord(Outer.Op));
AssertTrue('Right is TBinaryExpr', Outer.Right is TBinaryExpr);
Inner := TBinaryExpr(Outer.Right);
AssertEquals('Inner op is Mul', Ord(boMul), Ord(Inner.Op));
finally
Prog.Free;
end;
end;
procedure TParserTests.TestExpr_Parenthesised;
var
Prog: TProgram;
Outer: TBinaryExpr;
Inner: TBinaryExpr;
begin
{ (1 + 2) * 3 — outer should be Mul, left child is Add }
Prog := ParseSource(
'program P; var n: Integer; begin n := (1 + 2) * 3 end.');
try
Outer := TBinaryExpr(TAssignment(Prog.Block.Stmts[0]).Expr);
AssertEquals('Outer op is Mul', Ord(boMul), Ord(Outer.Op));
AssertTrue('Left is TBinaryExpr', Outer.Left is TBinaryExpr);
Inner := TBinaryExpr(Outer.Left);
AssertEquals('Inner op is Add', Ord(boAdd), Ord(Inner.Op));
finally
Prog.Free;
end;
end;
procedure TParserTests.TestExpr_IdentInExpr;
var
Prog: TProgram;
Assign: TAssignment;
Bin: TBinaryExpr;
begin
Prog := ParseSource(
'program P; var x, y: Integer; begin y := x + 1 end.');
try
Assign := TAssignment(Prog.Block.Stmts[0]);
Bin := TBinaryExpr(Assign.Expr);
AssertTrue('Left is TIdentExpr', Bin.Left is TIdentExpr);
AssertEquals('Ident name', 'x', TIdentExpr(Bin.Left).Name);
finally
Prog.Free;
end;
end;
{ Error cases }
procedure TParserTests.TestError_MissingProgramKeyword;
begin
try
ParseSource('begin end.').Free;
Fail('Expected EParseError');
except
on E: EParseError do ; { expected }
end;
end;
procedure TParserTests.TestError_MissingDot;
begin
try
ParseSource('program P; begin end').Free;
Fail('Expected EParseError');
except
on E: EParseError do ; { expected }
end;
end;
initialization
RegisterTest(TParserTests);
end.

View file

@ -1,4 +1,4 @@
= Modern Object Pascal Compiler — Clean Pascal
= Blaise — Modern Object Pascal Compiler
:doctype: article
:toc: left
:toclevels: 3
@ -250,14 +250,14 @@ PasBuild invokes the compiler binary as:
[source,shell]
----
cleanpascal --project project.xml --config debug --output bin/hello
blaise --project project.xml --config debug --output bin/hello
----
Or for single-file compilation during bootstrap:
[source,shell]
----
cleanpascal --source Hello.pas --output bin/hello --target linux-x86_64
blaise --source Hello.pas --output bin/hello --target linux-x86_64
----
Supported flags:

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>clean-pascal</name>
<name>blaise</name>
<version>0.1.0-SNAPSHOT</version>
<author>Graeme Geldenhuys</author>
<license>BSD-3-Clause</license>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>clean-pascal-rtl</name>
<name>blaise-rtl</name>
<!-- version inherited from root aggregator -->
<build>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project>
<name>clean-pascal-migration</name>
<name>blaise-migration</name>
<!-- version inherited from root aggregator -->
<build>