Commit graph

80 commits

Author SHA1 Message Date
Graeme Geldenhuys 5907e1d314 hand source: TObjectList takes strong refs to stored objects
The hand-written self-hosting TObjectList stored bare Pointers and
took no ARC ref when adding.  In loops like

    while Check(tkIdent) do
    begin
      CD := TConstDecl.Create;
      ...
      ABlock.ConstDecls.Add(CD);
    end;

reassigning CD on the next iteration released the prior CD (its only
strong ref) and the list's pointer dangled.  When a later allocation
reused that heap slot, the list silently contained the wrong object,
and semantic analysis crashed in TSymbol.Create reading CD.Name from
an unrelated object's memory.

Add/Put/Delete/Clear/Destroy now AddRef on insert and Release on
remove or destroy, gated on FOwnsObjects so the semantics match the
real compiler's TObjectList.  Extract continues to transfer the ref
to the caller without release.

Lets stage-2 compile the hand source far enough to hit the next bug
(currently a nil Self in TRecordTypeDesc.AddVTableSlot during
AnalyseTypeDecls) — more hand-source polish to follow.
2026-04-24 17:59:21 +01:00
Graeme Geldenhuys 4797ce71a9 fix: drain child process output to avoid pipe deadlock
RunProcess used poWaitOnExit combined with poUsePipes, which
deadlocks when the child fills the stderr pipe buffer before we
read — the child blocks waiting for the reader, we block waiting for
exit.  Drain the pipe in a loop while the child runs, then wait on
exit separately.  Also fold stderr into stdout so qbe/cc diagnostics
round-trip through a single handle.

rtl/Makefile: install blaise_rtl.a into compiler/target (not
compiler/target/bin) so it lives next to the blaise binary produced
by pasbuild.
2026-04-24 17:42:54 +01:00
Graeme Geldenhuys 979943d274 codegen: short-circuit boolean and/or, fix class-expr field assign
Two codegen bugs surfaced while debugging self-hosting. Both were
previously invisible in unit tests because they only bite when the
compiled programme exercises the affected shapes.

EmitFieldAssignment emitted a spurious `loadl` for the receiver when
ObjExpr was set and IsClassAccess was True, dereferencing the class
pointer a second time. That read the object's vtable, and the ARC
write-barrier then released a method pointer and stored nil into the
vtable slot. Broke statements like `TFoo(X).Field := nil`.

boAnd and boOr were emitted as bitwise `and`/`or`, evaluating both
sides unconditionally. FPC and Delphi use short-circuit by default,
and guarded nil-checks like `(P <> nil) and P.IsX` rely on it. Now
emit a stack-slot-backed branch that skips the RHS when the result is
already determined.

The two boolean-op codegen tests updated to look for the new sc_rhs
and sc_end labels.
2026-04-24 17:42:34 +01:00
Graeme Geldenhuys 2cc192cd57 Stub program unit for the migration analyser. 2026-04-24 08:12:30 +01:00
Graeme Geldenhuys 916bf12af7 build: disable blaise-rtl from default reactor build
RTL Pascal sources (System.pas, Classes.pas, etc.) are compiled by
the Blaise compiler, not FPC. C sources are compiled separately via
GCC. Marking blaise-rtl as activeByDefault=false excludes it from
'pasbuild compile' and 'pasbuild test' reactor runs; it can still be
built explicitly with 'pasbuild compile -m blaise-rtl'.

Also adds a bootstrapExclude entry for System.pas so that if
blaise-rtl is compiled explicitly, the FPC bootstrap program does not
include System (FPC provides it implicitly).
2026-04-24 08:06:01 +01:00
Graeme Geldenhuys 9c4df99aca chore: clean-up .gitkeep files we don't need any more 2026-04-24 07:43:04 +01:00
Graeme Geldenhuys f66260093e 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 00:21:43 +01:00
Graeme Geldenhuys 49a1a0f478 Add implicit Self field access, OrdAt builtin, and migration infrastructure
Implicit Self: bare field names inside methods (e.g. FPos := FPos + 1)
now work without explicit Self. prefix. TIdentExpr.IsImplicitSelf /
ImplicitFieldInfo flags set by AnalyseExpr when lookup fails but a class
field matches; TAssignment.ImplicitSelfField set by AnalyseAssignment.
Codegen emits loadl %_var_Self + offset for reads, storew/storel through
Self + offset for writes. Required for migrating the compiler source which
was written in FPC style without explicit Self.

OrdAt(s, i): integer RTL function — returns ASCII ordinal of character at
1-based position i. Replaces FSource[FPos] (array indexing) in the
migrated uPasTokeniser.

Migration script (tests/blaise-compiler.pas): initial work-in-progress
self-hosting source. Script handles: const→class body stripping, inline
qualifier removal, inline-var removal, Exit(expr) expansion, shr/shl
conversion, constructor/destructor→procedure, access modifier stripping,
keyword array → TStringList/InitKeywords, FSource[expr] → OrdAt(FSource, expr),
char-range in [...] → integer comparisons, repeat...until → while True.

834 tests pass.
2026-04-23 15:24:13 +01:00
Graeme Geldenhuys 5c0083abee Add constructor-with-args, const blocks; begin self-hosting source
Constructor calls with arguments: TMethodCallExpr.IsConstructorCall flag;
AnalyseMethodCallExpr handles TypeName.Create(args) → allocate via
_ClassAlloc then call user-defined Create method; EmitCaseStmt generates
dispatch, EmitExpr handles the new constructor path.

