= Blaise — Modern Object Pascal Compiler
:doctype: article
:toc: left
:toclevels: 3
:sectnums:
:icons: font
:source-highlighter: rouge
:revdate: 2026-04-20
:revnumber: 1.0
:status: APPROVED
:reponame: new-pascal-compiler
:mode: Builder / Open Source

== Problem Statement

The Object Pascal ecosystem in 2026 has two options: Embarcadero Delphi (proprietary,
Windows-first, ~$3k/yr) and Free Pascal Compiler (open source but architecturally
chaotic, stalled governance, 5 language modes, thousands of include files, 140+
unmerged PRs). There is no modern, clean, cross-platform, open-source Object Pascal
compiler with a living ecosystem.

The author has already shipped three of the five pieces a complete Pascal ecosystem
needs: PasBuild (build system), OPDF (debug format with CLI reference and IDE
integration), FPTest (testing framework). The missing pieces are the compiler itself
and an LSP server.

== What Makes This Different

* *The EUREKA inversion* — every previous Pascal+LLVM project failed by building the
  compiler first then dying on the ecosystem. This project starts with the ecosystem
  already built, inverting the historical failure mode.
* *Single string type* — UTF-8 reference-counted string + RawBytes for binary data.
  No ShortString, AnsiString, WideString, UnicodeString, or OpenString.
* *Zero-GUID interfaces* — Java-style vtable mapping; no 128-bit hex strings required.
* *Reified generics* — monomorphization (Rust/Swift style), no type erasure.
* *Single language mode* — one clean modern Object Pascal dialect only.
* *OPDF debugger integration* — the author designed the format; reference CLI debugger
  and IDE support already exist. OPDF emission planned for Phase 5.
* *PasBuild drives the compiler* — `project.xml` replaces makefiles and `.lpi`/`.dproj`.
* *BSD 3-Clause* — maximum adoption, zero legal friction.

== Constraints

* Bootstrap compiler written in FPC; intermediate FPC features (classes, generics) are
  permitted — the goal is "FPC toolchain is available," not "minimal FPC subset".
* Phase 1 targets Linux x86_64 only. macOS ARM64 added in Phase 6. Windows in Phase 6 (LLVM).
* TDD throughout: FPTest test suite grows alongside the compiler.
* Self-hosting is a credibility milestone, not a day-one requirement.
* No legacy Pascal baggage: no `with` statement, no old-style `object`, no COM GUIDs,
  no multiple string types, no cross-platform `{$IFDEF}` hacks in the language itself.
* Build-tool-drives-compiler: the compiler accepts `--unit-path` search directories and
  resolves `uses` clauses itself. PasBuild (or make, shell scripts, or any other tool)
  passes the appropriate flags; no project-file format is embedded in the compiler binary.
  This decouples Blaise from any single build tool.

== Language Features — Drop vs. Keep

=== Dropped (Deliberately Unsupported)

* ShortString, AnsiString, WideString, UnicodeString, OpenString
* `with` statement (source of symbol resolution bugs; hard to analyse)
* Old-style `object` types (use `record` for value types, `class` for heap)
* COM-style GUIDs on interfaces
* Multiple language modes (no `{$mode delphi}`, `{$mode objfpc}`, etc.)
* Archaic I/O (`assign`, `reset`, `rewrite`, `blockread`)
* Untyped `file` and `text` file types (replace with stream-based RTL)

=== Kept / Modernised

* `class` with single inheritance, virtual methods, and properties
* `record` with methods and operator overloading (advanced records)
* `interface` without GUIDs — vtable mapping at compile time
* Generics with monomorphization (reified, not erased)
* ARC (Automatic Reference Counting) for strings and interfaces
* `uses` unit system with clean dependency resolution via PasBuild
* Inline variable declarations (`var x := 42;`)
* Anonymous methods / lambdas
* Attributes (custom annotations)
* Nullable types via `T?` syntax

== Premises

The following premises were agreed upon before design work began:

. Viable as a solo/small-team project *if* scope is ruthlessly constrained to one mode,
  one string type, and modern semantics only — no legacy compatibility shims.
. The biggest risk is not "can we build a compiler" but "will anyone contribute?" —
  contributor attraction requires early visible progress and clean module boundaries.
. LSP support comes *after* the compiler pipeline is stable — an LSP without a stable
  compiler is useless and frustrating.
. The migration tool (FPC/Delphi → new dialect) is the adoption unlock, but deferred
  until the compiler can compile real programs.
. Self-hosting is a credibility milestone, not a day-one requirement.

== Backend Strategy

=== QBE First, LLVM Second

https://c9x.me/compile/[QBE] is a lightweight compiler backend (~13k lines of C)
targeting x86_64, ARM64, and RISC-V. It is:

* Far simpler to integrate than LLVM
* Fast to compile with (critical during active development)
* Stable, with no aggressive API churn
* Used by cproc, the Hare language, and others

*Why not LLVM from day one:* LLVM is not fully ABI-stable, parameter passing is not
fully abstracted, and LLVM's toolchain is slow (hurts the development loop). For a
new compiler, emitting `.ll` text and shelling to `llc` mitigates some of this, but
QBE provides a simpler path to a first working binary.

*When to add LLVM:* Once the compiler is self-hosting and QBE is producing correct
code, LLVM is added as a second output adapter (see the Backend Adapter Interface
below). LLVM unlocks WASM, industrial-grade optimisation, and the full target matrix.

*QBE vendoring:* QBE source is included in the compiler repository and built from
source as part of the bootstrap build. This eliminates API churn risk entirely — the
compiler pins to a known-good QBE version and upgrades deliberately.

=== Backend Comparison

[cols="1,2,1,1,1", options="header"]
|===
| Backend | Targets | Optimisation | Stability | Complexity

| QBE
| x86_64, ARM64, RISC-V
| Good
| High
| Very low

