blaise/compiler/src/test/pascal/cp.test.codegen.pas

355 lines
9.4 KiB
ObjectPascal
Raw Normal View History

{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit cp.test.codegen;
{$mode objfpc}{$H+}
interface
uses
Classes, SysUtils, fpcunit, testregistry,
Add Phase 2: record/class types, ARC strings, RTL stubs, and grammar doc Symbol table: - Added tyClass kind and NewClassType factory; TRecordTypeDesc now accepts an optional kind parameter so records and classes share the same descriptor with distinct semantics. Semantic analyser: - AnalyseTypeDecls runs before PushScope so type symbols land in global scope and survive PopScope. - Handles TRecordTypeDef and TClassTypeDef; sets IsConstructorCall on TypeName.Create expressions and IsClassAccess on class-variable field access and assignment nodes. Code generator (QBE IR): - Class variables: 8-byte pointer slot, zeroed on entry. - Constructor calls: malloc(sizeof fields), store pointer. - Class field access/write: load pointer, add field offset, load/store. - ARC string assignment: AddRef new value, Release old value, then store. - Block exit: Release every string variable in scope (EmitStringCleanup). Lexer / Parser / AST: - Added tkClass keyword and ParseClassDef; FieldDecl parsing refactored into ParseFieldDecl(AFields) shared by both record and class. - AST gains TClassTypeDef, and IsConstructorCall/IsClassAccess flags on TFieldAccessExpr and TFieldAssignment. RTL: - blaise_arc.c: Phase 2 no-op stubs for _StringAddRef / _StringRelease. - rtl/Makefile: builds blaise_rtl.a; `make install` copies it next to the compiler binary for automatic discovery by FindRTL. - Blaise.pas driver: FindRTL checks BLAISE_RTL env var then binary dir; links the RTL archive when present. Tests: - 180 unit tests (records ×23, classes ×22, ARC ×9, codegen ×9, …). - 4 end-to-end integration tests in tests/integration/test_arc_strings.sh covering string assignment, two-var cleanup, reassignment cycle, and empty-program linkage. Docs: - design.adoc updated with Phase 2 implementation status table. - docs/grammar.ebnf: new authoritative EBNF grammar for the Blaise language as implemented, including ARC semantic annotations.
2026-04-20 21:04:12 +03:00
uLexer, uParser, uAST, uSemantic, 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_UsesSysWriteStr;
{ Main function structure }
procedure TestOutput_HasMainFunction;
procedure TestOutput_HasRetZero;
{ WriteLn }
procedure TestWriteLn_NoArgs_CallsSysWriteNewline;
procedure TestWriteLn_StringLit_CallsSysWriteStr;
procedure TestWriteLn_IntExpr_CallsSysWriteInt;
{ Variables and assignment }
procedure TestIntVar_HasAlloc;
procedure TestAssignment_HasStorew;
procedure TestAssignment_LoadAndStore;
{ Arithmetic }
procedure TestAdd_EmitsAddInstruction;
procedure TestMul_EmitsMulInstruction;
{ Header comment }
procedure TestOutput_HasSourceComment;
{ String equality }
procedure TestStringEq_EmitsStringEquals;
procedure TestStringNe_EmitsStringEqualsNegated;
procedure TestStringEq_SemanticCompiles;
{ True / False built-in constants }
procedure TestTrue_EmitsCopyOne;
procedure TestFalse_EmitsCopyZero;
procedure TestTrue_AssignToBoolVar;
procedure TestFalse_AssignToBoolVar;
procedure TestTrue_InIfCondition;
procedure TestBoolFunc_ReturnTrue;
end;
implementation
function TCodeGenTests.GenerateIR(const ASrc: string): string;
var
L: TLexer;
P: TParser;
Pr: TProgram;
Add Phase 2: record/class types, ARC strings, RTL stubs, and grammar doc Symbol table: - Added tyClass kind and NewClassType factory; TRecordTypeDesc now accepts an optional kind parameter so records and classes share the same descriptor with distinct semantics. Semantic analyser: - AnalyseTypeDecls runs before PushScope so type symbols land in global scope and survive PopScope. - Handles TRecordTypeDef and TClassTypeDef; sets IsConstructorCall on TypeName.Create expressions and IsClassAccess on class-variable field access and assignment nodes. Code generator (QBE IR): - Class variables: 8-byte pointer slot, zeroed on entry. - Constructor calls: malloc(sizeof fields), store pointer. - Class field access/write: load pointer, add field offset, load/store. - ARC string assignment: AddRef new value, Release old value, then store. - Block exit: Release every string variable in scope (EmitStringCleanup). Lexer / Parser / AST: - Added tkClass keyword and ParseClassDef; FieldDecl parsing refactored into ParseFieldDecl(AFields) shared by both record and class. - AST gains TClassTypeDef, and IsConstructorCall/IsClassAccess flags on TFieldAccessExpr and TFieldAssignment. RTL: - blaise_arc.c: Phase 2 no-op stubs for _StringAddRef / _StringRelease. - rtl/Makefile: builds blaise_rtl.a; `make install` copies it next to the compiler binary for automatic discovery by FindRTL. - Blaise.pas driver: FindRTL checks BLAISE_RTL env var then binary dir; links the RTL archive when present. Tests: - 180 unit tests (records ×23, classes ×22, ARC ×9, codegen ×9, …). - 4 end-to-end integration tests in tests/integration/test_arc_strings.sh covering string assignment, two-var cleanup, reassignment cycle, and empty-program linkage. Docs: - design.adoc updated with Phase 2 implementation status table. - docs/grammar.ebnf: new authoritative EBNF grammar for the Blaise language as implemented, including ARC semantic annotations.
2026-04-20 21:04:12 +03:00
A: TSemanticAnalyser;
CG: TCodeGenQBE;
begin
L := TLexer.Create(ASrc);
P := TParser.Create(L);
Pr := P.Parse;
Add Phase 2: record/class types, ARC strings, RTL stubs, and grammar doc Symbol table: - Added tyClass kind and NewClassType factory; TRecordTypeDesc now accepts an optional kind parameter so records and classes share the same descriptor with distinct semantics. Semantic analyser: - AnalyseTypeDecls runs before PushScope so type symbols land in global scope and survive PopScope. - Handles TRecordTypeDef and TClassTypeDef; sets IsConstructorCall on TypeName.Create expressions and IsClassAccess on class-variable field access and assignment nodes. Code generator (QBE IR): - Class variables: 8-byte pointer slot, zeroed on entry. - Constructor calls: malloc(sizeof fields), store pointer. - Class field access/write: load pointer, add field offset, load/store. - ARC string assignment: AddRef new value, Release old value, then store. - Block exit: Release every string variable in scope (EmitStringCleanup). Lexer / Parser / AST: - Added tkClass keyword and ParseClassDef; FieldDecl parsing refactored into ParseFieldDecl(AFields) shared by both record and class. - AST gains TClassTypeDef, and IsConstructorCall/IsClassAccess flags on TFieldAccessExpr and TFieldAssignment. RTL: - blaise_arc.c: Phase 2 no-op stubs for _StringAddRef / _StringRelease. - rtl/Makefile: builds blaise_rtl.a; `make install` copies it next to the compiler binary for automatic discovery by FindRTL. - Blaise.pas driver: FindRTL checks BLAISE_RTL env var then binary dir; links the RTL archive when present. Tests: - 180 unit tests (records ×23, classes ×22, ARC ×9, codegen ×9, …). - 4 end-to-end integration tests in tests/integration/test_arc_strings.sh covering string assignment, two-var cleanup, reassignment cycle, and empty-program linkage. Docs: - design.adoc updated with Phase 2 implementation status table. - docs/grammar.ebnf: new authoritative EBNF grammar for the Blaise language as implemented, including ARC semantic annotations.
2026-04-20 21:04:12 +03:00
A := TSemanticAnalyser.Create;
try
A.Analyse(Pr);
finally
A.Free;
end;
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_UsesSysWriteStr;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(''Hello'') end.');
AssertTrue('Calls _SysWriteStr',
IRContains(IR, 'call $_SysWriteStr('));
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_CallsSysWriteNewline;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn() end.');
AssertTrue('Calls _SysWriteNewline',
IRContains(IR, 'call $_SysWriteNewline(w 1)'));
end;
procedure TCodeGenTests.TestWriteLn_StringLit_CallsSysWriteStr;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(''Hi'') end.');
AssertTrue('Calls _SysWriteStr with string pointer',
IRContains(IR, 'call $_SysWriteStr(w 1,'));
AssertTrue('Calls _SysWriteNewline for line ending',
IRContains(IR, 'call $_SysWriteNewline(w 1)'));
end;
procedure TCodeGenTests.TestWriteLn_IntExpr_CallsSysWriteInt;
var
IR: string;
begin
IR := GenerateIR('program P; begin WriteLn(42) end.');
AssertTrue('Calls _SysWriteInt',
IRContains(IR, 'call $_SysWriteInt(w 1,'));
AssertTrue('Calls _SysWriteNewline for line ending',
IRContains(IR, 'call $_SysWriteNewline(w 1)'));
end;
{ Variables and assignment }
procedure TCodeGenTests.TestIntVar_HasAlloc;
var
IR: string;
begin
IR := GenerateIR('program P; var x: Integer; begin end.');
Achieve self-hosting for hello-world compilation The blaise compiler now compiles its own source (tests/blaise-compiler.pas) into a second-stage binary that successfully compiles and runs a hello-world program. All 834 compiler tests still pass. Codegen and RTL fixes that unblocked self-hosting: - String literal interning (uCodeGenQBE): set FStrLits.CaseSensitive := True. FPC's TStringList.IndexOf is case-insensitive by default, so distinct literals like 'WRITELN' and 'WriteLn' collapsed to the same label. - ARC on implicit Self.Field assignments (uCodeGenQBE): EmitAssignment's implicit-Self branch did a raw storel for class/string fields. Fresh _ClassAlloc'ed objects start at refcnt 0, so without the addref they were freed by later local releases. Now addrefs new value and releases old. - #nn character literals (uLexer): UnescapeString handled only 'text'. Extended to decode #nn decimal literals and concatenated forms like 'abc'#13#10'def'. Local OrdAt helper lets the body parse under FPC and the self-hosted compiler. - Chr() builtin: registered in uSymbolTable, handled in uSemantic, emitted in uCodeGenQBE as _Chr, implemented in rtl/blaise_str.c. - LF line separator (Classes.pas): TStringList.GetText used #13#10; QBE rejects CR in its input. Changed to #10. Self-hosting source (tests/blaise-compiler.pas) — needs a virtual Destroy on TASTNode and Exception (post-TObject-stripping they became root classes without a vtable, breaking `is` checks and exception message layout). TAST* descendants chain `override`. Migration tooling: add tools/migrate_full.py — the script that generates tests/blaise-compiler.pas by flattening the eight compiler units and RTL Classes unit into a single self-contained Pascal source. The postprocess() step injects virtual Destroy on TASTNode / TTypeDesc and strips `override` from non-vtable root classes.
2026-04-24 02:21:43 +03:00
{ Program-level var is a data-section global, not a stack alloc }
AssertTrue('Has data decl for x',
IRContains(IR, 'data $x'));
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'));
Achieve self-hosting for hello-world compilation The blaise compiler now compiles its own source (tests/blaise-compiler.pas) into a second-stage binary that successfully compiles and runs a hello-world program. All 834 compiler tests still pass. Codegen and RTL fixes that unblocked self-hosting: - String literal interning (uCodeGenQBE): set FStrLits.CaseSensitive := True. FPC's TStringList.IndexOf is case-insensitive by default, so distinct literals like 'WRITELN' and 'WriteLn' collapsed to the same label. - ARC on implicit Self.Field assignments (uCodeGenQBE): EmitAssignment's implicit-Self branch did a raw storel for class/string fields. Fresh _ClassAlloc'ed objects start at refcnt 0, so without the addref they were freed by later local releases. Now addrefs new value and releases old. - #nn character literals (uLexer): UnescapeString handled only 'text'. Extended to decode #nn decimal literals and concatenated forms like 'abc'#13#10'def'. Local OrdAt helper lets the body parse under FPC and the self-hosted compiler. - Chr() builtin: registered in uSymbolTable, handled in uSemantic, emitted in uCodeGenQBE as _Chr, implemented in rtl/blaise_str.c. - LF line separator (Classes.pas): TStringList.GetText used #13#10; QBE rejects CR in its input. Changed to #10. Self-hosting source (tests/blaise-compiler.pas) — needs a virtual Destroy on TASTNode and Exception (post-TObject-stripping they became root classes without a vtable, breaking `is` checks and exception message layout). TAST* descendants chain `override`. Migration tooling: add tools/migrate_full.py — the script that generates tests/blaise-compiler.pas by flattening the eight compiler units and RTL Classes unit into a single self-contained Pascal source. The postprocess() step injects virtual Destroy on TASTNode / TTypeDesc and strips `override` from non-vtable root classes.
2026-04-24 02:21:43 +03:00
{ Program-level var n is a global: store goes to $n }
AssertTrue('Stores to n', IRContains(IR, 'storew %_t0, $n'));
end;
procedure TCodeGenTests.TestAssignment_LoadAndStore;
var
IR: string;
begin
IR := GenerateIR(
'program P; var x, y: Integer; begin x := 1; y := x end.');
Achieve self-hosting for hello-world compilation The blaise compiler now compiles its own source (tests/blaise-compiler.pas) into a second-stage binary that successfully compiles and runs a hello-world program. All 834 compiler tests still pass. Codegen and RTL fixes that unblocked self-hosting: - String literal interning (uCodeGenQBE): set FStrLits.CaseSensitive := True. FPC's TStringList.IndexOf is case-insensitive by default, so distinct literals like 'WRITELN' and 'WriteLn' collapsed to the same label. - ARC on implicit Self.Field assignments (uCodeGenQBE): EmitAssignment's implicit-Self branch did a raw storel for class/string fields. Fresh _ClassAlloc'ed objects start at refcnt 0, so without the addref they were freed by later local releases. Now addrefs new value and releases old. - #nn character literals (uLexer): UnescapeString handled only 'text'. Extended to decode #nn decimal literals and concatenated forms like 'abc'#13#10'def'. Local OrdAt helper lets the body parse under FPC and the self-hosted compiler. - Chr() builtin: registered in uSymbolTable, handled in uSemantic, emitted in uCodeGenQBE as _Chr, implemented in rtl/blaise_str.c. - LF line separator (Classes.pas): TStringList.GetText used #13#10; QBE rejects CR in its input. Changed to #10. Self-hosting source (tests/blaise-compiler.pas) — needs a virtual Destroy on TASTNode and Exception (post-TObject-stripping they became root classes without a vtable, breaking `is` checks and exception message layout). TAST* descendants chain `override`. Migration tooling: add tools/migrate_full.py — the script that generates tests/blaise-compiler.pas by flattening the eight compiler units and RTL Classes unit into a single self-contained Pascal source. The postprocess() step injects virtual Destroy on TASTNode / TTypeDesc and strips `override` from non-vtable root classes.
2026-04-24 02:21:43 +03:00
{ Program-level vars x, y are globals: load/store use $name }
AssertTrue('Loads x', IRContains(IR, 'loadw $x'));
AssertTrue('Stores y', IRContains(IR, '$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;
{ String equality }
procedure TCodeGenTests.TestStringEq_EmitsStringEquals;
var
IR: string;
begin
IR := GenerateIR(
'program P;' + LineEnding +
'var S1, S2: string; B: Boolean;' + LineEnding +
'begin' + LineEnding +
' B := S1 = S2' + LineEnding +
'end.');
AssertTrue('Uses _StringEquals', IRContains(IR, '$_StringEquals'));
end;
procedure TCodeGenTests.TestStringNe_EmitsStringEqualsNegated;
var
IR: string;
begin
IR := GenerateIR(
'program P;' + LineEnding +
'var S1, S2: string; B: Boolean;' + LineEnding +
'begin' + LineEnding +
' B := S1 <> S2' + LineEnding +
'end.');
AssertTrue('Uses _StringEquals for <>', IRContains(IR, '$_StringEquals'));
end;
procedure TCodeGenTests.TestStringEq_SemanticCompiles;
var
IR: string;
begin
IR := GenerateIR(
'program P;' + LineEnding +
'function Same(A, B: string): Boolean;' + LineEnding +
'begin' + LineEnding +
' Result := A = B' + LineEnding +
'end;' + LineEnding +
'var B: Boolean;' + LineEnding +
'begin' + LineEnding +
' B := Same(''hello'', ''world'')' + LineEnding +
'end.');
AssertTrue('Compiles', Length(IR) > 0);
AssertTrue('Uses _StringEquals', IRContains(IR, '$_StringEquals'));
end;
{ True / False built-in constants }
procedure TCodeGenTests.TestTrue_EmitsCopyOne;
var
IR: string;
begin
IR := GenerateIR(
'program P; var B: Boolean; begin B := True end.');
AssertTrue('True emits copy 1', IRContains(IR, 'copy 1'));
end;
procedure TCodeGenTests.TestFalse_EmitsCopyZero;
var
IR: string;
begin
IR := GenerateIR(
'program P; var B: Boolean; begin B := False end.');
AssertTrue('False emits copy 0', IRContains(IR, 'copy 0'));
end;
procedure TCodeGenTests.TestTrue_AssignToBoolVar;
var
IR: string;
begin
IR := GenerateIR(
'program P; var B: Boolean; begin B := True end.');
AssertTrue('Compiles to IR', Length(IR) > 0);
end;
procedure TCodeGenTests.TestFalse_AssignToBoolVar;
var
IR: string;
begin
IR := GenerateIR(
'program P; var B: Boolean; begin B := False end.');
AssertTrue('Compiles to IR', Length(IR) > 0);
end;
procedure TCodeGenTests.TestTrue_InIfCondition;
var
IR: string;
begin
IR := GenerateIR(
'program P; var N: Integer; begin if True then N := 1 end.');
AssertTrue('Compiles to IR', Length(IR) > 0);
AssertTrue('Has conditional branch', IRContains(IR, 'jnz'));
end;
procedure TCodeGenTests.TestBoolFunc_ReturnTrue;
var
IR: string;
begin
IR := GenerateIR(
'program P;' + LineEnding +
'function IsOK: Boolean;' + LineEnding +
'begin' + LineEnding +
' Result := True' + LineEnding +
'end;' + LineEnding +
'var B: Boolean;' + LineEnding +
'begin' + LineEnding +
' B := IsOK' + LineEnding +
'end.');
AssertTrue('IsOK function emitted', IRContains(IR, '$IsOK'));
AssertTrue('True emits copy 1', IRContains(IR, 'copy 1'));
end;
initialization
RegisterTest(TCodeGenTests);
end.