Commit graph

84 commits

Author SHA1 Message Date
Graeme Geldenhuys 5894411a35 chore(license): relicense from BSD-3-Clause to Apache 2.0 + Runtime Library Exception
Adopts the Swift/LLVM model: Apache License 2.0 with the Runtime Library
Exception text used verbatim by the Swift project. SPDX identifier:
Apache-2.0 WITH Swift-exception.

Apache 2.0 brings an explicit patent grant and patent-retaliation clause
that BSD-3 lacks. The Runtime Library Exception ensures binaries
produced by the Blaise compiler do not inherit attribution obligations
from the linked-in RTL.

* LICENSE — full Apache 2.0 + RLE text (replaces the deleted LICENCE).
* NOTICE — project header plus QBE attribution (MIT, vendored).
* docs/language-rationale.adoc — new Project Governance section
  capturing the decision, alternatives considered, and rationale.
* SPDX headers updated across all .pas, .pp, .inc, .c source files,
  build scripts, PasBuild plugins, and the Makefile.
* project.xml license field updated.
* tools/migrate_full.py HEADER template updated so generated
  self-hosting source carries the new licence.
2026-05-03 19:45:29 +01:00
Graeme Geldenhuys 5af72daac7 feat(exceptions): typed except handlers and bare re-raise (Step 8)
Implements `on E: TClass do` dispatch inside except blocks and correct
bare `raise` propagation inside typed handlers.

New AST node TExceptHandlerClause carries VarName, TypeName, and Body.
TTryExceptStmt gains a Handlers list and ElseBody for the typed case;
ExceptBody remains for the legacy catch-all form.

Parser detects the 'on' identifier after 'except' to switch between the
typed-handler path and the plain catch-all path.  The no-binding form
`on TFoo do` is also supported.

Semantic pass validates that handler types resolve to classes, injects
a synthetic TVarDecl into the enclosing block for each bound variable
(so EmitVarAllocs allocates a stack slot at function entry), and opens a
local scope so the variable is accessible inside the handler body.

Codegen emits a _CurrentException call (before _PopExcFrame) followed by
an _IsInstance check per handler.  The first matching handler stores the
exception pointer into the variable slot and runs the body.  If no handler
matches and there is no else clause, _Reraise propagates to the enclosing
handler.

RTL: _CurrentException now reads g_current_exception (set by _Raise at
raise time) rather than g_exc_top->exception.  This is correct because
_PopExcFrame has already unwound the handler's own frame by the time a
bare raise is executed inside the handler body.

EmitRaiseStmt bare-raise path now calls _CurrentException then _Reraise
instead of _Raise(0), which previously cleared g_current_exception.

22 new tests (17 unit + 5 E2E): parser, semantic, codegen, and runtime
dispatch (correct handler selection, subclass matching, unmatched reraise,
bare raise propagation, else clause execution).  Total: 1131 tests, 0 failures.
2026-05-03 10:30:15 +01:00
Graeme Geldenhuys a2d0f2cbe4 fix(self-host): three Int64-correctness bugs unblock fixpoint
The self-built compiler segfaulted in MemCopy/_StringCopy when given any
multi-unit input, and even when it ran it produced IR that diverged from
the FPC-built compiler's output. Three independent bugs all rooted in
Int32-vs-Int64 confusion:

1. _StringCopy(S, From, MaxInt) — the Copy(S, N, MaxInt) "rest of string"
   idiom — performed `(Start + Count) > SLen` to clamp Count, which
   silently overflowed for Count = MaxInt. The check now compares
   `Count > SLen - Start` instead, which is overflow-safe.

2. _StrToInt / _StrToInt64 only accepted decimal digits, silently
   returning 0 for the FPC-compatible '$xx' hex form. The compiler uses
   hex for FNV constants ($811C9DC5 etc.), so the self-built binary
   produced 0 where it should produce a real FNV offset. Both routines
   now accept '$' followed by [0-9A-Fa-f]+.

3. Comparing an Int64 against any non-Int64 operand (notably the literal
   0 in `if N < 0`) emitted `csltw` — a 32-bit comparison — even when one
   side was 64-bit. _Int64ToStr's negative-sign check therefore decided
   sign by inspecting the low 32 bits of N, so any Int64 whose low 32
   bits had the sign bit set printed as a wrapped negative. The codegen
   now picks `csltl/csgtl/cslel/csgel/ceql/cnel` and sign-extends the
   non-Int64 side when either operand is Int64.

Two ConstValue emit sites also switched from `%d` (truncated to int32 by
the C `_StringFormat` runtime) to `IntToStr(...)` + `%s`, so Int64
constants survive the trip through Format.

Result: stage-2 (FPC-built) and stage-3 (self-built) IR are now
byte-identical at 88,883 lines — fixpoint achieved.