| LLVM
| 10+ including WASM
| Industrial
| Moderate
| Very high

| Custom
| Whatever you write
| Custom
| High
| Extreme
|===

=== Backend Adapter Interface

Both QBE and LLVM backends implement the same `ICodeGen` interface. This boundary
makes backend swaps clean:

[source,pascal]
----
type
  ICodeGen = interface
    procedure BeginModule(const Name: string);
    procedure EmitFunction(AFunc: TASTFunction);
    procedure EmitGlobal(AGlobal: TASTGlobal);
    procedure Finalise(const OutputPath: string);
  end;
----

The AST-to-backend boundary passes a typed AST (after semantic analysis). Neither
backend sees Pascal source; both see the same AST node types. Adding LLVM in Phase 6
means writing `TCodeGenLLVM` that implements `ICodeGen` — no changes to the parser
or type checker.

== Approaches Considered

=== Approach A: Pipeline-First (Chosen)

Build the smallest possible end-to-end compiler pipeline first.
Lexer → Parser → AST → QBE IR → native binary. Target: Hello World compiled
via PasBuild within 4 months (Linux x86_64 first).

* *Effort:* Medium
* *Risk:* Low
* *Pros:* Proves viability fast; testable at each phase; PasBuild integration
  natural once compiler CLI exists; avoids premature architecture work
* *Cons:* Generics and ARC deferred; no LSP until later
* *Reuses:* PasBuild, OPDF (deferred but ready), FPTest

=== Approach B: Architecture-First (Rejected)

Design full hexagonal architecture upfront before first binary.

* *Effort:* Large
* *Risk:* Medium — architecture becomes a distraction before anything compiles
* *Rejected because:* Architecture astronauting is the #1 killer of solo compiler
  projects. Approach A naturally evolves into B once the pipeline is proven.

=== Approach C: Integration-First (Rejected)

Start with ecosystem shell (PasBuild + stub compiler + VS Code extension), build
the real compiler inside it.

* *Effort:* Medium-Large
* *Risk:* Low-Medium
* *Rejected because:* The stub compiler period feels hollow; a VS Code extension
  without real parsing frustrates contributors.

== Recommended Approach: Pipeline-First

=== Calling Convention (ABI) — Locked Before Phase 1

*Linux/macOS (x86_64):* System V AMD64 ABI (cdecl). This is QBE's default for
the `abi` calling convention. No custom Pascal ABI required.

*`var` parameters:* Caller passes a pointer to the variable; callee receives a
pointer and dereferences it. This matches how C handles `int*` output parameters.

[source]
----
# QBE IR for: procedure Swap(var A, B: Integer)
# Caller passes %a_ptr and %b_ptr as pointers
function $Swap(%a_ptr =l, %b_ptr =l) {
@start
  %a =w loadw %a_ptr
  %b =w loadw %b_ptr
  storew %b, %a_ptr
  storew %a, %b_ptr
  ret
}
----

*Windows (x86_64):* Deferred to Phase 6 (LLVM backend). LLVM handles the
Microsoft x64 calling convention automatically. QBE's Windows support is not
production-tested; targeting Linux/macOS first avoids this risk.

*ARM64 (macOS/Linux):* QBE supports ARM64 with the same `abi` calling convention
directive; no special handling required. Added in Phase 2.

=== UTF-8 String Memory Layout — Locked Before Phase 1

The single string type is a managed reference type. On the heap, a string block is:

....
+--[4 bytes]--+--[4 bytes]------+--[4 bytes]--+--[N bytes]--+--[1 byte]--+
| RefCount    | Length (bytes)  | Capacity    | UTF-8 data  | NUL        |
+-------------+-----------------+-------------+-------------+------------+
....

* A `string` variable on the stack is a single pointer (8 bytes on 64-bit).
* Nil pointer = empty string. No separate empty-string allocation.
* String literals of length 0 compile to `nil`. All other literals allocate.
* `_StringAddRef(nil)` and `_StringRelease(nil)` are no-ops (nil check first).
* Concatenation with `nil` treats `nil` as an empty string.
* ARC: compiler inserts `_StringAddRef` / `_StringRelease` at assignment and
  scope exit. `_StringRelease` decrements RefCount; frees when RefCount reaches 0.
* `RawBytes`: identical layout but the compiler does not insert encoding validation.
  Assignments between `string` and `RawBytes` require explicit conversion.

=== Compiler CLI Interface — Locked Before Phase 1

PasBuild invokes the compiler binary as:

[source,shell]
----
blaise --source Hello.pas \
       --unit-path src/main/pascal \
       --output bin/hello
----

Or for single-file compilation during bootstrap:

[source,shell]
----
blaise --source Hello.pas --output bin/hello --target linux-x86_64
----

The compiler resolves `uses` clauses itself via `--unit-path` search directories.
PasBuild (or any other build tool) reads `project.xml` and passes the appropriate
flags; no project-file format is embedded in the compiler binary.

Supported flags:

[cols="1,3", options="header"]
|===
| Flag | Description

| `--source <path>`
| Single entry-point source file

| `--unit-path <dir>`
| Add directory to the unit search path (repeatable; maps to `-Fu` in FPC-style invocation)

| `--output <path>`
| Output binary path

| `--target <id>`
| `linux-x86_64` (default), `macos-arm64`, `linux-arm64`

| `--debug`
| Emit OPDF debug info (Phase 5+)

| `--emit-ir`
| Emit QBE IR to stdout (useful for debugging the compiler)
|===

=== Phase 1 — Bootstrap Pipeline (Months 1–4)

*Goal:* Hello World compiled to a native binary on Linux x86_64, driven by PasBuild.

. *Lexer* — tokenise a minimal Pascal subset: keywords, identifiers, literals,
  operators. Unit: `uLexer.pas`. TDD with FPTest from day one.