const block: TConstDecl AST node; TBlock.ConstDecls list; ParseConstBlock;
AnalyseConstDecls registers each constant as skConstant in the symbol table.

tests/blaise-compiler.pas: initial self-hosting source stub — program header,
CHR_* character constants, Exception hierarchy, and Classes.pas RTL content.
The file is built incrementally; this commit has sections 1-2.

834 tests pass.
2026-04-23 15:04:01 +01:00
Graeme Geldenhuys 926b594516 Add const blocks, fix ParseBlock ordering, and stub blaise-compiler.pas
const: TConstDecl AST node; ParseConstBlock (integer and string literal values);
AnalyseConstDecls (registers each as skConstant in symbol table). Enables
CHR_* character constants and dupAccept/dupIgnore/dupError in self-hosting source.

ParseBlock: add tkConst to the multi-section loop.
Blaise now supports: case, enum, const, multi-type/var blocks, all file I/O
builtins — the complete language surface needed for the source migration.

834 tests pass.
2026-04-23 14:37:39 +01:00
Graeme Geldenhuys fb89879f86 Add OrdAt builtin for character-level string access in self-hosting migration
_OrdAt(s, i): returns ASCII ordinal of character at 1-based position i.
Required for migrating uPasTokeniser to Blaise: replaces FSource[FPos] and
char set-membership checks with integer comparisons.
2026-04-23 13:50:30 +01:00
Graeme Geldenhuys 1e8ebf04f1 Add case statements and enum types — required for self-hosting migration
case statement: TCaseStmt + TCaseBranch AST nodes; tkCase/tkOf tokens;
ParseCaseStmt (multi-value branches, optional else); AnalyseCaseStmt
(ordinal selector check); EmitCaseStmt (ceqw dispatch chain + jmp).

enum types: TEnumTypeDef AST node; TEnumTypeDesc symbol-table descriptor
(tyEnum, ByteSize=4, QBE 'w'); ParseEnumDef; AnalyseTypeDecls enum path
registers each member as skConstant with ordinal value 0..N-1; IsNumeric,
IsOrdinal, ByteSize, AllocAlign, QbeTypeOf all updated for tyEnum.
Variable alloc path handles tyEnum identically to tyInteger.

These two features unblock the self-hosting source migration: the compiler
source uses TTokenKind, TTypeKind, TBinaryOp, and TSymbolKind enums
throughout, all wrapped in case dispatch.

834 tests pass, zero Valgrind errors.
2026-04-23 13:47:17 +01:00
Graeme Geldenhuys 637369aa10 Close self-hosting gaps: multi-section blocks, file I/O, CLI args, process
Parser: allow any number of type/var/procedure/function sections in any
order within a single block. Required for single-file concatenation of
compiler units during self-hosting migration.

RTL (blaise_io.c): _SetArgs, _ParamCount, _ParamStr, _ReadFile, _WriteFile,
_AppendFile, _FileExists, _GetEnvVar, _Exec, _Halt. All exposed as Blaise
builtins via uSymbolTable/uSemantic/uCodeGenQBE.

$main now emits (w %argc, l %argv) and calls _SetArgs at startup so
ParamStr/ParamCount work at runtime.

TIdentExpr: IsNoArgFuncCall flag for builtin functions called without parens
(e.g. ParamCount without ()). Codegen synthesises a temporary TFuncCallExpr
so the existing builtin dispatch handles it transparently.

IR comment: removed stale "(Phase 2)" tag.

815 tests pass, zero Valgrind errors.
2026-04-23 13:00:34 +01:00
Graeme Geldenhuys 6688f32926 Add TObjectList and TStringList to RTL; fix string ARC via typed pointers
New builtins: CompareStr, CompareText (→ _StringCompare/_StringCompareText),
ZeroMem (→ memset), _ClassAddRef/_ClassRelease for manual ARC management.

EmitPointerWrite now emits retain/release when the base type is tyString,
enabling ARC-correct string storage in ^string arrays. ZeroMem is used to
zero-initialise newly grown string slots so the "release old" half of ARC
never sees uninitialised memory.

Classes.pas: TObjectList (^Pointer array, no ARC overhead) and TStringList
(^string + ^Pointer parallel arrays, sorted binary search via CompareText,
complete Delete/Insert/Clear with correct ARC). Generics.Collections and
phase3_milestone updated to zero-init new array slots, fixing previously
hidden memory safety issues revealed by the EmitPointerWrite ARC fix.

785 tests pass, zero Valgrind errors.
2026-04-23 12:32:59 +01:00
Graeme Geldenhuys 3b9339ae70 Add Format() built-in with variadic tagged dispatch
Format(fmt, arg0, arg1, ...) emits a variadic call to the C RTL
function _StringFormat using tagged (tag, value) pairs after the
variadic marker '...':
  tag=0 → integer arg (w QBE type)
  tag=1 → string arg  (l QBE type)

The C implementation (blaise_str.c) scans the format string for
%d and %s specifiers across two passes (length calculation then fill)
and builds a new Blaise ARC-managed string.

7 new IR-level tests in cp.test.stringops; 3 new e2e tests verify:
  Format('val=%d', 42)         → 'val=42'
  Format('hello %s', 'world')  → 'hello world'
  Format('%s=%d', 'Alice', 30) → 'Alice=30'