Three new e2e regression tests cover the StrToInt hex form, the
Copy MaxInt idiom, and the Int64 sign-handling case.
2026-05-02 21:22:39 +01:00
Graeme Geldenhuys 6bc88f2cde fix(codegen): implicit-Self field as base of chained access
EmitInstancePtr handled the IsImplicitSelf flag on TIdentExpr but not on
TFieldAccessExpr leaves (Base=nil, RecordName a field of Self). For a
chain such as FInner.Leaf.Value inside a method, the inner FInner.Leaf
node was emitted as `loadl %_var_FInner` — a phantom local — causing
QBE to reject the IR with `invalid type for first operand`.

This blocked self-hosting: TOPDFEmitter accesses FProgram.Block.* and
FProgram.SymbolTable.* in several methods, all of which produced
%_var_FProgram references that QBE refused, silently dropping every
function defined after the first error and breaking the link step.

Add the equivalent IsImplicitSelf branch in the TFieldAccessExpr leaf
path, mirroring the TIdentExpr handling. Add a regression test using a
3-level chain (the minimum that exercises the buggy path).
2026-05-02 20:28:28 +01:00
Graeme Geldenhuys 1931b84bc8 fix(compiler): self-hosting fixpoint unblocks — Cardinal, PtrUInt, xor, _ClassRelease
Seven source-compatibility fixes required to get the Blaise compiler to
parse and analyse its own source during the self-hosting fixpoint attempt.
None of these affect compiled output for existing programs; all 1105 tests
still pass.

  Cardinal alias (uSymbolTable): maps to UInt32; used in uDebugOPDF.pas
  PtrUInt alias (uSymbolTable):  maps to Int64; used in uDebugOPDF.pas
  xor operator (uLexer/uAST/uParser/uSemantic/uCodeGenQBE): tkXor/boXor
    emit QBE 'xor'; used in uDebugOPDF.pas FNV hash
  _ClassRelease external (blaise_arc.pas interface): C function declared
    callable from Pascal; contnrs.pas calls it directly
  contnrs.pas uses blaise_arc: makes _ClassRelease visible
  not 7 → -8 (uDebugOPDF.pas): Blaise 'not' is Boolean-only; -8 = not 7
  TProcess.Options removed (Blaise.pas): Blaise RTL TProcess has no
    Options property; pipes are always active
  FOutput.Strings[] (uDebugOPDF.pas): Blaise TStringList has no default
    [] property; must use explicit .Strings[I] form

Stage-2 IR now generates: 88,526 lines, exit 0.
Remaining blocker: %_var_FProgram QBE error (see handover.txt).
2026-05-02 19:59:04 +01:00
Graeme Geldenhuys c657cccc11 feat(rtl+compiler): string data-pointer ABI — consistency with class objects
Changes Blaise's string variable convention from header-pointer to
data-pointer, matching how class objects work: the pointer stored in a
variable slot points to the first useful byte, with the ARC bookkeeping
header immediately before it at negative offsets.

New layout (data-pointer convention):
  data_ptr - 12  RefCount  (4 bytes, Integer)
  data_ptr -  8  Length    (4 bytes, Integer)
  data_ptr -  4  Capacity  (4 bytes, Integer)
  data_ptr +  0  char data (UTF-8 bytes + NUL terminator)

Old layout (header-pointer convention):
  header_ptr +  0  RefCount
  header_ptr +  4  Length
  header_ptr +  8  Capacity
  header_ptr + 12  char data

Benefits:
- Consistency: both strings and class objects use "pointer to content"
  convention; ARC header hidden at negative offsets for both
- S[N] subscript: simplifies from (ptr + N + 11) to (ptr + N - 1)
- PChar(str) cast: becomes an identity — str IS already the data pointer
- C interop: zero-cost on non-Win32 targets (Linux, macOS POSIX)
- OPDF: recAnsiStr now emits correct negative LengthOffset/RefCountOffset

RTL changes (all six string-layer files):
  blaise_str.pas   StrAlloc returns base+12; StrLen reads ptr-8;
                   StrData is identity; all callers updated
  blaise_arc.pas   _StringAddRef/Release access ptr-HDR_SIZE;
                   _StringEquals/_StringConcat updated
  blaise_sys.pas   _SysWriteStr/Int/Int64 read len at ptr-8
  blaise_str_fmt.c str_alloc returns base+HDR_SIZE; str_data identity;
                   str_len reads ptr-8
  blaise_io.c      io_str_alloc/data/len updated; all callers fixed
  blaise_process.c proc_str_alloc/data/from_cstr updated

Compiler codegen:
  EmitStrLit    adds 'add $__sN, 12' to convert header label to data ptr
  S[N]          'add idx, 11' → 'sub idx, 1'
  PChar(str)    '+12 add' removed — identity pass-through
  for B in S    length from ptr-8; data from ptr+0 (no skip offset)

OPDF emitter: LengthOffset=-8, RefCountOffset=-12 (was +4, +0)