. *Parser* — recursive-descent parser targeting a minimal AST. Handles:
  `program`, `uses`, `var`, `begin`/`end`, `writeln`, string literals.
  Units: `uParser.pas`, `uAST.pas`.

. *QBE IR emitter* — traverse AST, emit QBE IR text. Link with `cc`.
  Unit: `blaise.codegen.qbe.pas`.
+
*Phase 1 pre-work (before parser work begins):* Spend 2–3 weeks writing QBE IR
snippets by hand targeting libc to build fluency. QBE variadic calls require
explicit type encoding and are non-obvious. Proof of concept:
+
[source]
----
# Global string data (NUL-terminated)
data $hello = { b "Hello, World!", b 10, b 0 }
data $fmt_s = { b "%s", b 0 }

# printf is variadic: pass fmt pointer + data pointer
export function w $main() {
@start
  %fmt =l $fmt_s
  %msg =l $hello
  call $printf(l %fmt, ..., l %msg)
  ret 0
}
----
+
`WriteLn(s)` where `s: string` → extract UTF-8 data pointer from the string struct
(offset 12 bytes from struct base), then call `printf` with a `"%s\n"` format string.
+
*`var` parameters + ARC:* `var S: string` — caller passes a pointer; callee does
*not* call `_StringRelease(S)` (does not own the reference). The Phase 1 test suite
must include `var` parameter + string tests to catch ARC double-free bugs early.

. *PasBuild integration* — `project.xml` format that drives the new compiler.
  PasBuild invokes the compiler binary, manages unit paths, and handles
  debug/release configurations.

. *Phase 1 RTL* — minimal `System.pas`. Concrete signatures:
+
[source,pascal]
----
type
  Integer  = Int32;    // 32-bit signed (maps to QBE 'w' type)
  Int64    = Int64;    // 64-bit signed (maps to QBE 'l' type)
  Boolean  = (False = 0, True = 1);  // 8-bit
  string   = ^StringHeader;          // opaque managed pointer (Phase 1: no user access)

procedure Write(const S: string); overload;
procedure WriteLn(const S: string); overload;
procedure WriteLn; overload;        // empty line
----
+
Phase 1 does *not* support `WriteLn(X, Y)` multi-arg or format specifiers.
Those are Phase 2 RTL additions alongside SysUtils.

*Phase 1 minimal grammar (BNF for parser implementation):*

....
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       ::= ProcCall | Assignment | empty
ProcCall   ::= Ident '(' [ExprList] ')'
Assignment ::= Ident ':=' Expr
ExprList   ::= Expr {',' Expr}
Expr       ::= Term (('+' | '-') Term)*
Term       ::= Factor (('*' | '/') Factor)*
Factor     ::= IntLit | StringLit | Ident | '(' Expr ')'
TypeName   ::= 'Integer' | 'Boolean' | 'string'
....

Comments: `{ ... }` and `// ...` are handled by the Lexer (skipped, not in AST). +
Integer literals: decimal only in Phase 1. Hex (`$FF`) added in Phase 2.

*Milestone:* `pasbuild build` produces a native "Hello, World" binary on Linux x86_64.

*Risk:* ABI design takes 2 weeks if QBE IR generation hits unexpected issues — the
4-month timeline includes this buffer.

=== Phase 2 — Type System (Months 5–7)

*Goal:* Real programs with classes and records.

* Single UTF-8 string type with ARC (compile-time `_AddRef`/`_Release` insertion)
* `class` — heap-allocated, single inheritance (explicit in this phase),
  virtual methods, properties. Final classes first, inheritance second.
* `record` — stack-allocated with methods and operator overloading
* `Integer`, `Int64`, `UInt32`, `Boolean`, `Float64` — no legacy aliases
* Unit interface/implementation sections with proper dependency ordering
* Phase 2 RTL additions: `SysUtils` (string utilities), `Classes` (`TObject`, `TList` stub)

*Exception handling — ARC co-design:* `try`/`except`/`finally` with stack unwinding.
ARC and exceptions are co-designed. Algorithm for stack-local ARC cleanup:

. Compiler tracks all ARC-managed variables in scope at each point in the function.
. On exception path entry, compiler emits cleanup code for all in-scope ARC variables
  in LIFO order (last declared, first released) before jumping to the handler.
. `var` parameter strings are *not* released by the callee (ownership stays with caller).
. `finally` blocks always run — the generated cleanup code is emitted on both
  normal and exception exit paths.

Exception type hierarchy: `Exception` → `EInvalidOperation`, `EAccessViolation`, etc.
(Phase 2 RTL defines the base hierarchy; not in Phase 1.)

*Milestone:* A non-trivial program (e.g., a linked list using `TObject`) compiles
and runs correctly under memory leak checking.

=== Phase 3 — Generics + Interfaces (Months 8–11)

*Goal:* The language becomes genuinely useful.

==== Zero-GUID Interface Dispatch

Each class has a compile-time *Interface Table* (an array of pointers). When
`TMyObject` declares `implements IFoo, IBar`, the compiler assigns each interface a
stable index within that class's table and generates a vtable stub per interface.

[source,pascal]
----
type
  IFoo = interface
    procedure DoFoo;
  end;
  IBar = interface
    procedure DoBar;
  end;
  TMyObject = class(TObject, IFoo, IBar)
    procedure DoFoo; override;
    procedure DoBar; override;
  end;
----

Generated (conceptually):

....
TMyObject._InterfaceTable = [
  0 => { IFoo_TYPEID, @TMyObject_IFoo_vtable },
  1 => { IBar_TYPEID, @TMyObject_IBar_vtable },
]
....

`Supports(Obj, IFoo)` at runtime walks `Obj.ClassType._InterfaceTable` and compares
`TYPEID` (a compiler-assigned integer, not a GUID). `as IFoo` raises `EInvalidCast`
if not found. `is IFoo` returns `False` without raising.