All 765 tests pass.
2026-04-23 11:56:00 +01:00
Graeme Geldenhuys 3258679445 Add built-in string operation functions (Length, Pos, Copy, etc.)
Implements 8 string RTL functions as compiler built-ins:

Compiler changes (uSymbolTable, uSemantic, uCodeGenQBE):
  - Length(s)              → $_StringLength  → Integer
  - Pos(sub, s)            → $_StringPos     → Integer (1-based)
  - Copy(s, from, count)   → $_StringCopy    → string
  - UpperCase(s)           → $_StringUpperCase → string
  - LowerCase(s)           → $_StringLowerCase → string
  - SameText(s1, s2)       → $_StringSameText → Boolean
  - IntToStr(n)            → $_IntToStr      → string
  - StrToInt(s)            → $_StrToInt      → Integer

RTL (blaise_str.c + Makefile):
  Implements all 8 functions against the BlaiseStrHdr layout defined
  in blaise_arc.c. All return new strings with RefCount=0 (ARC picks
  them up at the assignment site).

Tests: 31 new IR-level tests in cp.test.stringops.pas;
       7 new e2e tests in cp.test.e2e.pas verifying correct runtime
       output (e.g. Length("hello")=5, Pos=7, Copy=ell, etc.).
All 755 tests pass.
2026-04-23 11:50:06 +01:00
Graeme Geldenhuys 103e5231e8 Implement 'inherited' keyword for static parent-class dispatch
Adds full lexer→parser→semantic→codegen support for the 'inherited
MethodName(args)' statement form:

- uLexer: new tkInherited token
- uAST: new TInheritedCallStmt node (Name, Args, ResolvedParentType,
  ResolvedMethod)
- uParser: ParseInheritedStmt; dispatched from ParseStmt
- uSemantic: tracks FCurrentClass in AnalyseMethodDecl; new
  AnalyseInheritedCall resolves the parent chain and validates args
- uCodeGenQBE: EmitInheritedCall emits a direct static call to
  $ParentType_MethodName, bypassing vtable dispatch

6 new tests in cp.test.inherit.pas; all 724 tests pass.
2026-04-23 11:33:05 +01:00
Graeme Geldenhuys 29c32344f7 Mark Phase 3 complete; document Destroy hook and RTL rewrite
Updates the implementation status table to reflect:
- ARC for interface references: done (class/interface addref/release,
  [Weak] cycle-breaking, Free rewired to _ClassRelease).
- RTL rewrite under ARC rules: done (Destroy replaces Free on
  collections, EmitFieldCleanupFn Destroy dispatch, milestone updated).

Closes out all outstanding Phase 3 follow-up items.
2026-04-23 10:53:12 +01:00
Graeme Geldenhuys 0508b6cdc6 Rewrite RTL under ARC rules; add Destroy destructor hook
Compiler:
- TRecordTypeDesc gains HasDestroyMethod boolean property, set by the
  semantic analyser whenever a class (regular or generic instantiation)
  declares a 'Destroy' method.
- EmitFieldCleanupFn emits a call to $<TypeName>_Destroy before the
  ARC field-release loop when HasDestroyMethod is set, giving classes a
  hook to free raw resources (malloc buffers etc.) at refcount zero.

RTL (Generics.Collections):
- TList<T> and TDictionary<K,V> replace 'procedure Free' with
  'procedure Destroy'. Destroy frees the internal raw buffers (FData /
  FKeys / FValues) and nils them, making re-entrant calls safe.
  Free is no longer user-defined on these classes so List.Free uses
  the built-in ARC release path (_ClassRelease + slot nil-out).

tests/phase3_milestone.pas:
- TIntList and TStrIntDict migrated to Destroy (removed FreeMem(Self)
  which was unsafe under universal ARC on TObject).

Tests (718 total, 0 failures):
- cp.test.arc: 3 new IR tests for Destroy dispatch in field cleanup fn
  (non-generic, absent-on-class-without-Destroy, generic instantiation).
- cp.test.e2e: 4 new end-to-end / valgrind tests — ClassDestroy frees
  buffer, TList ARC lifecycle, Phase3Milestone stdout and valgrind-clean.
2026-04-23 10:51:56 +01:00
Graeme Geldenhuys 6569c1f82b Implement [Weak] attribute for cycle-breaking under ARC
A weak reference is a slot that does not contribute to the target's
refcount and is automatically nil'd when the target is freed.  This
breaks reference cycles that would otherwise leak under universal ARC.

Attribute model (Delphi-compatible)

    type
      TNode = class
        Other: TNode;            // strong, default
        [Weak] Back: TNode;      // weak, no refcount, zeroed on free
      end;

The parser accepts `[Ident]` and `[Ident(args)]` attribute lists before
var and class-field declarations.  Attribute names are captured
verbatim on TVarDecl.Attributes / TFieldDecl.Attributes; unknown
attributes are accepted silently so user-defined attributes (which
will materialise with RTTI) don't need to wait for the syntax.

Semantic analysis resolves `[Weak]` with the Delphi convention that
the `Attribute` suffix is optional — `[Weak]` and `[WeakAttribute]`
both resolve to the recognised marker.  Applying [Weak] to a non-
reference type (Integer, string, record) is an error at declaration
time.  Resolved weakness is surfaced on TVarDecl.IsWeak / TFieldInfo.IsWeak
and TSymbol.IsWeak for codegen consumption.