Tests: 2 test assertions updated to match new ABI semantics;
1105 total, all passing.
2026-05-02 12:26:13 +01:00
Graeme Geldenhuys 4f82c7cc91 feat(compiler): for..in — string byte iteration (Step 7c)
Adds 'for B in S do' iteration over the raw UTF-8 bytes of a string,
consistent with S[N] returning Byte.  Loop variable may be any ordinal
type (Byte, Integer, etc.).

- uAST: TForInStmt gains IsStringIter field
- uSemantic: tyString branch in for-in dispatch — requires ordinal loop
  variable; injects synthetic __idx_N (Integer) slot into block Decls
- uCodeGenQBE: IsStringIter path — 0-based index, condition reads
  length from header at ptr+4 (csltw), element loaded via
  loadub(ptr+12+idx); loop variable assigned with storew

Language layout reminder: string header is [refcount(4)][length(4)]
[capacity(4)][data...] so length is at offset +4 and char data at +12.

Docs: language-rationale.adoc updated — string byte iteration now listed
as supported under 'for..in'; set iteration and generic collection
iteration noted as deferred.

Tests: 8 new tests (3 semantic + 5 codegen); 1105 total, all passing.
2026-05-02 11:12:52 +01:00
Graeme Geldenhuys 244dfe3259 feat(compiler): for..in — static array iteration (Step 7b)
Extends TForInStmt with an IsArrayIter path for 'for X in Arr do'
where Arr is a static array[L..H] of T.

- uAST: TForInStmt gains IsArrayIter, IdxVarName, ArrayLow, ArrayHigh
- uSemantic: collection-kind dispatch — tyStaticArray goes to the
  index-based path; tyClass goes to the enumerator protocol path;
  anything else is an error.  The static-array path injects a synthetic
  __idx_N (Integer) slot into the enclosing block for EmitVarAllocs.
- uCodeGenQBE: EmitForInStmt opens with an IsArrayIter branch that
  emits an index-counter loop: init to ArrayLow, cslew to ArrayHigh,
  element load via (base + (idx-low)*elemSize), assign to loop var
  with ARC handling for string/class elements, increment + jmp back.
  Non-zero-based ranges subtract LowBound before the multiply.

Tests: 8 new tests (5 semantic, 3 codegen for arrays); 1097 total,
all passing.
2026-05-02 11:01:57 +01:00
Graeme Geldenhuys 4ddc1944bd feat(compiler): for..in loop — class-based enumerator protocol (Step 7a)
Implements `for X in Collection do` iteration using the GetEnumerator
protocol, matching FPC/Delphi semantics.  This first commit covers
class-based enumerators; static array and byte-string iteration are
deferred to follow-on commits.

Compiler changes:
- uAST: new TForInStmt node (VarName, CollExpr, Body; semantic
  annotations: EnumVarName, ResolvedVarType, GetEnumDecl, MoveNextDecl,
  CurrentDecl)
- uParser: ParseForStmt returns TASTStmt; detects 'for X in' vs
  'for X :=' and produces TForInStmt or TForStmt respectively
- uSemantic: AnalyseStmt handles TForInStmt — verifies GetEnumerator/
  MoveNext/Current protocol, checks type compatibility, injects a
  synthetic TVarDecl (__forin_N) into the enclosing block so
  EmitVarAllocs allocates the slot and EmitArcCleanup releases it
- uCodeGenQBE: EmitForInStmt — calls GetEnumerator, ARC-stores result
  in synthetic slot, loops via MoveNext, reads Current, assigns to
  loop variable; handles both static and virtual dispatch
- uDebugOPDF: CollectStmtLines extended with TForInStmt case

RTL additions:
- classes.pas: TStringListEnumerator + TStringList.GetEnumerator
- contnrs.pas: TObjectListEnumerator + TObjectList.GetEnumerator

Docs:
- grammar.ebnf: ForStmt gains the 'FOR IDENT IN Expr DO Stmt' alternative
- language-rationale.adoc: new Iteration section documenting the protocol
  choice and what is deferred

Tests: 17 new tests in cp.test.forin (parser, semantic, codegen);
1089 total, all passing.
2026-05-02 10:21:12 +01:00
Graeme Geldenhuys 7a177f4609 feat(compiler): export standalone functions and global data — Step 6b
Standalone procedures/functions now emit 'export function' in QBE IR
instead of bare 'function'. Global data items now emit 'export data'.
This makes these symbols globally visible (.globl in assembly output),
which is required for the OPDF companion file to reference them via
.quad directives at link time.

Without export, QBE emits local-only labels that the linker cannot
resolve from a separate object file (.opdf.s).
2026-05-01 22:54:15 +01:00
Graeme Geldenhuys 345489fec1 feat(rtl): eliminate libc printf/fprintf and most pure-C helpers — Step 9 (80%)
Write/WriteLn now call _SysWriteStr/_SysWriteInt/_SysWriteInt64/
_SysWriteNewline (blaise_sys.pas + blaise_sys_posix.c) instead of
printf/fprintf.  _SysWriteNewline uses POSIX write(2) directly;
no format strings, no libc I/O buffering.