`TYPEID` is a 32-bit integer computed as `CRC32(UnitName + '.' + InterfaceName)`.
This is deterministic across all builds and compilation orders. If Unit A defines
`IFoo` and Unit B imports it, both will have identical TYPEIDs regardless of
recompilation order. Note: not a security hash — collisions are theoretically
possible but astronomically unlikely for normal interface names.

==== Monomorphization Engine

`TList<T>` at compile time: when the compiler sees `TList<Integer>` in a `uses`
clause or variable declaration, it generates a specialised type `TList__Integer`
with all `T` replaced by `Integer`. This happens at compile time, not link time.

*Code bloat mitigation:*

* Only types that are *actually used* are specialised (demand-driven, not eager).
* Linker dead-code elimination: compile with `-gc-sections` (GNU ld) or
  `-dead_strip` (macOS ld) to remove unused specialisations from the final binary.
* Phase 3 includes binary size benchmarks for a canonical generic library.

*Scope:* Monomorphization is compile-time only. No link-time specialisation.
Cross-unit generic instantiation: the compiler generates specialisations in the
unit that first uses them; duplicate specialisations across units are merged by
the linker's COMDAT-folding or deduplicated by the compiler's instantiation tracker.

==== Property Declarations

Classes support Delphi-style `property` declarations with field-backed or
method-backed read/write access.

[source,pascal]
----
type
  TCounter = class
    FCount: Integer;
    function GetCount: Integer;
    procedure SetCount(AValue: Integer);
    property Count: Integer read FCount write FCount;  { field-backed }
    property CountVia: Integer read GetCount write SetCount; { method-backed }
  end;
----

* **Field-backed read/write**: redirected at semantic analysis time; codegen
  emits a plain field load or store.
* **Method-backed read**: `PropRead`/`PropOwnerType` set on `TFieldAccessExpr`;
  codegen emits a getter method call.
* **Read-only enforcement**: assigning to a property with no `write` clause raises
  `ESemanticError` at compile time.
* **Soft-keyword detection**: `property` is not a reserved word; the parser
  detects it contextually inside the class body loop to avoid lexer conflicts.

Implementation status (as of 2026-04-21): field-backed and method-backed reads
and writes are fully operational. 14 tests in `cp.test.properties.pas` —
all passing (527 total, 0 failures).

==== Standalone Generic Functions

Standalone functions can declare type parameters using the same Delphi syntax as
generic classes.

[source,pascal]
----
function Identity<T>(Val: T): T;
begin
  Result := Val;
end;

var X: Integer;
begin
  X := Identity<Integer>(42);
end;
----

* **On-demand instantiation**: the function template is registered during
  `AnalyseStandaloneDecls` but not analysed until a call site with concrete type
  args is encountered.
* **Shared body**: concrete instances re-use the template's `TBlock` with
  `OwnBody = False`; each instantiation re-annotates the body nodes with the
  current concrete types (same documented limitation as generic class methods).
* **QBE mangling**: the call site emits `call $Identity_Integer` and the function
  body is emitted as `function w $Identity_Integer(w %_par_Val)`.

Implementation status (as of 2026-04-21): single-type-parameter functions with
value params are fully operational. 14 tests in `cp.test.genericfuncs.pas` — all
passing (541 total, 0 failures).

==== Other Phase 3 Items

* ARC for interface references (with `[Weak]` attribute for cycle-breaking)
* `is` and `as` operators using Interface Table lookup
* `Generics.Collections`: `TList<T>`, `TDictionary<K,V>` — Phase 3 RTL additions

*Milestone:* A generic `TList<Integer>` and `TDictionary<string, Integer>` compile
and pass the FPTest suite. Binary size measured and within acceptable bounds.

=== Phase 4 — Multi-file Compilation (v0.2.0)

*Goal:* Compile real multi-file Pascal source — the compiler's own source — using
itself. Retire the single-file hand source (`tests/blaise-compiler.pas`).

NOTE: Single-file bootstrap and self-hosting fixpoint were achieved at v0.1.0.
The three-stage bootstrap (FPC → hand-source → self-hosted) confirmed that the
compiler can compile a representative Pascal programme with itself.

Architecture: whole-programme compilation — all units compiled from source every
build, single combined QBE IR output, no `.ppu` cache.

* `TUnitLoader` — resolves `uses` clauses to source files via `--unit-path` search
  directories; performs post-order DFS for dependency ordering; detects cycles
* `AnalyseUnitForExport` on `TSemanticAnalyser` — promotes interface-section symbols
  to global scope so subsequent units and the programme can reference them
* `AppendUnit`/`AppendProgram` on `TCodeGenQBE` — accumulates multi-unit IR into a
  single output buffer with globally-unique string-literal labels
* `--unit-path <dir>` CLI flag (repeatable); FPC-style `-Fu<dir>` also honoured

*Milestone:* `pasbuild test -m blaise-compiler` passes with the compiler compiling
its own multi-unit source (uLexer, uParser, uAST, uSymbolTable, uSemantic,
blaise.codegen.qbe, uUnitLoader) via `uses` resolution, producing an identical binary.
Hand source retired.

=== Phase 5 — OPDF Integration (previously Phase 4)

*Goal:* First-class debugging. Runs in parallel with Phase 4; does not block Phase 6.

* Emit OPDF debug info alongside QBE IR in debug builds
* PasBuild `debug` target automatically enables OPDF via `--debug` flag
* Test against the existing OPDF CLI reference debugger
* Verify source-line mapping, variable inspection, and call stacks

*Milestone:* Step through a compiled program in the reference OPDF debugger,
inspect variables, and see correct source lines.

=== Phase 6 — LLVM Backend + Windows (previously Phase 5)

*Goal:* Full platform support via a second backend.