Lexer: tkLBracket / tkRBracket added.

RTL (blaise_weak.c): global chained hash table maps target pointers to
lists of registered weak slot addresses.  _WeakAssign registers a new
slot (and unregisters any prior), _WeakClear unregisters and zeros,
_WeakZeroSlots nils all slots registered against a target.  The last
is called from _ClassRelease at refcount zero, before field cleanup
and free, so weak readers never see dangling memory.

Codegen: weak class/interface vars bypass addref/release entirely.
Variable assignment, field assignment, scope-exit cleanup, exception-
path cleanup, and per-class _FieldCleanup functions all branch on the
weak flag to emit _WeakAssign / _WeakClear against the slot address
instead of strong refcount operations.

Tests

  * cp.test.weakref.pas — 14 unit tests covering lexer, parser,
    semantic validation (including the suffix-drop rule), and codegen
    emission at every insertion point.
  * cp.test.e2e.TestRun_WeakRef_BreaksCycle_Valgrind — compile and run
    a two-node mutual-reference program with [Weak].  Without the
    attribute, valgrind reports both nodes as definitely lost; with it,
    the run is leak-free.

All 711 compiler tests pass.

Deferred: TCustomAttribute / WeakAttribute Pascal-side class
declarations.  The compiler resolves `[Weak]` purely by attribute
name, so the RTL classes are not load-bearing today.  They will be
added as empty marker types when RTTI arrives and RTTI-queryable
attributes become meaningful.
2026-04-23 10:15:00 +01:00
Graeme Geldenhuys 3e4219dd48 Extend ARC to interface references; fix two latent IR bugs
Interface variables are fat pointers (obj + itab); only the obj slot is
refcounted.  Adds addref/release around every interface assignment path
(as-cast, class-to-interface, and the previously unhandled
interface-to-interface case), scope-exit release of the obj slot, and
exception-path release with obj-slot zeroing.  itab slots point at
static rodata and are left alone.

Two pre-existing latent bugs surfaced while wiring the new valgrind
e2e test and are fixed here rather than deferred:

  1. Indirect call IR emitted `call %%_tN(...)` (double percent) at
     three sites in EmitProcCall and EmitExpr.  QBE rejected it with
     'invalid character %'.  No passing test previously exercised the
     affected paths — the matching expression-form code at line 1938
     used the correct single-percent form.  Fixed to `call %s` which
     renders FPtrTemp (already prefixed with %) correctly.

  2. Classes declared as `class(TObject, IFoo)` emitted a reference to
     $typeinfo_TObject with no corresponding data item, causing the
     linker to fail.  Emit a stub `data $typeinfo_TObject = { l 0, l 0 }`
     unconditionally at the top of EmitTypeInfoDefs.

Added four IR-level tests covering the three interface-assignment
paths and scope-exit release, plus two valgrind-clean end-to-end
tests (one class-only, one interface) that assert leak-freedom without
any explicit Free call in the source.  All 696 tests pass.
2026-04-23 01:07:25 +01:00
Graeme Geldenhuys 6e18aba8f1 Insert ARC on class vars, class fields, params; rewire Free as release
Completes the variable/field/param ARC insertion pass for class types
and wires the destructor path for ARC-managed fields.

Compiler changes:
  * EmitAssignment: tyClass branch mirrors string ARC (addref new, release
    old, store)
  * EmitFieldAssignment: same pattern for string and class field stores
  * EmitStringCleanup renamed EmitArcCleanup; releases class locals
    alongside string locals at block exit
  * EmitExcPathArcCleanup: releases class locals on exception unwind
  * EmitFuncDef / EmitMethodDef: addref class value params on entry and
    release on exit (matches the existing string convention for standalone
    funcs; closes the gap for methods)
  * Obj.Free lowered to _ClassRelease + nil-out of the variable slot —
    Free is now a sanctioned synonym for immediate release, and the
    scope-exit release on the zeroed slot becomes a safe no-op
  * New EmitFieldCleanupDefs emits a $_FieldCleanup_<Class> function per
    class (and per generic instantiation) that releases each ARC-managed
    field; _ClassRelease invokes it before free() when the refcount
    reaches zero

Inherited fields are already merged into the derived class's Fields list
by the semantic analyser, so the derived cleanup is authoritative — no
parent chain, which would otherwise double-release.

RTL changes:
  * Object header grows from 8 to 16 bytes to carry the cleanup fn pointer
  * _ClassAlloc takes (size, cleanup_fn); refcount still starts at 0
  * _ClassRelease calls cleanup before free when rc reaches zero

Absorbs Task 4 (Free as release synonym).  phase2_milestone valgrind
test remains leak-clean with the new ARC rules in force; 5 new codegen
assertions cover the insertion points explicitly.
2026-04-23 00:52:55 +01:00
Graeme Geldenhuys c36709e627 Add _ClassAddRef and _ClassRelease RTL helpers
Nil-safe addref/release that operates on the hidden 8-byte header
introduced by _ClassAlloc.  _ClassRelease frees the block when the
refcount reaches zero.  Destructor dispatch is deferred: Blaise has no
user-defined Destroy mechanism yet, and when one lands the vptr slot
will be invoked before free().

