fix(semantic): break analyser<->symbol-table cycle that leaked the AST

Compiling even a tiny program leaked ~339 heap objects (TSymbol ×129,
TObjectList ×156, TTypeDesc ×14, plus the TProgram, TSymbolTable and
TSemanticAnalyser singletons), despite the driver correctly calling
Prog.Free() and Semantic.Free().

Root cause was a strong reference cycle: TSemanticAnalyser owns its
TSymbolTable (FTable), and the table held a plain strong reference back to
the analyser via FUsesChainProvider (set in Analyse so Lookup can consult
per-unit visibility).  Under refcount ARC the two keep each other alive:
Semantic.Free() drops the analyser to rc=1 (the table still references it),
the analyser destructor never runs, FTable.Free() is never called, and the
entire reachable graph — every scope, symbol, builtin type descriptor, and
the whole AST — leaks as one unit.

Marking FUsesChainProvider [Unretained] breaks the cycle.  The provider is
the analyser that owns the table and always outlives it (its destructor is
what frees the table), so a non-owning reference cannot dangle.

Impact: --debug leak count on a tiny compile drops from 339 to 14 (~96%
reclaimed).  The remaining 14 are a separate, smaller global-scope fragment.
Full suite OK (2671 tests), self-compile clean, FIXPOINT_OK.

This adjusts TSymbolTable's field layout (one ARC slot becomes non-managed),
so it is a layout-affecting change validated via stage-2 fixpoint.
This commit is contained in:
Graeme Geldenhuys 2026-06-08 18:05:07 +01:00
parent 935bd5237d
commit 53ea92da00

View file

@ -432,8 +432,16 @@ type
{ Optional hook into uses-chain lookup. Set by TSemanticAnalyser
so Lookup() can consult per-unit visibility after exhausting the
pushed scopes but BEFORE falling back to the global scope.
Task #44 step 7. }
FUsesChainProvider: TUsesChainProvider;
Task #44 step 7.
[Unretained]: the provider is the TSemanticAnalyser that OWNS this
symbol table (TSemanticAnalyser.FTable). A strong reference here
would form an analyser<->table cycle that refcount ARC cannot
collect, so Semantic.Free() would never run the analyser destructor
and the whole AST + symbol-table graph (~325 objects) would leak.
The analyser always outlives the table — its destructor is what
frees the table — so a non-owning reference is safe. }
[Unretained] FUsesChainProvider: TUsesChainProvider;
{ Recursion guard. TSemanticAnalyser.LookupViaUsesChain (which
sits behind FUsesChainLookup) calls FTable.Lookup itself to