* Add LLVM as a second backend (`TCodeGenLLVM implements ICodeGen` — adapter swap)
* macOS ARM64 support via QBE (Darwin link flags, `libSystem`); fat-binary RTL for
  `blaise_rtl.a` (x86_64 + ARM64 slices)
* Windows x86_64 support via LLVM (LLVM handles the Microsoft x64 ABI automatically)
* CI/CD pipeline: GitHub Actions building for Linux x86_64, macOS ARM64, Windows x86_64

*Milestone:* QBE backend producing correct binaries on Linux x86_64 and macOS ARM64;
LLVM backend on Windows x86_64.

=== Phase 7 — LSP + VS Code Extension (previously Phase 6)

*Goal:* Developer experience that attracts contributors.

LSP development may begin on the FPC-bootstrapped compiler once Phase 3 is complete.
It does not need to wait for Phase 4 multi-file compilation. Porting the LSP to the
self-hosted compiler is a Phase 8 secondary milestone, not a blocker.

* Language server built on the compiler's symbol table and type checker
* Hover type info, go-to-definition, error squiggles
* VS Code extension
* Incremental compilation for fast feedback

*Milestone:* Write Pascal in VS Code with full IDE support.

=== Phase 8 — Migration Analyser (previously Phase 7)

*Goal:* Unlock adoption from existing FPC/Delphi codebases.

*Scope: analysis and reporting only — no automatic translation.* This tool is a
static analyser, not a transpiler. Automatic translation adds months of scope and
is not the unlock; the report is.