Pure Pascal implementations replace these libc calls in the RTL:
  - toupper / tolower  (inline ASCII arithmetic in blaise_str.pas)
  - strlen             (NUL-scan loop in blaise_str.pas)
  - memcpy             (byte-copy loop in both blaise_str.pas and blaise_arc.pas)
  - memcmp             (byte-compare loop in blaise_arc.pas)

Remaining libc dependency: malloc/free (needs Pascal allocator — Step 9d).

All 1035 compiler tests pass.
2026-05-01 17:22:07 +01:00
Graeme Geldenhuys 584f0378e2 feat(rtl): migrate blaise_arc.c → blaise_arc.pas — Step 9c
Port all non-function-pointer ARC functions from C to Pascal:
  _StringAddRef, _StringRelease, _StringEquals, _StringConcat,
  TObject_Destroy, _ClassAddRef, _ClassFree

Remaining in C (blaise_arc_class.c — renamed from blaise_arc.c):
  _ClassAlloc and _ClassRelease — store and call a function pointer
  (the $_FieldCleanup_TypeName hook), which Blaise cannot express yet.

Compiler fixes required to support this port:
- uCodeGenQBE: EmitFuncDef now exits early for IsExternal declarations;
  previously it crashed (access violation) when iterating a nil body
- uSymbolTable: remove _ClassAddRef and _ClassRelease from RegisterBuiltins;
  they are internal RTL symbols emitted directly by the codegen, not
  user-callable builtins, and pre-registering them caused "Duplicate
  identifier" errors when the unit interface re-declared them

RTL Makefile: blaise_arc_class.c replaces blaise_arc.c in C_SRCS;
blaise_arc.pas added to PAS_OBJS with its own build driver.

All 1035 compiler tests pass.
2026-05-01 16:26:17 +01:00
Graeme Geldenhuys f68620e0b5 feat(compiler): mod operator, param assignment, PChar arithmetic, blaise_str.pas — Step 9b
- Add `mod` operator: tkMod/boMod in lexer+AST+parser, QBE `rem` in codegen
- Allow assignment to value parameters (skParameter) in semantic checker
- PChar + Integer arithmetic: semantic and codegen now accept PChar on pointer-arithmetic LHS
- PChar nil comparison: nil is now compatible with PChar in all comparison/assignment contexts
- Untyped Pointer ↔ PChar: CheckTypesMatch accepts PChar where Pointer expected and vice-versa
- Type cast widening fix: Int64(IntExpr) emits `extsw` instead of invalid `l copy w`
- CoerceArg helper: sign-extends w arguments to l at all call sites (proc/func/method)
- blaise_str.pas: Pascal port of blaise_str.c — compiles cleanly and passes runtime test

All 1035 tests pass. blaise_str driver produces correct output for IntToStr, StrToInt,
StringLength, StringUpperCase, StringLowerCase, StringTrim, StringPos, StringCopy, Chr.
2026-05-01 14:33:37 +01:00
Graeme Geldenhuys 179d2e1b50 feat(compiler): external declaration support — Step 9a
Add the 'external' directive to proc/function/method declarations,
enabling Pascal code to bind to C symbols without a body:

  procedure Foo; external;
  function Bar: Integer; external name 'c_bar';

- uLexer: tkExternal token; mapped in the identifier fast-path
- uParser: ParseMethodDecl and ParseForwardDecl consume 'external'
  and optional 'name <string>'; body-trigger check is suppressed for
  external declarations so the following 'begin' is not consumed as a
  method body
- uAST: TMethodDecl.IsExternal, TMethodDecl.ExternalName
- uSemantic: external interface decls are exempt from the
  "no implementation" check; AnalyseStandaloneDecl skips AnalyseBlock
  when IsExternal is set
- uCodeGenQBE: EmitProcCall and TFuncCallExpr codegen use ExternalName
  as the QBE symbol when set; no function body emitted for external decls
- grammar.ebnf: MethodDirective / ExternalDirective rules; MethodDecl
  and StandaloneDecl updated to use { MethodDirective SEMICOLON }

11 new tests in cp.test.external — all 1035 tests pass.
2026-05-01 13:47:24 +01:00
Graeme Geldenhuys 7ff298165b feat(compiler): full const support — method bodies, class-level, grammar
- Fix ParseMethodDecl: add tkConst to body-trigger condition so const
  blocks are parsed inside method, procedure, and function bodies
- ParseConstBlock now accepts TObjectList directly (was TBlock) so it
  can be called uniformly from all scope positions
- Class body loop: handle CONST sections (→ TClassTypeDef.ConstDecls)
  and VAR keyword before field declarations
- TClassTypeDef.ConstDecls: new owned TObjectList of TConstDecl
- TFieldAccessExpr: new IsConstant/ConstValue/ConstString fields set by
  uSemantic when TypeName.ConstName resolves to a class-level constant
- uSemantic: register class constants as both unqualified and qualified
  (TFoo.MaxItems) symbols; resolve TypeName.ConstName in field-access
  analysis before raising "Unknown class method" error
