- 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
Second self-hosting fixpoint: stage-3 (hand source compiled by stage-2,
itself compiled by stage-1) produces byte-identical IR to stage-2 at
59974 lines. New since v0.1.0: string subscript, char literals, const
open arrays, array literals, static arrays, Low/High, PChar, @expr
address-of, set types, GetEnvironmentVariable, and the four SysUtils
path functions. Also fixes _CurrentExceptionMessage (was always empty
due to _PopExcFrame being called before the except body).
Two fixes to reach the fixpoint milestone:
1. RTL: _CurrentExceptionMessage now reads from a dedicated
g_current_exception thread-local set in _Raise, rather than from
g_exc_top->exception. The codegen calls _PopExcFrame() before
executing the except body, so g_exc_top was already unwound and
the exception message was always empty.
2. hand source (tests/blaise-compiler.pas): replace the hand-written
ExtractFileName and ChangeFileExt helpers with the new compiler
built-ins (step 11). Add symbol-table registration, semantic
type-checking, and codegen for all four path functions.
Result: stage-1 (FPC) → stage-2 → stage-3 → stage-4 are all 59974
lines; diff stage3 vs stage4 is empty. Fixpoint achieved.
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.
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.
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)
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.
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.
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.
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.
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
Documents how Blaise's UTF-8 string type interacts with OS APIs:
POSIX/Xlib work directly via PChar; Windows W APIs require UTF-8 to
UTF-16LE conversion at the RTL boundary. Explains why this does not
motivate adding a UTF-16 string type to the language. Phase 6 item.
language-rationale.adoc records design decisions for the Blaise dialect:
single string type, no Char type, Byte subscript semantics, char literal
coercion, PChar (planned), ARC, out parameters, build-tool-drives-compiler,
and three open questions (integer width, enum storage, Result convention).
CLAUDE.md updated to require all future language design decisions to be
documented in language-rationale.adoc.
#N numeric char literals (e.g. #45 = Ord('-')) work without additional
implementation: UnescapeString converts them to single-byte strings,
CoerceToCharOrd handles the rest. Test added to lock in the behaviour.
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.
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.
Remove the -alpha suffix now that the compiler has reached self-hosting
fixpoint: stage-3 (hand source compiled by stage-2, itself compiled by
stage-1) produces byte-identical IR to stage-2. This is the first
official release of the Blaise compiler.
EmitExpr for TBinaryExpr now checks for boAnd/boOr first and emits
short-circuit branches (jnz to sc_rhs/sc_end labels) before evaluating
either operand. The eager `and` instruction previously emitted was
safe for bitwise use but caused a null-pointer crash when the compiler
compiled itself: TSymbolTable.FindType evaluated Sym.Kind even when Sym
was nil, because the code generator did not short-circuit the guard
`(Sym <> nil) and (Sym.Kind = skTypeAlias)`.
Stage-3 now produces byte-identical IR to stage-2 (fixpoint reached).
1. ImplicitSelfField ARC: emit _ClassAddRef/_ClassRelease (or
_StringAddRef/_StringRelease) around storel when assigning to a
class- or string-typed implicit-Self field. Without this, the
TCodeGenQBE.FLines StringList was freed at Generate() exit, so
GetOutput returned garbage and stage-3 produced empty output.
2. UnescapeString: rewrite to handle both 'quoted' strings and #N
decimal char-code literals (the old version only handled
single-quoted forms, so #10 → empty string and GetText joined
all lines without newlines).
3. Chr builtin: add to symbol table, semantic pass, and codegen
(emits _Chr(w arg) → l result), required by the new UnescapeString.
4. AnalyseFieldAccess: set ResolvedMethod on parameterless constructor
TFieldAccessExpr nodes so the codegen can emit the Create body call
(companion to the EmitExpr IsConstructorCall fix from the previous
commit).
Add the full structure-return (sret) convention to the hand source so
stage-2 generates correct IR for functions/methods that return tyRecord:
- EmitMethodDef/EmitFuncDef: declare record-returning functions as void
with a hidden first param `l %_par__sret`; initialise %_var_Result
from it; emit bare `ret` at exit.
- IsRecordCall / EmitRecordCallSret: detect record-returning call
expressions (TFuncCallExpr, TMethodCallExpr, TFieldAccessExpr) and
emit them with the sret address as the first argument. Handles the
IsImplicitSelf path with correct Self+offset pointer arithmetic.
- EmitRecordCopy / EmitRecordReleaseFields: field-by-field ARC-correct
copy and release helpers for record types.
- EmitAssignment tyRecord branch: for record-typed lhs, either call
EmitRecordCallSret (when the rhs is a call) or EmitRecordCopy.
- ImplicitSelfField tyRecord branch in EmitAssignment: same logic for
assignments to record fields reached via implicit Self.
- TIdentExpr IsImplicitSelf tyRecord fix: return the field address
rather than loading through it (callers need the address for copies).
- TIdentExpr local/global record variable fix: return VarRef address
directly instead of an extra loadl.
All fixes mirror what the real multi-file source already had; this
brings the hand source to parity for record-return code generation.
- TSymbol gains ConstString field; AnalyseConstDecls stores CD.StrVal
- TIdentExpr gains ConstString and NoArgFuncDecl fields
- AnalyseIdent propagates ConstString to identifier exprs
- EmitExpr: string constants emit 'l copy $__sN' not 'w copy 0'
- IsNoArgFuncCall now set unconditionally for any skFunction with a
return type; NoArgFuncDecl is looked up from FProcIndex so codegen
can emit a proper call $FuncName() for user-defined zero-arg functions
- EmitExpr no-arg call path forwards NoArgFuncDecl as ResolvedDecl so
EmitFuncCall finds the declaration instead of falling through to
builtin dispatch and raising ECodeGenError
- TFieldAccessExpr IsMethodCall/PropRead/IsClassAccess paths use
VarRef(RecordName, IsGlobal) instead of hardcoded %%_var_ prefix,
fixing global-variable receivers such as Source.GetText
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.
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.
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.
The hand-written self-hosting TObjectList stored bare Pointers and
took no ARC ref when adding. In loops like
while Check(tkIdent) do
begin
CD := TConstDecl.Create;
...
ABlock.ConstDecls.Add(CD);
end;
reassigning CD on the next iteration released the prior CD (its only
strong ref) and the list's pointer dangled. When a later allocation
reused that heap slot, the list silently contained the wrong object,
and semantic analysis crashed in TSymbol.Create reading CD.Name from
an unrelated object's memory.
Add/Put/Delete/Clear/Destroy now AddRef on insert and Release on
remove or destroy, gated on FOwnsObjects so the semantics match the
real compiler's TObjectList. Extract continues to transfer the ref
to the caller without release.
Lets stage-2 compile the hand source far enough to hit the next bug
(currently a nil Self in TRecordTypeDesc.AddVTableSlot during
AnalyseTypeDecls) — more hand-source polish to follow.
RunProcess used poWaitOnExit combined with poUsePipes, which
deadlocks when the child fills the stderr pipe buffer before we
read — the child blocks waiting for the reader, we block waiting for
exit. Drain the pipe in a loop while the child runs, then wait on
exit separately. Also fold stderr into stdout so qbe/cc diagnostics
round-trip through a single handle.
rtl/Makefile: install blaise_rtl.a into compiler/target (not
compiler/target/bin) so it lives next to the blaise binary produced
by pasbuild.
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.
RTL Pascal sources (System.pas, Classes.pas, etc.) are compiled by
the Blaise compiler, not FPC. C sources are compiled separately via
GCC. Marking blaise-rtl as activeByDefault=false excludes it from
'pasbuild compile' and 'pasbuild test' reactor runs; it can still be
built explicitly with 'pasbuild compile -m blaise-rtl'.
Also adds a bootstrapExclude entry for System.pas so that if
blaise-rtl is compiled explicitly, the FPC bootstrap program does not
include System (FPC provides it implicitly).
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.
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.
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.
const: TConstDecl AST node; ParseConstBlock (integer and string literal values);
AnalyseConstDecls (registers each as skConstant in symbol table). Enables
CHR_* character constants and dupAccept/dupIgnore/dupError in self-hosting source.
ParseBlock: add tkConst to the multi-section loop.
Blaise now supports: case, enum, const, multi-type/var blocks, all file I/O
builtins — the complete language surface needed for the source migration.
834 tests pass.
_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.
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.
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.
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.
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.
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.
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.
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.
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.
Nil-safe addref/release that operates on the hidden 8-byte header
introduced by _ClassAlloc. _ClassRelease frees the block when the
refcount reaches zero. Destructor dispatch is deferred: Blaise has no
user-defined Destroy mechanism yet, and when one lands the vptr slot
will be invoked before free().
Helpers are not yet called by codegen; Task 3 inserts addref at class
assignment and release at scope exit.
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).