* Parse FPC/Delphi source (using the new compiler's lexer/parser in a permissive mode)
* Flag incompatible constructs: multiple string types, `with` statements, COM GUIDs,
  old-style `object`, archaic I/O, `{$IFDEF}` patterns
* Produce a structured migration report: incompatibility count, location, and
  recommended modern equivalent for each
* Does *not* attempt automatic rewriting

*Milestone:* Analyse a real fpGUI unit (`uFCL.pas` or similar). Report identifies
all incompatibilities. Human reviewer confirms no false negatives.

== Open Questions

. *Project name* — "CleanPascal", "ModPas", "Pasca", "Helix", "OP2" — needs a name
  that signals a clean break without alienating the Pascal community.
. *Package registry* — Deferred until Phase 8. Minimum viable is local file-based
  dependencies via PasBuild `project.xml`. No registry server in scope until Phase 8+.

== Committed Decisions

These are not open for reconsideration:

[cols="1,3", options="header"]
|===
| Decision | Detail

| Calling convention
| System V AMD64 ABI (cdecl) on Linux/macOS. `var` params = pointer passing.
  Windows Microsoft x64 ABI via LLVM in Phase 6.

| String layout
| refcount (4B) + length (4B) + capacity (4B) + UTF-8 data (N bytes) + NUL (1B).

| RTL scope
| Phase 1 = `System.pas` only. Phase 2 adds `SysUtils`, `TObject`, `TList` stub.
  Phase 3 adds `Generics.Collections`. Everything else deferred or community-contributed.

| Windows support
| Phase 6 via LLVM backend. Not in Phases 1–5.

| QBE
| Vendored in the compiler repository, built from source. Version pinned.

| Interface TYPEID
| `CRC32(UnitName + '.' + InterfaceName)` — deterministic across all builds.
|===

== Success Criteria

[cols="3,1", options="header"]
|===
| Criterion | Phase

| Hello World compiles to a native binary via PasBuild
| Phase 1

| A non-trivial Linked-List program using TObject descendants.
| Phase 2

| A real library (generic list, hash map) compiles and passes tests
| Phase 3

| Compiler compiles itself (single-file bootstrap, self-hosting fixpoint) ✓
| Phase 1 (achieved v0.1.0)

| Compiler compiles its own multi-unit source via `uses` resolution
| Phase 4

| A non-trivial FPC program analysed and migration report generated
| Phase 8
|===

== Distribution Plan

* *Source:* GitHub (BSD 3-Clause), releases tagged with semantic version
* *Binaries:* GitHub Releases — pre-built for Linux x86_64, macOS ARM64, Windows x86_64
* *CI/CD:* GitHub Actions — build + test on push; release binaries on tag
* *Package format:* TBD — extend PasBuild's `project.xml` with dependency declarations

== Phase 2 — Implementation Status

Phase 1 (bootstrap pipeline) is complete. Phase 2 type-system work is in progress:

[cols="2,1,3", options="header"]
|===
| Item | Status | Notes

| Symbol table with scope nesting
| Done
| `uSymbolTable.pas` — `TScope` chain, case-insensitive lookup, built-in types

| Semantic analysis pass
| Done
| `uSemantic.pas` — type inference, type checking, `ResolvedType` annotation

| Wire semantic pass into driver
| Done
| `Blaise.pas` — semantic step between parse and codegen

| `record` types
| Done
| Stack-allocated aggregates; field access via pointer arithmetic

| `class` types with fields and methods
| Done
| Heap-allocated; `TypeName.Create` → `malloc`; field access via pointer deref;
  methods with explicit `Self` parameter; single inheritance with field promotion

| `var` parameters (pass-by-reference)
| Done
| Caller passes stack address; callee spills pointer, double-dereferences on read,
  stores through pointer on write; `skVarParameter` in symbol table.
  Records and static arrays declared as plain value parameters use the same
  by-pointer ABI (the local slot holds a pointer to the caller's storage,
  not the aggregate bytes), so semantic marks both `skVarParameter` and
  `skParameter`-of-aggregate as `IsVarParam` for codegen.  Scalar value
  parameters are unaffected and continue to be passed by value.

| Unit `interface`/`implementation` compilation
| Done
| `TUnit` AST node; `ParseUnit`; semantic signature matching (intf vs. impl);
  `export` prefix on interface-declared functions in QBE IR

| ARC call insertion (compiler side)
| Done
| `_StringAddRef`/`_StringRelease` at string assignments and scope exit;
  addref on entry and release on exit for string value parameters;
  `_StringConcat` emitted for `string + string` expressions

| ARC RTL (`blaise_arc.c`)
| Done
| Full ref-counting in C; 12-byte header (refcnt/length/capacity) at string pointer;
  refcnt = −1 marks immortal static literals; `_StringConcat` allocates with
  refcnt = 0 so the compiler-inserted addref at assignment brings it to 1;
  `make install` copies `blaise_rtl.a` next to the compiler binary for auto-linking

| `try`/`except`/`finally` (IR structure)
| Done
| setjmp/longjmp-based dispatch: compiler emits `alloc16 32` (512 B) frame on
  Pascal stack, `_PushExcFrame` + `setjmp` at try entry, `jnz` to handler on
  non-zero return; `try/finally` emits finally body on both normal and exception
  paths and calls `_Reraise` to propagate; `try/except` pops frame before handler
  body; `raise Obj` calls `_Raise`; RTL (`blaise_exc.c`) provides
  `_PushExcFrame`, `_PopExcFrame`, `_Raise`, `_CurrentException`, `_Reraise`;
  ARC cleanup on exception paths deferred to Phase 3

| Virtual method dispatch
| Done
| `virtual`/`override` directives parsed; vtable slots assigned before field layout
  so vptr (8 bytes at offset 0) shifts all field offsets correctly;
  `data $vtable_T = { l $fn, ... }` emitted per class; constructor stores vptr
  after `malloc`; virtual calls use `loadl` vptr + indirect `call %fptr`;
  static methods retain direct `call $T_M` dispatch; subtype assignment
  (`TDerived := TBase`) allowed via parent-chain walk in semantic pass

| `is`/`as` type tests
| Done
| `_IsInstance` RTL helper; typeinfo pointer stored at vtable slot 0;
  `is` emits `call $_IsInstance` returning boolean; `as` adds a checked
  downcast with `_Raise_InvalidCast` on failure; BlaiseTypeInfo struct in RTL

| ARC cleanup on exception paths
| Done
| `EmitExcPathArcCleanup` walks in-scope variable declarations and emits
  `_StringRelease` + `storel 0` for each string var on the `@fin_exc` path,
  before `_Reraise`; slot zeroing prevents double-release in nested handlers

| Separate method implementations
| Done
| `procedure TFoo.Bar(...)` outside the class definition; parser detects
  qualified names (`TypeName.MethodName`) in `ParseMethodDecl`; body optional
  for forward-only class declarations; `LinkClassMethodImpls` transfers bodies
  to class method declarations before `AnalyseMethodBodies` runs

| `Free` built-in
| Done
| `Obj.Free` with no user-defined `Free` method emits `call $free(l ptr)`;
  semantic pass sets `ResolvedMethod := nil` as signal; codegen handles nil
  method as built-in free before normal dispatch path

|===

Phase 2 is complete. The linked-list milestone (TObject subclass, separate method
implementations, `Free`, zero valgrind leaks) has been verified on Linux x86_64.
macOS ARM64 is deferred to Phase 6.

== Phase 3 — Implementation Status

Phase 2 is complete. Phase 3 is complete except for one item deferred
pending a class-ownership design decision (see *ARC for interface
references* below). Interfaces, monomorphization, RTL collections
(`TList<T>`, `TDictionary<K,V>` including string keys), `Generics.Defaults`,
generic class method implementations in separate blocks, generic standalone
functions with constraint syntax, and the `TDictionary<string,Integer>`
zero-leak valgrind milestone are all done.

=== Interfaces

[cols="2,1,3", options="header"]
|===
| Item | Status | Notes

| Interface declaration parsing
| Done
| `type IFoo = interface ... end;` — methods only, no fields;
  `interface(IParent)` for inheritance; `['{GUID}']` attribute silently ignored
  (zero-GUID design); `TInterfaceTypeDef` AST node; `tkIntf` token reused from
  unit interface section (context disambiguates)

| TYPEID generation + impllist
| Done
| `data $typeinfo_IFoo = { l 0 }` — address IS identity token; class typeinfo
  extended to 2 fields `{ l parent, l impllist }` where impllist is a
  NULL-terminated `{typeinfo*, itab*}` pair array; emitted by `EmitInterfaceDefs`
  / `EmitTypeInfoDefs`

| Interface type in symbol table
| Done
| `TInterfaceTypeDesc` (tyInterface) alongside `TRecordTypeDesc`; methods stored
  in declaration order (unsorted list for correct itab indexing);
  `FImplements` non-owning list on `TRecordTypeDesc` tracks class→interface pairs;
  `TObject` pre-registered as built-in root class

| Semantic: class implements interface
| Done
| Parser parses `class(TBase, IFoo, IBar)` — first name = parent class, rest =
  interfaces; semantic pass verifies all interface methods exist on the class;
  `CheckTypesMatch` extended to allow class→interface assignment when class
  implements that interface

| Interface vtable (itab) generation
| Done
| `data $itab_TFoo_IFoo = { l $TFoo_DoIt, ... }` emitted by `EmitInterfaceDefs`;
  method order matches interface declaration order; one itab per (class, interface)
  pair

| Interface variable assignment
| Done
| Interface var = fat pointer: `%_var_F_obj` + `%_var_F_itab` (two alloc8 slots);
  assignment stores obj pointer and itab address; ARC on interface refs deferred
  (Phase 3 follow-up)

| Interface method dispatch
| Done
| Load obj from `%_var_F_obj`, load itab from `%_var_F_itab`; index by method
  slot (slot × 8); indirect `call %fptr(l obj)` — same pattern as virtual dispatch

| `as` cast to interface
| Done
| `F := T as IFoo` — codegen calls `_GetItab(obj, $typeinfo_IFoo)`; non-nil
  result stored to `F_itab`; nil triggers `_Raise_InvalidCast`; handled
  inline in `EmitAssignment` (fat-pointer LHS + TAsExpr RHS)

| `is` test against interface
| Done
| `T is IFoo` — codegen calls `_ImplementsInterface(obj, $typeinfo_IFoo)`;
  walks class typeinfo impllist chain; RTL in `blaise_exc.c`

| RTL: `IInterface` base
| Done
| `IInterface` pre-registered as built-in interface type in
  `TSymbolTable.RegisterBuiltins`; no methods (ARC replaces COM ref-counting)

|===

=== Generics

[cols="2,1,3", options="header"]
|===
| Item | Status | Notes

| Generic type declaration parsing
| Done
| `type TBox<T> = class ... end;` — Delphi syntax (no `generic`/`specialize`);
  single and multiple type parameters.  Parsed as `TGenericTypeDef` in the AST.

| Generic class method implementations
| Done
| `procedure TList<T>.Add(Value: T);` in a separate block — parser detects
  `IDENT < TypeParams > DOT IDENT` and stores params in `OwnerTypeParams` on
  `TMethodDecl`; `LinkGenericClassMethodImpls` transfers bodies to the template
  before instantiation; `LinkClassMethodImpls` guards against treating these as
  non-generic qualified names.  RTL units use this form exclusively.

| Generic standalone function type parameters
| Done
| `function Min<T>(A, B: T): T;` — template registration and on-demand
  instantiation at call sites. Constraint syntax (`T: class`,
  `T: ISomeInterface`) landed alongside the boolean operator work.
  Tests in `cp.test.genericfuncs.pas`.

| Monomorphization: instantiation registry
| Done
| Demand-driven: on first use of `TBox<Integer>` in a var declaration, the
  semantic analyser creates a substituted clone of the class AST, resolves all
  field and method types with concrete args, analyses method bodies with the
  concrete type params in scope, and registers the instance in
  `TProgram.GenericInstances`.  Registry in `TSymbolTable.FGenerics` prevents
  duplicate instantiations.

| Codegen: monomorphized type emission
| Done
| Each instantiation emits its own QBE typeinfo, vtable (if virtual methods
  present), and method bodies.  Type names are mangled with `QBEMangle`:
  `TBox<Integer>` → `TBox_Integer`, `TPair<string,Integer>` →
  `TPair_string_Integer`.

| `TList<T>` in RTL
| Done
| `Generics.Collections` unit: `Grow`, `Add`, `Get`, `Delete`, `Clear`,
  `Free`, `Count` property.  Method bodies in the implementation section using
  the `TList<T>.Method` qualified-name syntax.  Full test suite in
  `cp.test.tlist.pas` — all 11 tests passing.

| `TDictionary<K, V>` in RTL
| Done
| `Generics.Collections` unit: parallel-array linear-scan map. `Grow`,
  `FindKey`, `Add` (upsert), `TryGetValue`, `ContainsKey`, `Remove`, `Count`
  property. Integer and string keys both supported; content-aware
  comparison uses the `_StringEquals` RTL helper for string types.

| `Generics.Defaults` in RTL
| Done
| `IEqualityComparer<T>` and `IComparer<T>` generic interfaces with
  `TIntegerEqualityComparer` and `TIntegerComparer` concrete implementations.
  13 tests in `cp.test.genericdefaults.pas` — all passing.

| `True` / `False` built-in constants
| Done
| Registered in `TSymbolTable.RegisterBuiltins` as `skConstant` of
  `FTypeBoolean`. Usable as literal Boolean values anywhere a Boolean
  expression is expected.

| ARC for interface references
| Done
| Refcount header on every class allocation (`_ClassAlloc` / `_ClassRelease`);
  compiler inserts addref/release on class and interface assignment and at
  scope exit; `Free` retained as a sanctioned synonym for `_ClassRelease`
  with slot nil-out; `[Weak]` attribute for cycle-breaking via
  `_WeakAssign` / `_WeakClear` / `_WeakZeroSlots`; RTL rewritten to use
  `Destroy` as the destructor hook (replaces `Free` on collections).

| RTL rewrite under ARC rules
| Done
| `TList<T>` and `TDictionary<K,V>` in `Generics.Collections` replace
  `procedure Free` with `procedure Destroy`; `Destroy` frees internal
  raw buffers (`FData` / `FKeys` / `FValues`) and nils them;
  `EmitFieldCleanupFn` invokes `Destroy` before field-level ARC cleanup
  whenever `HasDestroyMethod` is set; `tests/phase3_milestone.pas` updated
  to ARC rules; four new e2e valgrind tests added.

|===

*Phase 3 milestone:* `TList<Integer>` and `TDictionary<string, Integer>` compile
and pass a functional test suite. A program using both under valgrind shows
zero leaks. Achieved on 2026-04-22 (commit `d49ebb4`).

*Phase 3 complete.* All items — including universal ARC on `TObject`,
`[Weak]` cycle-breaking, and RTL rewrite under ARC rules — are done as of
2026-04-23.

== Class-Ownership Model — Decision Record

:revdate-decision: 2026-04-23
:status-decision: APPROVED

Interface references in Blaise must eventually participate in automatic
reference counting. Doing so requires first deciding how classes
themselves are managed, because interface ARC and class lifetime cannot
be designed independently. Three options were considered.

=== Option A — Universal ARC on `TObject`

Every class carries a refcount header. Assignment of a class or
interface variable addrefs; scope exit releases. `Free` is retained as
a sanctioned synonym for immediate release rather than removed.

Pros:

* One lifetime rule across the whole language — strings, classes, and
  interfaces all behave the same. Matches the "one clean dialect" ethos.
* Eliminates the entire class of use-after-free and leak bugs that
  Delphi still ships with.
* No dual class hierarchy; `IFoo := Obj` always works without the
  developer having to know which base class the object inherits from.
* `[Weak]` is a single, uniformly-applied concept rather than a
  subset-of-classes concern.
* Removes the best-known Object Pascal inconsistency — that strings and
  interfaces are ARC-managed but classes are not.

Cons:

* Severe porting friction for Delphi/FPC codebases that rely on explicit
  `try..finally Obj.Free`. Mitigated by retaining `Free` as release and
  by migration-analyser support (Phase 8).
* Small per-allocation cost (refcount header) and per-assignment cost
  (addref/release pair) on every class, not only interfaced ones.
* Cycles become a pervasive concern across any non-trivial object graph,
  raising the floor of language knowledge required to write correct code.
* Destructor timing becomes refcount-driven rather than programmer-driven,
  changing the subjective feel of `Destroy` compared with Delphi.
* Existing Blaise RTL (`TList<T>`, `TDictionary<K,V>`) uses explicit
  `Free` and must be reworked under the new rules.

=== Option B — `TInterfacedObject` alongside `TObject` (Delphi model)

`TObject` stays manually managed. A separate `TInterfacedObject` base
class carries the refcount. Interface references addref/release only
when the backing object descends from `TInterfacedObject`.

Pros:

* Direct Delphi compatibility — ported code works largely as-is and
  developer muscle memory transfers without retraining.
* Developer opt-in; ARC costs are paid only where the developer chose
  them.
* `try..finally Obj.Free` patterns continue to work everywhere they
  work today.
* `[Weak]` scope is smaller — only the interfaced-object subtree.
* Lowest-friction adoption path for the existing Object Pascal
  community.

Cons:

* Two lifetime models coexist in a single language. This is the same
  category of legacy wart that the project was founded to remove
  (multiple string types, multiple language modes, multiple object
  models).
* "Which base class do I inherit from?" becomes a papercut on every new
  class, and wrong choices are expensive to reverse later.
* Preserves the classic Delphi footgun: holding a plain `TObject`
  through an interface reference either leaks or double-frees depending
  on which side of the split is trusted.
* Interface-assignment codegen needs to branch on whether the backing
  class is refcounted, increasing ABI-boundary complexity.

=== Option C — Non-owning interface references

Interface references are borrowed views; they never addref or release.
Lifetime is controlled exclusively by the concrete class reference held
elsewhere in the program.

Pros:

* Simplest implementation — matches Blaise's current behaviour; almost
  no additional work required.
* Zero runtime cost on interface assignment.
* No cycle problem, because there is no ARC to cycle on.
* Consistent with the explicit `Obj.Free` philosophy as it stands today.

Cons:

* Silently incompatible with Delphi semantics. Code that relies on an
  `IFoo` reference keeping its object alive (factory methods, DI
  containers, observer lists, RAII-style resource wrappers) will compile
  cleanly and crash at runtime.
* Introduces dangling-reference hazards into a language that otherwise
  has none. Regresses Pascal's safety story.
* Interfaces collapse to a polymorphism and type-erasure tool only,
  losing their role as lifetime contracts. Large bodies of idiomatic
  Object Pascal become inexpressible.
* `[Weak]` becomes meaningless, since there is no strong reference to
  contrast with.

=== Decision — Option A

Blaise will adopt universal ARC on `TObject`. Every class allocation
includes a refcount header; the compiler inserts addref and release
calls at class and interface assignment sites and at scope exit; `Free`
is retained as a sanctioned synonym for immediate release.

The decision rests on three reasons:

Consistency with decisions already made::
The project has already accepted comparable compatibility costs in
pursuit of simplification — a single string type, removal of the `with`
statement, removal of old-style `object`, removal of COM GUIDs,
collapse to a single language mode. Option B would be the one place
that the project preserves a 1980s wart for convenience, and the wart
in question (classes are manual, interfaces are ARC) is arguably the
single most widely criticised inconsistency in modern Object Pascal.
Resolving it is precisely the kind of cleanup this project exists to do.

Porting cost is mostly deletion, not translation::
The dominant Delphi pattern is
`try Obj := TFoo.Create; ... finally Obj.Free; end`. Under option A the
`finally` arm simply disappears, and the migration analyser (Phase 8)
flags the sites. That is the cheapest class of migration available.
The genuinely difficult work — lifetime auditing, cycle identification —
has to be done under option B as well, the moment a codebase touches
interfaces; option B does not actually spare a careful porter from any
of it.

Pays forward for new code, not backward for old::
Option B optimises for developers porting existing Delphi code once.
Option A optimises for every developer writing new Blaise code for the
life of the language. Given the project premise is that Object Pascal
in 2026 needs a fresh start rather than another FPC, the future cohort
is the right constituency to favour.

Constraints attached to the decision:

* `Free` is retained as a keyword-level synonym for immediate release
  and nil-out. It is neither an error nor a silent no-op. This preserves
  developer muscle memory and reduces mechanical migration cost to
  near-zero.
* `[Weak]` must land in the same release as universal ARC. ARC without
  a cycle-breaker would be a liability.
* The Blaise RTL is rewritten to match the new rules as part of the
  same work package.
* The documentation frames this as a deliberate break with Delphi's
  manual-class model, in the same register as the single-string-type
  decision — not as an accident of implementation.

The decision flips to option B only on evidence that migrating existing
Delphi codebases is the project's primary adoption channel. Current
premises assume the primary channel is new developers writing new code,
so option A stands.

== Landscape Notes

* `lacsap`, `mselang`, `wanderlan/llvm-pascal` — all abandoned or incomplete.
  Each built the compiler first and died on the ecosystem.
* FPC 3.2.4-rc1 appeared mid-2025; development continues but slowly.
  The governance problem (140+ unmerged PRs) is real and ongoing.
* Oxygene (RemObjects) targets .NET — a different audience.
* https://c9x.me/compile/[QBE] — underexplored by the Pascal community, used by
  the Hare language. Strong fit for the bootstrap phase.