Helpers are not yet called by codegen; Task 3 inserts addref at class
assignment and release at scope exit.
2026-04-23 00:25:23 +01:00
Graeme Geldenhuys e08284d8a7 Add 8-byte refcount header to every class allocation
Introduces _ClassAlloc and _ClassFree in blaise_arc.c.  _ClassAlloc
prefixes an 8-byte header (4B refcount + 4B padding) before the user
pointer; the user pointer still points at the vptr, so field offsets
and virtual dispatch are unchanged.  Refcount starts at 0 to match the
string ARC convention (the compiler's addref-on-assignment pass will
bring it to 1 once Task 3 lands).

Obj.Free is temporarily lowered to _ClassFree, which bypasses the
refcount but accounts for the hidden header.  Task 4 will rewire this
to _ClassRelease so Free becomes a sanctioned synonym for immediate
release under the full ARC rules.

First step of the universal-ARC-on-TObject work documented in the
class-ownership decision record (commit 0c09b42).
2026-04-23 00:23:08 +01:00
Graeme Geldenhuys 0c09b4228c Document class-ownership decision: universal ARC on TObject
Record the decision (option A) in docs/design.adoc with full pros/cons
analysis of the three options considered. Update the Phase 3 status
table so interface-ref ARC reads "Planned — option A chosen" rather
than "Deferred — design-blocked". Mirror the commitment in README.adoc
under Design Philosophy and list the TObject/TInterfacedObject split
as dropped.
2026-04-23 00:08:55 +01:00
Graeme Geldenhuys 2d4a005f56 Collapse Write+WriteLn pairs into multi-arg WriteLn in milestone tests 2026-04-22 23:19:55 +01:00
Graeme Geldenhuys cbb1af5ffc Replace force-exit workarounds with break now that Break is supported 2026-04-22 23:18:47 +01:00
Graeme Geldenhuys b38a486a0e Add boolean operators, Exit/Break, multi-arg WriteLn, chained field access, generic constraints
Five language features that surfaced as missing during prior sessions,
plus test coverage. All 685 tests pass (634 previously); the five new
end-to-end tests compile-link-run against the RTL to verify real
behaviour beyond IR shape.

- AND/OR/NOT with standard Pascal precedence; operands must be Boolean.
- Exit jumps to a per-function/main exit label so ARC cleanup still
  runs on early return. Break uses a loop-end label stack; rejected
  outside a loop.
- Write/WriteLn now accept any number of arguments, emitting one printf
  per value plus one trailing newline call for WriteLn.
- TFieldAccessExpr gained an optional Base expression; parser wraps
  each additional .IDENT, semantic recurses to type the chain, codegen
  uses a new EmitInstancePtr helper for nested record/class storage.
- <T: class>, <T: record>, <T: TypeName> constraints on generic types,
  interfaces, and standalone functions; validated at instantiation
  (class-kind, value-type, or subclass/interface-implementor check).

Side effect: EmitVarAllocs now memset(0)s record storage so QBE's SSA
checker accepts reads from zero-initialised record fields. Matches
Pascal's defined default-zero semantics.

New test units split by concern: cp.test.booleanops, cp.test.flowjumps,
cp.test.multiwrite, cp.test.chainedfields, cp.test.genericconstraints
(46 new IR-level tests in total). cp.test.e2e gains 5 smoke tests
covering the same features through qbe + cc + run.
2026-04-22 23:06:50 +01:00
Graeme Geldenhuys b897f3e08d Add copyright headers and compiler output
Add BSD-3-Clause copyright header to all source files (44 .pas files).
Update compiler to output copyright line in version/usage output.
2026-04-22 19:00:46 +01:00
Graeme Geldenhuys 6ef9cd8c7e Add end-to-end test harness; fix EmitDataSection skipping format strings
The existing fptest suite is IR-only: tests generate QBE text and grep for
expected strings. That harness missed two runtime bugs — the alloc16 32
exception frame (fixed in 00e44e8) and, now, an EmitDataSection early-exit
that omitted unconditional printf format strings when the program had no
string literals.

cp.test.e2e.pas is a new test unit that compiles Pascal through the full
pipeline (Lexer -> Parser -> Semantic -> CodeGenQBE -> qbe -> cc -> native
binary), executes the result, and asserts on stdout, exit code, or valgrind
output. It walks up from CWD to locate vendor/qbe and rtl so it runs both
from the project root and from compiler/target/ (where PasBuild invokes
TestRunner). Scratch artefacts live under compiler/target/test-e2e/ and
are preserved on failure for post-mortem debugging. Tests Ignore()
gracefully when qbe, the RTL, or valgrind is not available.

Initial cases: bare try/finally, try/finally with locals (stack-corruption
regression for the alloc16 bug), nested try/finally, virtual dispatch in
expression position inside try/finally, Phase 2 milestone stdout, and
Phase 2 milestone valgrind-clean.

On first run the virtual-dispatch case surfaced the latent EmitDataSection
bug: the procedure exited early when FStrLits.Count = 0, skipping the
$__fmt_d_nl / $__fmt_nl definitions that WriteLn(Integer) and bare WriteLn
always reference. Any program that printed an integer without also
containing a string literal failed to link. Fix: emit the format strings
unconditionally; the literals loop is the only block now gated on count.