- uCodeGenQBE: emit integer copy or _StringRetain for IsConstant paths
- docs/grammar.ebnf: add ConstSection/ConstDecl/ConstExpr rules;
  update Block, InterfaceSection, ImplementationSection, ClassDef, and
  GenericClassDef to allow ConstSection

All 1024 tests pass including 15 new const-scope regression tests.
2026-05-01 13:08:26 +01:00
Graeme Geldenhuys 5ec85211bd feat(compiler+rtl): multi-file self-hosting fixpoint — Step 11
Fixes required to reach byte-identical IR between stage-1 and stage-2
when compiling the multi-file Blaise.pas source.

Compiler fixes:
- uCodeGenQBE.pas: QbeEscapeString — replace Format('%02x',[C]) with
  manual hex encoding (Hi := C shr 4; Lo := C and 15; Chr(...));
  Blaise's _StringFormat does not support %x, causing non-ASCII bytes
  (e.g. UTF-8 em-dash U+2014) to be emitted as \%02x instead of \E2\80\94
- uCodeGenQBE.pas: EmitFuncCallExpr — route IntToStr to _Int64ToStr when
  the argument resolves to Int64 (QBE type 'l'), matching FPC's overloaded
  IntToStr resolution; prevents 64-bit values being truncated to Int32
  before string conversion
- uSymbolTable.pas: MaxInt changed from Int64 (9223372036854775807) to
  Integer (2147483647); all uses are Copy(S,N,MaxInt) meaning "rest of
  string", and 2147483647 is handled correctly by _StringCopy; avoids
  Int64 literal truncation/extsw issues in the self-hosted binary

RTL fixes:
- blaise_str.c: _StringCopy — negative count (64-bit MaxInt truncated to
  int32 = -1) now treated as "rest of string" rather than 0
- blaise_str.c: add _UpCase helper
- classes.pas: TStringList.GetText — add trailing #10 to match Delphi/FPC
  TStringList.Text semantics (each line including the last ends with LF);
  SaveToFile updated to not double-add the trailing newline

Tests: 1009 pass
Fixpoint: diff /tmp/multifile.ssa /tmp/multifile2.ssa → empty
2026-05-01 09:26:39 +01:00
Graeme Geldenhuys d20970f27b feat(compiler+rtl): multi-file self-hosting pipeline — Step 10
Fixes required to get the multi-file compiler binary (blaise-multi)
to compile Blaise.pas without crashing.

RTL fixes:
- contnrs.pas: TObjectList ARC — Add/Put/Delete/Clear/Destroy now call
  _ClassAddRef/_ClassRelease so stored objects survive beyond their
  creating scope; fixes premature-free of TTypeDecl nodes during
  AnalyseUnitForExport
- blaise_str.c: _StringCopy — treat negative count as "rest of string"
  instead of 0; 64-bit MaxInt (0x7fffffffffffffff) is truncated to -1
  when passed as a 32-bit w argument to _StringCopy, which was causing
  Copy(S, 2, MaxInt) to return empty string in FindTypeOrInstantiate
- blaise_str.c: add _UpCase C helper
- blaise_arc.c: minor ARC fixes

Compiler fixes:
- uAST.pas: add IsVarParam flag to TMethodCallStmt and TMethodCallExpr
- uSemantic.pas: set IsVarParam when object symbol is skVarParameter
- uCodeGenQBE.pas: emit double-dereference for method calls on var/out
  parameters — the local slot holds the caller's address, not the
  object pointer, so one extra loadl is needed to reach the receiver
- uSymbolTable.pas, uUnitLoader.pas, uParser.pas, uLexer.pas: various
  fixes discovered during multi-file pipeline testing
- Blaise.pas: improved error reporting

Tests: 1009 pass
2026-05-01 06:52:48 +01:00
Graeme Geldenhuys 53235aa6b6 feat(compiler): self-hosting fixpoint reached — Step 10
Parser, lexer, tokeniser, semantic analyser and symbol table extended
to compile the multi-file Blaise source with the Blaise compiler itself.
Stage-2 and stage-3 IR are byte-identical; fixpoint confirmed.

Changes to reach fixpoint:
- uLexer: source filename parameter for actionable parse error messages
- uParser: method directive loop (inline/stdcall/abstract/…), unit
  interface/implementation loops for var/const/type blocks, chained
  field-assignment (A.B.C := v), filename in all EParseError messages
- uPasTokeniser: full rewrite to remove FPC-only features (char sets,
  shr, Exit(value), private/public visibility) so Blaise can compile it
- uSymbolTable: remove unsupported default parameter values
- uSemantic: replace Exit(value) with Result+Exit; expand five `with`
  statement blocks to explicit typed local variables
- uAST, uCodeGenQBE: minor additions supporting the above
- uUnitLoader: pass filename to TLexer.Create
- classes.pas: inline dupIgnore literal (const section deferred)
2026-04-30 13:30:30 +01:00
Graeme Geldenhuys e568ce21f2 feat(rtl+compiler): Process.pas + C helper — Step 8
Add process management infrastructure for multi-file self-hosting:

rtl/src/main/c/blaise_process.c (new)
  - BlaiseProcess struct with per-handle exe, argv, pipe fd, pid
  - _ProcessCreate / _ProcessFree: alloc/dealloc
  - _ProcessSetExe / _ProcessAddArg: configure before Execute
  - _ProcessExecute: fork+exec with stdout+stderr piped
  - _ProcessRunning: WNOHANG waitpid check
  - _ProcessReadOutput: blocking read, returns '' on EOF
  - _ProcessWaitOnExit / _ProcessExitCode: reap child
  - Windows stubs via #ifdef _WIN32

rtl/src/main/pascal/Process.pas (new)
  - TProcess class backed by the above C helpers via built-ins
  - Execute, ReadOutput, WaitOnExit, Running, ExitCode, Parameters

compiler (uSymbolTable / uSemantic / uCodeGenQBE)
  - 9 new process built-ins: ProcessCreate, ProcessSetExe,
    ProcessAddArg, ProcessExecute, ProcessRunning, ProcessReadOutput,
    ProcessWaitOnExit, ProcessExitCode, ProcessFree

compiler/src/main/pascal/Blaise.pas
  - RunProcess: open-array → TStringList; loop uses ReadProcessChunk
    wrapper (FPC-compatible); call sites rebuilt with explicit TStringList
  - ReadProcessChunk wraps FPC's Output.Read for stage-1; will be
    replaced by Proc.ReadOutput at Step 10

Tests
  - cp.test.process.pas: 18 semantic + codegen tests for all built-ins
  - cp.test.e2e.pas: TestRun_ProcessBuiltins_CapturesOutput,
    TestRun_ProcessBuiltins_ExitCode (verify fork/exec/pipe at runtime)
2026-04-29 18:31:55 +01:00
Graeme Geldenhuys aa5ff0495b feat(compiler): replace CreateFmt with Create(Format(...)) — Step 7
Replace all 83 .CreateFmt(...) calls across 5 compiler unit files with
.Create(Format(...)) to eliminate dependency on Exception.CreateFmt
(which requires array-of-const / TVarRec, unsupported in Blaise):

  uLexer.pas:       2 replacements
  uParser.pas:      61 replacements
  uSemantic.pas:    8 replacements
  uCodeGenQBE.pas:  10 replacements
  uUnitLoader.pas:  2 replacements

The Format call retains FPC-compatible [args] array notation so the
multi-file source continues to compile under FPC.  The hand source
(blaise-compiler.pas) already uses Format without array brackets and
is unaffected.

All 975 tests pass.  Fixpoint verified.

Also: docs/future-improvements.adoc — multi-line string literals section
expanded with visual before/after examples for each candidate syntax
(heredoc, backtick, triple-brace).
2026-04-29 16:12:52 +01:00
Graeme Geldenhuys ef7a98c687 feat: indexed properties, StrToInt64/Int64ToStr built-ins; restore fixpoint
- Indexed properties fully implemented across parser, semantic analyser,
  symbol table, and codegen (read and write, with index type-checking)
- StrToInt64 and Int64ToStr built-ins added (RTL: _StrToInt64, _Int64ToStr)
- Int literal and const codegen use Int64ToStr to avoid int32 truncation
- QbeEscapeString in hand source now uses manual hex arithmetic instead of
  Format('%02x') which Blaise's _StringFormat does not support
- Copy(..., MaxInt) calls replaced with Copy(..., Length(x)) to avoid RTL
  truncation of the 64-bit sentinel through _StringCopy's int32 parameter
- Stage-3 IR is byte-identical to stage-2: fixpoint holds after all changes
2026-04-29 12:18:45 +01:00
Graeme Geldenhuys 81d9e9f320 feat: ChangeFileExt, ExtractFileName, ExtractFilePath, IncludeTrailingPathDelimiter as built-ins
Implements step 11 of the self-hosting feature list: the four SysUtils
path-manipulation functions needed by the compiler source.  Backed by C RTL
helpers in blaise_io.c; registered as compiler built-ins following the same
pattern as GetEnvVar.  Language-rationale records the decision and the
migration path to a Blaise-native SysUtils shim once multi-unit support lands.
2026-04-28 22:08:36 +01:00
Graeme Geldenhuys f1580333ac feat: GetEnvironmentVariable as built-in alias for GetEnvVar
Registers GetEnvironmentVariable (FPC/Delphi-compatible name) as a built-in
function alongside the existing GetEnvVar, both mapping to the RTL
_GetEnvVar C function. Required for self-hosting: Blaise.pas uses the
FPC name to locate the RTL and QBE binary at runtime.