634/634 tests green, ~0.9 s total.
2026-04-22 18:43:15 +01:00
Graeme Geldenhuys 00e44e8876 Fix try/finally and try/except exception frame size (alloc16 32 → 512)
The exception frame was allocated as alloc16 32 (32 × 16 = 512 bytes on paper,
but QBE's alloc16 N means N items of 16 bytes: so 32 × 16 = 512 is actually
correct). Wait - re-reading: alloc16 N in QBE allocates N bytes aligned to 16.
So alloc16 32 was only 32 bytes — far less than sizeof(BlaiseExcFrame) which
needs ~216 bytes on Linux x86_64 (200-byte jmp_buf + two pointer fields).

setjmp writes its full jmp_buf into the undersized slot, silently corrupting
whatever sat above it on the stack: saved registers, local variables, and the
virtual method pointer loaded for virtual dispatch. Any try block followed by
a virtual method call in expression position crashed with a bad function pointer.

Fix: alloc16 32 → alloc16 512 in both EmitTryFinallyStmt and EmitTryExceptStmt,
matching the RTL contract documented in blaise_exc.c lines 8-13.

Two new regression tests assert alloc16 512 appears in emitted IR for both
try forms. 628 tests pass (was 626).

Also adds tests/phase2_milestone.pas: linked list with virtual dispatch,
inheritance, try/finally, and 'is' type test — zero valgrind leaks.
Updates design.adoc Phase 3 status table to reflect current implementation.
2026-04-22 18:22:48 +01:00
Graeme Geldenhuys d49ebb4f51 Phase 3 milestone: TList<Integer> + TDictionary<string,Integer> zero leaks
Four bugs fixed on the path to the milestone:

1. Non-generic class fields resolved with FindType instead of
   FindTypeOrInstantiate — typed pointer fields (^Integer, ^string) in
   concrete classes failed with 'Unknown type'.

2. Integer/Boolean/Int64 local variables not zero-initialised — QBE
   validator rejected 'slot read but never stored to' for variables
   written only through var-param pointers.

3. Class method var-param signature wrong — EmitMethodDef emitted the
   param type (w for Integer) rather than 'l' (pointer) for var params;
   EmitParamAllocs spilled the pointer with storew instead of storel.

4. Method call sites did not pass addresses for var-param arguments —
   EmitMethodCallExpr always called EmitExpr (value load) rather than
   forwarding %_var_Name (address) for IsVarParam params.

Milestone result (valgrind --leak-check=full):
  7 allocs, 7 frees, 0 bytes in use at exit, ERROR SUMMARY: 0 errors
2026-04-22 16:58:36 +01:00
Graeme Geldenhuys 266a42b05d Add _StringEquals RTL helper and string equality codegen
blaise_arc.c: content-aware _StringEquals compares two Blaise string pointers
by length then memcmp; nil treated as empty string; returns int32 1/0.

Codegen: string '=' and '<>' now route through _StringEquals instead of
ceql (pointer equality) — the '<>' case negates by ceqw result, 0.

3 new tests in cp.test.codegen.pas confirm IR contains $_StringEquals.
626 total tests, 0 failures.
2026-04-22 16:40:17 +01:00
Graeme Geldenhuys c07b28138f Add True and False as built-in Boolean constants
Registers True (1) and False (0) as skConstant symbols in the symbol table.
TIdentExpr gains IsConstant/ConstValue fields set by the semantic pass; codegen
emits 'copy N' rather than loading from a non-existent stack slot.

Idiomatic Boolean returns (Result := True / Result := False) now work in
all Blaise source including the RTL TDictionary.TryGetValue method.
6 new tests in cp.test.codegen.pas — 623 total, 0 failures.
2026-04-22 16:37:31 +01:00
Graeme Geldenhuys 76f7e104b3 Add TDictionary<K,V> generic map to RTL with tests
Linear-scan hash map backed by two parallel arrays (FKeys: ^K, FValues: ^V).
Operations: Grow, FindKey, Add (upsert), TryGetValue, ContainsKey, Remove.

Key equality uses '=' on the monomorphized type; integer keys work directly
via ceqw in the generated IR.  String key support is deferred until a
content-aware RTL equality helper is added.

TryGetValue avoids True/False constants (not yet in builtins) by computing
the return value via comparison: Result := Idx >= 0.

13 tests across parser, semantic, and codegen layers.  617 total, 0 failures.
2026-04-22 15:58:22 +01:00
Graeme Geldenhuys 3bbb67be0f Support generic class method implementations in separate blocks
Adds the standard Pascal unit structure for generic classes where method
bodies live in the implementation section rather than inline in the class
declaration:

  procedure TList<T>.Add(Value: T);
  begin ... end;

Parser: after reading an IDENT, if '< IDENT (> | ,)' follows, type params
are parsed tentatively; a subsequent DOT marks them as owner type params
(OwnerTypeParams on TMethodDecl) rather than the method's own type params.

Semantic: LinkGenericClassMethodImpls looks up the TGenericTypeDef template
by base name, finds the matching forward declaration, and transfers the body
before instantiation runs.  LinkClassMethodImpls gains a guard to skip
generic-owner entries so they are not mistakenly looked up in FMethodIndex.

RTL: Generics.Collections and Generics.Defaults refactored from inline
bodies to proper interface/implementation split.  604 tests, 0 failures.
2026-04-22 15:36:32 +01:00
Graeme Geldenhuys 15c94646a6 Add Generics.Defaults RTL unit with tests
Adds IEqualityComparer<T> and IComparer<T> generic interface declarations
alongside TIntegerEqualityComparer and TIntegerComparer concrete implementations.

13 tests cover parse, semantic, and codegen for generic interface instantiation,
itab emission, and interface dispatch.  All 595 tests pass.
2026-04-22 14:40:10 +01:00
Graeme Geldenhuys 3289be5313 Add unary minus and two-token lookahead to expression parser
Synthesise '-X' as '0 - X' in ParseFactor.  The recursive call uses
Self.ParseFactor because a bare self-name in {$mode objfpc} reads the
Result variable rather than recursing — leaving NegNode.Right pointing
at uninitialised stack memory and crashing on teardown.

Add FLookahead2 / PeekKind2 so the generic-type-args heuristic can
require '<' Ident ('>'|',') before treating '<' as opening a generic
instantiation — otherwise expressions like 'if A < B then' are misread
as generic invocations.
2026-04-22 14:27:06 +01:00
Graeme Geldenhuys e30757fef3 Add generic interface support: IFoo<T> = interface...end
Implements full generic interface support through the compiler pipeline:

Parser: TGenericInterfaceDef AST node; IDENT<TypeParams> = interface...end
syntax; ParseGenericName helper for class(IFoo<T>) parent/implements lists.

AST: TGenericInterfaceDef (ParamNames + IntfDef) and TGenericInterfaceInstance
(mangled InstName + TypeDesc) tracked on TProgram.GenericIntfInstances.

Symbol table: TInterfaceTypeDesc extended with FReturnTypes parallel list so
interface dispatch expressions can resolve return types.

Semantic: InstantiateGenericInterface instantiates IFoo<T> on demand from a
template; FindTypeOrInstantiate falls through class→interface; parent-as-interface
detection moves IFoo<Integer> from ParentName to ImplementsNames; type-safe
guard in InstantiateGeneric prevents misidentifying interface templates as class
templates; AnalyseMethodCallExpr extended with interface dispatch path.

Codegen: EmitInterfaceDefs emits typeinfo for GenericIntfInstances using the
mangled name (IEqualityComparer_Integer); itab/impllist names QBEMangle interface
names; EmitExpr TMethodCallExpr handles interface dispatch via itab pointer.

13 new tests in cp.test.genericintfs covering parser, semantic, and codegen.
2026-04-22 10:17:30 +01:00
Graeme Geldenhuys f05982f8c5 Rename Collections.pas to Generics.Collections.pas; support dotted uses names
Parser: extend UsesClause to accept dotted unit names (IDENT { DOT IDENT }),
so 'uses Generics.Collections;' parses correctly. The dot-separated
segments are joined into a single string in UsedUnits (e.g. 'Generics.Collections').

RTL: rename Collections.pas → Generics.Collections.pas and update the
unit header to 'unit Generics.Collections' to match FPC's naming and
mirror Delphi's System.Generics.Collections for source-level compatibility.

Grammar: add UnitName rule documenting the dotted-name syntax and its
path-separator semantics.

Tests: 1 new parser test (TestProgramWithDottedUnitName); 569 total, 0 failures.
2026-04-22 08:57:47 +01:00
Graeme Geldenhuys 6c3de2b39a Add TList<T> RTL unit and complete test suite with Grow/Delete/Clear
Collections.pas: full TList<T> implementation in Blaise source —
Add (with dynamic Grow via realloc), Get, Delete (shift-left), Clear,
Free, and Count property. FData: ^T backed by a flat typed pointer array;
SizeOf(T) used for element-stride arithmetic in Add/Get/Delete.

Tests: 12 tests in cp.test.tlist covering parser, semantic (SizeOf,
nil/Pointer compat, ^T instantiation), and codegen (SizeOf literal,
storew/loadw through typed pointers, calloc for Create, realloc for Grow).
2026-04-22 08:48:33 +01:00
Graeme Geldenhuys 19eaeb35ff Implement TList<T> foundations: SizeOf, ^T substitution, pointer compat
Compiler additions needed for TList<T> generic dynamic list:

- SubstTypeParam helper: handles ^T → ^Integer substitution in generic
  instantiation (the direct SameText check missed caret-prefixed types)
- Type param scope: push T=Integer bindings before analysing generic
  method bodies so that SizeOf(T) and local 'var P: ^T' resolve correctly
- SizeOf(TypeName) built-in: semantic returns Integer type with arg's
  ResolvedType set; codegen emits 'copy N' using TTypeDesc.ByteSize
- Pointer type compatibility: nil → ^T, Pointer ↔ ^T, and ^T ↔ ^T with
  same BaseType are now allowed in CheckTypesMatch and comparisons
- Constructor allocation changed from malloc to calloc so that all class
  fields (including FData/FCount/FCapacity in TList<T>) start zeroed

Tests: 11 new tests in cp.test.tlist.pas; 5 existing malloc assertions
updated to calloc; 567 total, 0 failures.
2026-04-22 08:36:31 +01:00
Graeme Geldenhuys 0cc28d395e Update grammar.ebnf to Phase 3: generics, interfaces, properties, pointers
Rewrote the EBNF grammar to reflect all language features implemented
through Phase 3 of the Blaise compiler:

- Full token vocabulary: IS, AS, NIL, VIRTUAL, OVERRIDE, TRY, EXCEPT,
  FINALLY, RAISE, UNIT, INTERFACE, IMPLEMENTATION, DOWNTO, CARET,
  NOT_EQUALS, LESS_EQ, GREATER_EQ, and soft keywords PROPERTY/READ/WRITE