No new RTL code — _GetEnvVar (blaise_io.c) handles both names.
Adds 2 tests in cp.test.selfhosting to verify semantic and codegen.
2026-04-28 19:14:37 +01:00
Graeme Geldenhuys 5152977987 feat: set types — set of TEnum, set literals, in, Include/Exclude, arithmetic
Implements Pascal bit-set types end-to-end:

- Lexer: tkSet, tkIn keywords
- AST: TSetTypeDef node; boIn binary operator
- Symbol table: tySet, TSetTypeDesc (w ≤32 members, l ≤64); NewSetType factory;
  Include/Exclude registered as built-in procedures
- Parser: ParseSetDef; in operator in expression parser
- Semantic: set type registration; set literal validation; boIn membership check;
  set arithmetic (+/-/*) and equality (=/≠) operator analysis; Include/Exclude
  argument validation
- Codegen: compile-time bitmask for set literals; shr+and for `in`; or/xor-and/and
  for union/difference/intersection; OR-in-place (Include), AND-NOT-in-place (Exclude)
- Tests: 32 new tests in cp.test.sets covering parse, semantic, and codegen
- Fix: rename Set→Assign method in genericmethodimpls test (set is now reserved)
- Docs: SetType/SetLiteral grammar rules; Set Types rationale section; resolve
  enum underlying type open question (confirmed w)
2026-04-28 19:14:28 +01:00
Graeme Geldenhuys f6856e366a feat: @expr address-of operator for array elements and local variables
Adds the @ prefix operator so callers can take the address of a static-array
element (@Arr[I]), an open-array element, or any local scalar variable and
store or pass the resulting typed pointer (^T).  No load is emitted — the
element address is returned directly from the pointer arithmetic that was
previously discarded after the subscript load.
2026-04-28 13:53:12 +01:00
Graeme Geldenhuys c078e7c5de feat: PChar type + PChar(str) / string(pchar) casts
Adds tyPChar as a distinct opaque-pointer type kind (maps to l in QBE IR).
PChar(str) emits add str_ptr, 12 to skip the 12-byte ARC header, yielding
a valid C char*. string(pchar) calls _StringFromPChar (new RTL function in
blaise_str.c) which measures strlen, allocs an ARC string, and copies.
PChar(pchar_expr) is an identity cast. Variable declarations allocate an
8-byte nil-initialised pointer slot.

6 new tests; all 898 tests pass.
2026-04-28 13:20:43 +01:00
Graeme Geldenhuys e4f3b9e38b feat: Low() / High() for static arrays
Low(A) and High(A) on a static array variable now emit compile-time
constants (copy LowBound / copy HighBound) rather than a runtime slot
load. The semantic pass accepts tyStaticArray alongside tyOpenArray.
4 new tests; all 892 tests pass.
2026-04-28 12:35:05 +01:00
Graeme Geldenhuys c365b77f56 feat: static array array[L..H] of T — stack-allocated fixed-size arrays
Adds TStaticSubscriptAssign AST node, ParseTypeName extension for
array[L..H] of T syntax (tkDotDot token), TStaticArrayTypeDesc with
RawSize for correct element-footprint sizing (1 for Byte, 4 for Integer),
and full codegen: alloc4/alloc8 + memset zero-init, storeb/storew for
writes, loadub/loadw for reads, LowBound subtraction for non-zero ranges.

Also includes array literal ['a','b','c'] call-site support (step 4):
TArrayLiteralExpr, open-array formal matching, stack buffer allocation,
and codegen emitting two QBE args (data ptr + compile-time high index).

13 new static-array tests + 9 new array-literal tests; all 888 tests pass.
2026-04-27 17:22:29 +01:00
Graeme Geldenhuys 4feb1b3867 feat: const open array param (array of T) — two-register ABI
Adds support for open-array parameters of the form `const A: array of T`
using the standard Pascal two-register ABI: a data pointer (`%_par_A`)
and a high-index value (`%_par_A_high`, count − 1).

- uLexer: add tkArray keyword token
- uAST: add IsOpenArray / IsConstParam fields to TMethodParam
- uSymbolTable: add tyOpenArray kind, TOpenArrayTypeDesc, NewOpenArrayType
- uParser: extend ParseParamList to recognise `array of T` syntax
- uSemantic: add ResolveParamType helper; handle High/Low builtins (moved
  before the FProcIndex lookup so they fire without a symbol-table entry);
  open-array subscript in AnalyseStringSubscriptExpr; open-array
  compatibility in CheckTypesMatch
- uCodeGenQBE: emit two params + two alloc slots per open-array formal;
  High/Low intrinsic emission; pointer-arithmetic subscript; two-arg
  forwarding at call sites; exclude open arrays from ARC addref/release
- Tests: 16 new tests covering parser, semantic, and codegen paths
- docs/grammar.ebnf: add ParamType rule (ARRAY OF TypeName | TypeName)
- docs/language-rationale.adoc: document open-array ABI decision
2026-04-27 16:31:04 +01:00
Graeme Geldenhuys 5dbdc35c1e feat: string subscript S[N] — byte access
S[N] returns the Nth byte (1-based) of a string as Integer.
Single-quoted ASCII literals coerce to their Ord value when compared
with a subscript result; multi-byte literals (e.g. emoji) are a
compile-time error.

Character data lives at string_ptr + 12 (after the 12-byte ARC header),
so S[N] emits: extuw index; add index, 11; add str_ptr, offset; loadub.

4 new tests in cp.test.stringops cover codegen (loadub, copy 104),
non-string subscript error, and multi-byte char literal error.
2026-04-27 13:34:41 +01:00
Graeme Geldenhuys 1ea878000d feat: multi-file compilation (v0.2.0)
Implement whole-programme multi-file compilation. The compiler now
resolves `uses` clauses, locates unit source files via `--unit-path`
search directories, compiles them, and merges exported symbols into
the programme scope. Combined QBE IR is emitted in dependency order.

New components:
- uUnitLoader: post-order DFS unit loader with cycle detection
  (EUnitNotFound, ECircularDependency); skips FPC RTL builtins
- TSemanticAnalyser.AnalyseUnitForExport: promotes unit interface
  symbols to global scope; implementation symbols stay scoped
- TCodeGenQBE.AppendUnit / AppendProgram: accumulate combined IR;
  FStrLitsEmitted tracks emitted string literals to avoid duplicates
- 8 new tests in cp.test.multifile covering loader, semantic export,
  and combined codegen

Language additions:
- `out` parameter modifier (treated as var — pass by reference)
- implementation-section `uses` clause in units

Blaise.pas wired to use loader pipeline; --unit-path flag is
repeatable; -Fu<path> FPC-style flags are honoured.

Design doc updated: Phase 4 (multi-file), phases 5–8 renumbered;
--unit-path added to CLI table; build-tool-drives-compiler principle
documented in constraints.

End-to-end verified: two-unit programme compiles and executes correctly.
2026-04-27 11:35:26 +01:00
Graeme Geldenhuys 2ca052a271 fix: free intermediate string temps in chained string concatenation
When EmitExpr generates code for A + B + C (string concat), the
intermediate temp T1 = A + B (from _StringConcat) starts with
refcnt=0.  Before this fix that temp was silently abandoned:

  calling _StringRelease(T1) with refcnt=0 would decrement to -1
  (the IMMORTAL sentinel), so the string was never freed and the
  compiler slowly exhausted all available memory when processing
  large source files.

The correct pattern is to temporarily own the intermediate before
the second concat and release it after, so _StringConcat has a
chance to copy its bytes first:

  _StringAddRef(T1)           ; 0 -> 1 (temporary ownership)
  T2 = _StringConcat(T1, C)  ; copies T1's bytes into T2
  _StringRelease(T1)          ; 1 -> 0 -> freed

The same ownership pattern is applied to the right operand when
it is itself an unowned string expression (TBinaryExpr, TFuncCallExpr,
or TMethodCallExpr).

Also mirrors EmitMethodCall var-param forwarding fix (IsVarParam
check for both ObjExpr path and main path) and the previously-landed
EmitVarArgAddr, IsGlobal, vtable-in-ctor-with-args fixes to the hand
source (tests/blaise-compiler.pas).

With this fix stage-2 (blaise-compiler compiled by itself) compiles
the full hand source in 17s using 23MB RSS instead of hitting the OOM
killer at 47GB.
2026-04-25 10:58:04 +01:00
Graeme Geldenhuys f6ac68f537 codegen: store vtable in constructor calls with arguments
TMethodCallExpr.IsConstructorCall (TypeName.Create with args) was missing
the storel $vtable_X instruction after _ClassAlloc.  The TFieldAccessExpr
path (no-arg Create) already had it.  The missing store left vtable pointer
as zero, causing _IsInstance to crash with null vtable dereference during
type analysis of TTypeDesc subclasses.

Also adds virtual Destroy to TTypeDesc in the hand source so TTypeDesc and
its subclasses (TRecordTypeDesc, TEnumTypeDesc, TInterfaceTypeDesc) get
vtable slots, making HasVTable return true and field offsets correct.

Regression tests added for both the no-arg and with-arg constructor vtable
store paths.
2026-04-25 00:01:12 +01:00
Graeme Geldenhuys 50c746996b codegen: fix var-param forwarding to use loaded pointer
When a var-param actual argument is itself a var parameter, the local
alloc slot holds the caller's address rather than being the address.
Passing %_var_Name directly gave the callee a pointer-to-slot instead
of the original caller's address, causing reads/writes to land in the
wrong location.

Add EmitVarArgAddr() which detects this case and emits loadl %_var_Name
to obtain the stored pointer before passing it. Applied at all seven
call-emission sites in uCodeGenQBE.

Root cause of TStringList_FindSorted writing Mid into Find's local slot
(overwriting the Idx pointer) instead of into the caller's Idx variable,
so LookupLocal always read Idx=0 after a sorted search.

Two regression tests added to cp.test.varparams.
2026-04-24 23:26:40 +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 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 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 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 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