- Unit grammar: UNIT, InterfaceSection, ImplementationSection, UsesClause
- Generic class definitions: CLASS < TypeParamList >
- Interface definitions with optional parent
- Property declarations with soft-keyword read/write accessors
- StandaloneDecl for TTypeName.Method implementation form
- Pointer type syntax: ^TypeName (recursive), pointer dereference (^),
  PointerWriteStmt (P^ := Expr)
- Complete statement hierarchy: if/while/for/try-finally/try-except/raise
- Complete expression hierarchy: Expr → Comparison → Additive → Term → Factor
- Factor includes NIL, generic function calls, type casts (via IDENT LPAREN)
- Updated built-ins section (GetMem/FreeMem/ReallocMem, Pointer type)
- Semantic disambiguation notes for generics, virtual dispatch, interfaces
- ARC implicit operations section (string RC, block exit, exception paths)
2026-04-22 07:50:41 +01:00
Graeme Geldenhuys 7e2f97f9ef Add pointer type infrastructure: ^T types, dereference, GetMem/FreeMem/ReallocMem
- uSymbolTable: tyPointer kind, TPointerTypeDesc(BaseType), GetMem/FreeMem/ReallocMem builtins
- uLexer/uPasTokeniser: tkCaret token for '^'; suppress ^X string escape (not in Clean Pascal)
- uParser: ParseTypeName handles '^TypeName' prefix (uses Self. for explicit recursion);
  TDerefExpr for P^ postfix; TPointerWriteStmt for P^ := V lhs
- uAST: TDerefExpr, TPointerWriteStmt nodes
- uSemantic: FindTypeOrInstantiate creates TPointerTypeDesc on demand for '^T';
  AnalyseDerefExpr, AnalysePointerWriteStmt; GetMem/ReallocMem/type-cast handling
- uCodeGenQBE: tyPointer→'l', EmitPointerWrite (storew/storel), TDerefExpr (loadw/loadl),
  GetMem→malloc, FreeMem→free, ReallocMem→realloc, pointer arithmetic (extsw + add/sub),
  type-cast via copy
- 15 new tests (556 total), all passing
2026-04-22 07:42:55 +01:00
Graeme Geldenhuys a3a57df8a7 Implement standalone generic function monomorphization
Adds demand-driven instantiation for 'function Name<T>(Param: T): T' syntax.
Templates registered on declaration; concrete instances created on first call
site (e.g. Identity<Integer>) with substituted param/return types, shared
body analysis, and QBE-mangled emission ('Identity_Integer'). 14 new tests,
541 total.
2026-04-21 23:26:52 +01:00
Graeme Geldenhuys 80c2b21d6c Implement Pascal property declarations with field- and method-backed access
Adds full property support to class types: field-backed reads/writes redirect
at semantic analysis time; method-backed reads emit a getter call in QBE IR.
Read-only enforcement raises ESemanticError on write attempts. Soft-keyword
detection keeps 'property' out of TTokenKind. 14 new tests, 527 total.
2026-04-21 23:15:15 +01:00
Graeme Geldenhuys b34bfc574d Implement Phase 3 generics monomorphization (Delphi syntax)
Parse generic class declarations (`TBox<T>`) as TGenericTypeDef nodes.
Introduce one-token parser lookahead to disambiguate `<` in type
positions vs. comparison operators.

Demand-driven instantiation: when a var declaration references
`TBox<Integer>`, InstantiateGeneric clones the class AST with type-param
substitution, resolves all field/method types, analyses method bodies
with the concrete class type in scope, and registers the instance in
TProgram.GenericInstances.  Instantiated type symbols are defined in
the global scope so they survive inner scope pops.

Codegen emits typeinfo, vtable, and method bodies for each generic
instance using QBEMangle (`TBox<Integer>` → `TBox_Integer`).

19 new tests in cp.test.generics cover parser, semantic, and codegen
paths.  All 513 tests pass.
2026-04-21 23:02:23 +01:00
Graeme Geldenhuys ffcb1a041f Update Phase 3 interface status and design notes in design doc
Mark is/as interface operators, IInterface built-in, and 2-field typeinfo
(with impllist) as Done.  Expand TYPEID entry to cover the impllist
data layout.
2026-04-21 21:51:24 +01:00
Graeme Geldenhuys 514c0fb122 doc: readme status update 2026-04-21 21:49:22 +01:00
Graeme Geldenhuys 3365558265 Add is/as for interface types with _ImplementsInterface/_GetItab dispatch
Extend BlaiseTypeInfo with an impllist field (NULL-terminated {ti,itab}
pairs) so the runtime can walk a class's interface set.  Add
_ImplementsInterface and _GetItab to blaise_exc.c for the 'is IFoo'
and 'F := T as IFoo' operators respectively.

Compiler changes:
- uAST: ResolvedTargetType field on TIsExpr
- uSemantic: accept tyInterface on RHS of is/as; set ResolvedTargetType
- uCodeGenQBE: EmitIsExpr dispatches to _ImplementsInterface for interface
  targets; EmitAssignment handles F := T as IFoo via _GetItab + branch
- EmitTypeInfoDefs: extend to 2-field layout {parent, impllist}
- EmitInterfaceDefs: emit per-class impllist data blocks
- uSymbolTable: register IInterface as built-in interface type

10 new tests in cp.test.interfaces; 2 existing typeinfo tests updated
for new 2-field format.  494 tests, 0 failures.
2026-04-21 21:25:49 +01:00