feat(debug): add --debug leak reporter + Pointer/PtrUInt cast pairs

Runtime leak tracker (blaise_arc.pas):
- Open-addressing hash map (1024 buckets, 16 bytes each) tracks every
  live class instance; zero overhead when disabled.
- _LeakTrackerEnable() activates tracking and registers _LeakTrackerReport
  via atexit; called from $main when --debug is passed.
- _LeakTrackerRegister(UserPtr, ClassName) inserts into the map; called
  from codegen at every constructor site (both TFieldAccessExpr and
  TMethodCallExpr paths).
- _ClassRelease unregisters on refcount-zero free via LTDelete.
- Report written to stderr lists count + class name of each survivor.

Compiler (--debug flag):
- New --debug CLI flag sets FDebugMode on TCodeGenQBE; emits
  call $_LeakTrackerEnable() in EmitMainHeader after unit init calls,
  and call $_LeakTrackerRegister(...) after every _ClassAlloc site.
- --debug-opdf remains independent (OPDF only, no leak tracking).
- SetDebugMode(Bool) added to TCodeGenQBE public API for e2e tests.

Pointer/PtrUInt cast pairs (semantic + codegen):
- Pointer(intOrPtrExpr) validated in AnalyseFuncCallExpr; accepts
  integer, pointer, class, metaclass, procedural, and nil sources.
- PtrUInt(intOrPtrExpr) same validation; resolves to tyUInt64.
- Codegen: w-to-l widening uses extuw for Pointer/PChar targets
  (zero-extend, not sign-extend) to preserve address semantics.
- Four new unit tests in cp.test.pointers.pas.

E2E test infrastructure:
- CompileAndRunWithRTLDebug added to TE2ETestCase base for in-process
  debug-mode compilation without recursive overload ambiguity.
- cp.test.e2e.leakcheck.pas: 5 tests covering no-leak silence,
  single leak, multiple leaks, reference cycles, and debug-off.

2258 tests pass; fixpoint verified.
This commit is contained in:
Graeme Geldenhuys 2026-06-01 16:37:02 +01:00
parent 506ada7916
commit a231c7d043
9 changed files with 574 additions and 12 deletions

1
.gitignore vendored
View file

@ -76,3 +76,4 @@ CLAUDE.md
# Local bug-tracking notes — not committed
bugs.txt
.gstack/

View file

@ -44,6 +44,7 @@ begin
WriteLn(' --unit-path <dir> Add directory to unit search path (repeatable)');
WriteLn(' --target <id> linux-x86_64 (default), macos-arm64');
WriteLn(' --emit-ir Print QBE IR to stdout and exit');
WriteLn(' --debug Enable runtime memory leak reporting on exit');
WriteLn(' --debug-opdf Emit OPDF debug info (.opdf.s companion file)');
WriteLn('');
WriteLn('Configuration:');
@ -181,6 +182,7 @@ function ParseArgs(
out OutputFile: string;
out EmitIR: Boolean;
out OPDFEnabled: Boolean;
out DebugMode: Boolean;
out SearchPaths: TStringList): Boolean;
var
I: Integer;
@ -191,6 +193,7 @@ begin
OutputFile := '';
EmitIR := False;
OPDFEnabled := False;
DebugMode := False;
SearchPaths := TStringList.Create;
I := 1;
@ -214,6 +217,8 @@ begin
end
else if Arg = '--emit-ir' then
EmitIR := True
else if Arg = '--debug' then
DebugMode := True
else if Arg = '--debug-opdf' then
OPDFEnabled := True
else if Arg = '--target' then
@ -356,6 +361,7 @@ var
ConfigPaths: TStringList;
EmitIR: Boolean;
OPDFEnabled: Boolean;
DebugMode: Boolean;
OPDFAsmFile: string;
Source: TStringList;
Lexer: TLexer;
@ -373,6 +379,7 @@ var
begin
SearchPaths := nil;
OPDFEnabled := False;
DebugMode := False;
OPDFAsmFile := '';
if IsFPCStyleInvocation then
begin
@ -385,7 +392,7 @@ begin
end
else
begin
if not ParseArgs(SourceFile, OutputFile, EmitIR, OPDFEnabled, SearchPaths) then
if not ParseArgs(SourceFile, OutputFile, EmitIR, OPDFEnabled, DebugMode, SearchPaths) then
begin
PrintUsage;
Halt(1);
@ -474,6 +481,7 @@ begin
try
CG := TCodeGenQBE.Create;
try
CG.SetDebugMode(DebugMode);
if (Units <> nil) and (Units.Count > 0) then
begin
CG.SetSymbolTable(Prog.SymbolTable);

View file

@ -267,6 +267,7 @@ type
function MethodEmitName(AMDecl: TMethodDecl;
const ATypeName, AMethodName: string): string;
public
FDebugMode: Boolean;
constructor Create;
destructor Destroy; override;
procedure Generate(AProg: TProgram);
@ -279,6 +280,7 @@ type
vtable, and field-cleanup data. Must be called before AppendUnit when
the units contain class type definitions. Prog.SymbolTable is correct. }
procedure SetSymbolTable(ASymTable: TSymbolTable);
procedure SetDebugMode(AEnabled: Boolean);
procedure AppendUnit(AUnit: TUnit);
{ Append program IR to existing output (companion to AppendUnit).
Emits any remaining string literals and the $main function. }
@ -594,6 +596,8 @@ begin
{ Call initialization sections of imported units in order }
for I := 0 to FUnitInitNames.Count - 1 do
EmitLine(' call $' + FUnitInitNames.Strings[I] + '_init()');
if FDebugMode then
EmitLine(' call $_LeakTrackerEnable()');
end;
procedure TCodeGenQBE.EmitMainFooter;
@ -7427,12 +7431,12 @@ begin
EmitLine(Format(' %s =w copy %s', [T, ArgTemp]))
else
begin
{ Widening from w to l: zero-extend for unsigned targets, sign-
extend otherwise (QBE rejects 'l copy w'). }
{ Widening from w to l: zero-extend for pointer/unsigned targets,
sign-extend otherwise (QBE rejects 'l copy w'). }
if (TASTExpr(FC.Args.Items[0]).ResolvedType <> nil) and
(QbeTypeOf(TASTExpr(FC.Args.Items[0]).ResolvedType) = 'w') then
begin
if FC.ResolvedType.Kind = tyUInt64 then
if FC.ResolvedType.Kind in [tyUInt64, tyPointer, tyPChar] then
EmitLine(Format(' %s =l extuw %s', [T, ArgTemp]))
else
EmitLine(Format(' %s =l extsw %s', [T, ArgTemp]));
@ -7659,6 +7663,14 @@ begin
EmitLine(Format(' storel $vtable_%s, %s',
[QBEMangle(RT.Name), SelfTemp]));
EmitLine(Format(' call $_ClassAddRef(l %s)', [SelfTemp]));
if FDebugMode then
begin
L := AllocTemp;
EmitLine(Format(' %s =l add $typeinfo_%s, 16', [L, QBEMangle(RT.Name)]));
R := AllocTemp;
EmitLine(Format(' %s =l loadl %s', [R, L]));
EmitLine(Format(' call $_LeakTrackerRegister(l %s, l %s)', [SelfTemp, R]));
end;
{ If there's a user-defined Create method, call it }
if MCallExpr.ResolvedMethod <> nil then
begin
@ -8326,6 +8338,15 @@ begin
if TRecordTypeDesc(FldAccess.ResolvedType).HasVTable then
EmitLine(Format(' storel $vtable_%s, %s',
[QBEMangle(FldAccess.ResolvedType.Name), T]));
if FDebugMode then
begin
{ Load classname ptr from typeinfo[2] (offset +16) and register with leak tracker }
L := AllocTemp;
EmitLine(Format(' %s =l add $typeinfo_%s, 16', [L, QBEMangle(FldAccess.ResolvedType.Name)]));
R := AllocTemp;
EmitLine(Format(' %s =l loadl %s', [R, L]));
EmitLine(Format(' call $_LeakTrackerRegister(l %s, l %s)', [T, R]));
end;
{ Call user-defined Create body if one exists }
if FldAccess.ResolvedMethod <> nil then
begin
@ -9543,6 +9564,11 @@ begin
FSymTable := ASymTable;
end;
procedure TCodeGenQBE.SetDebugMode(AEnabled: Boolean);
begin
FDebugMode := AEnabled;
end;
procedure TCodeGenQBE.AppendUnit(AUnit: TUnit);
var
I, J, K, S: Integer;

View file

@ -5513,6 +5513,46 @@ begin
Exit;
end;
{ Pointer(intOrPtrExpr) — reinterpret any integer or pointer value as
an untyped Pointer. Integer sources are treated as raw addresses (no
sign-extension guarantee beyond what the source type provides). }
if SameText(AExpr.Name, 'Pointer') then
begin
if AExpr.Args.Count <> 1 then
SemanticError('Pointer cast requires exactly one argument', AExpr.Line, AExpr.Col);
ArgType := AnalyseExpr(TASTExpr(AExpr.Args.Items[0]));
if not (ArgType.Kind in [tyInteger, tyInt64, tyUInt64, tyUInt32, tyByte,
tySmallInt, tyWord, tyPointer, tyPChar, tyClass,
tyMetaClass, tyProcedural, tyNil]) then
SemanticError(
Format('Pointer cast requires an integer, pointer, or class expression, got ''%s''',
[ArgType.Name]),
AExpr.Line, AExpr.Col);
Result := FTable.TypePointer;
AExpr.ResolvedType := Result;
Exit;
end;
{ PtrUInt(intOrPtrExpr) — reinterpret any integer or pointer value as
a pointer-sized unsigned integer (UInt64 on 64-bit targets). The
primary use is arithmetic on pointer values without signed overflow. }
if SameText(AExpr.Name, 'PtrUInt') then
begin
if AExpr.Args.Count <> 1 then
SemanticError('PtrUInt cast requires exactly one argument', AExpr.Line, AExpr.Col);
ArgType := AnalyseExpr(TASTExpr(AExpr.Args.Items[0]));
if not (ArgType.Kind in [tyInteger, tyInt64, tyUInt64, tyUInt32, tyByte,
tySmallInt, tyWord, tyPointer, tyPChar, tyClass,
tyNil]) then
SemanticError(
Format('PtrUInt cast requires an integer, pointer, or class expression, got ''%s''',
[ArgType.Name]),
AExpr.Line, AExpr.Col);
Result := FTable.TypeUInt64;
AExpr.ResolvedType := Result;
Exit;
end;
if SameText(AExpr.Name, 'string') then
begin
if AExpr.Args.Count <> 1 then

View file

@ -71,6 +71,7 @@ uses
cp.test.e2e.base,
cp.test.e2e.exceptions,
cp.test.e2e.arc,
cp.test.e2e.leakcheck,
cp.test.e2e.stringops,
cp.test.e2e.collections2,
cp.test.e2e.sysutils,

View file

@ -48,7 +48,15 @@ type
function RunUnderValgrind(const ASrc: string; out ALog: string): Boolean;
function CompileAndRunWithRTL(const ASrc: string;
out AStdout: string;
out AExitCode: Integer): Boolean;
out AExitCode: Integer): Boolean; overload;
function CompileAndRunWithRTL(const ASrc: string;
out AStdout: string;
out AExitCode: Integer;
ADebugMode: Boolean): Boolean; overload;
function CompileAndRunWithRTLDebug(const ASrc: string;
out AStdout: string;
out AExitCode: Integer;
ADebugMode: Boolean): Boolean;
end;
implementation
@ -301,6 +309,22 @@ end;
function TE2ETestCase.CompileAndRunWithRTL(const ASrc: string;
out AStdout: string;
out AExitCode: Integer): Boolean;
begin
Result := CompileAndRunWithRTLDebug(ASrc, AStdout, AExitCode, False);
end;
function TE2ETestCase.CompileAndRunWithRTL(const ASrc: string;
out AStdout: string;
out AExitCode: Integer;
ADebugMode: Boolean): Boolean;
begin
Result := CompileAndRunWithRTLDebug(ASrc, AStdout, AExitCode, ADebugMode);
end;
function TE2ETestCase.CompileAndRunWithRTLDebug(const ASrc: string;
out AStdout: string;
out AExitCode: Integer;
ADebugMode: Boolean): Boolean;
var
Lexer: TLexer;
Parser: TParser;
@ -340,6 +364,7 @@ begin
Semantic.AnalyseUnitForExport(TUnit(Units.Items[I]));
Semantic.Analyse(Prog);
CG := TCodeGenQBE.Create;
CG.SetDebugMode(ADebugMode);
CG.SetSymbolTable(Prog.SymbolTable);
for I := 0 to Units.Count - 1 do
CG.AppendUnit(TUnit(Units.Items[I]));

View file

@ -0,0 +1,200 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
unit cp.test.e2e.leakcheck;
{ E2E tests for the --debug leak tracker.
Verifies that _LeakTrackerEnable is activated in debug builds,
that leaked objects are reported on exit, and that cleanly-released
objects produce no report. }
interface
uses
blaise.testing, cp.test.e2e.base;
type
[Threaded]
TE2ELeakCheckTests = class(TE2ETestCase)
protected
procedure SetUp; override;
published
procedure TestDebug_NoLeak_NoReport;
procedure TestDebug_LeakedObject_ReportedOnExit;
procedure TestDebug_MultipleLeaks_AllReported;
procedure TestDebug_CycleRetained_Reported;
procedure TestRelease_NoReport_WhenDebugOff;
end;
implementation
procedure TE2ELeakCheckTests.SetUp;
begin
inherited SetUp;
SetUpScratch('compiler/target/test-e2e-leakcheck');
end;
const
LE = #10;
{ A clean program: object is created and properly released by ARC at scope exit. }
SrcNoLeak = '''
program P;
uses blaise_arc;
type
TBox = class
Value: Integer;
end;
var
B: TBox;
begin
B := TBox.Create;
B.Value := 99;
WriteLn(B.Value)
end.
''';
{ Deliberately leaked: object assigned to a raw Pointer, bypassing ARC release. }
SrcOneLeak = '''
program P;
uses blaise_arc;
type
TBox = class
Value: Integer;
end;
var
B: TBox;
P: Pointer;
begin
B := TBox.Create;
B.Value := 7;
P := Pointer(B);
B := nil;
{ P holds the only reference but is an unmanaged Pointer — ARC never
releases it, so TBox leaks. }
WriteLn('done')
end.
''';
{ Two distinct classes leaked to verify count and class-name reporting. }
SrcTwoLeaks = '''
program P;
uses blaise_arc;
type
TAlpha = class
X: Integer;
end;
TBeta = class
Y: Integer;
end;
var
A: TAlpha;
B: TBeta;
PA, PB: Pointer;
begin
A := TAlpha.Create;
B := TBeta.Create;
PA := Pointer(A);
PB := Pointer(B);
A := nil;
B := nil;
WriteLn('done')
end.
''';
{ Reference cycle: each object holds the other — both leak. }
SrcCycleLeak = '''
program P;
uses blaise_arc;
type
TNode = class
Other: TNode;
end;
var
X, Y: TNode;
begin
X := TNode.Create;
Y := TNode.Create;
X.Other := Y;
Y.Other := X;
{ X and Y each hold refcount >= 2 due to the cycle; when the local
vars go out of scope the refcount drops to 1, not 0 — both leak. }
WriteLn('done')
end.
''';
{ ------------------------------------------------------------------ }
procedure TE2ELeakCheckTests.TestDebug_NoLeak_NoReport;
var
Output: string;
ExitCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTLDebug(SrcNoLeak, Output, ExitCode, True));
AssertEquals('exit 0', 0, ExitCode);
AssertEquals('stdout', '99' + LE, Output);
AssertTrue('no leak report', Pos('leak', Output) < 0);
end;
procedure TE2ELeakCheckTests.TestDebug_LeakedObject_ReportedOnExit;
var
Output: string;
ExitCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTLDebug(SrcOneLeak, Output, ExitCode, True));
AssertEquals('exit 0', 0, ExitCode);
AssertTrue('leak header present', Pos('Blaise leak report', Output) >= 0);
AssertTrue('count 1', Pos('1 object(s)', Output) >= 0);
AssertTrue('class name', Pos('TBox', Output) >= 0);
end;
procedure TE2ELeakCheckTests.TestDebug_MultipleLeaks_AllReported;
var
Output: string;
ExitCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTLDebug(SrcTwoLeaks, Output, ExitCode, True));
AssertEquals('exit 0', 0, ExitCode);
AssertTrue('leak header present', Pos('Blaise leak report', Output) >= 0);
AssertTrue('count 2', Pos('2 object(s)', Output) >= 0);
AssertTrue('TAlpha reported', Pos('TAlpha', Output) >= 0);
AssertTrue('TBeta reported', Pos('TBeta', Output) >= 0);
end;
procedure TE2ELeakCheckTests.TestDebug_CycleRetained_Reported;
var
Output: string;
ExitCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit end;
AssertTrue('compile+run', CompileAndRunWithRTLDebug(SrcCycleLeak, Output, ExitCode, True));
AssertEquals('exit 0', 0, ExitCode);
AssertTrue('leak header present', Pos('Blaise leak report', Output) >= 0);
AssertTrue('count 2', Pos('2 object(s)', Output) >= 0);
AssertTrue('TNode reported', Pos('TNode', Output) >= 0);
end;
procedure TE2ELeakCheckTests.TestRelease_NoReport_WhenDebugOff;
var
Output: string;
ExitCode: Integer;
begin
if not ToolchainAvailable then begin Ignore('toolchain unavailable'); Exit end;
{ Same leaking source but compiled without debug mode — no report expected. }
AssertTrue('compile+run', CompileAndRunWithRTLDebug(SrcOneLeak, Output, ExitCode, False));
AssertEquals('exit 0', 0, ExitCode);
AssertTrue('no leak report', Pos('Blaise leak report', Output) < 0);
end;
initialization
RegisterTest(TE2ELeakCheckTests);
end.

View file

@ -50,6 +50,14 @@ type
procedure TestCodegen_Deref_EmitsLoad;
procedure TestCodegen_PointerWrite_EmitsStore;
procedure TestCodegen_PointerArith_EmitsAdd;
{ ------------------------------------------------------------------ }
{ Pointer(intExpr) and PtrUInt(ptrExpr) cast pairs }
{ ------------------------------------------------------------------ }
procedure TestSemantic_Pointer_FromInt_ReturnsPointerType;
procedure TestSemantic_PtrUInt_FromPointer_ReturnsUInt64Type;
procedure TestCodegen_Pointer_FromInt_EmitsExtuw;
procedure TestCodegen_PtrUInt_FromPointer_EmitsCopy;
end;
implementation
@ -375,6 +383,69 @@ begin
AssertTrue('Pointer arithmetic should emit add', Pos('add', IR) > 0);
end;
const
SrcPointerFromInt =
'program P;' +
'var N: Integer; P: Pointer;' +
'begin N := 42; P := Pointer(N) end.';
SrcPtrUIntFromPtr =
'program P;' +
'var P: Pointer; U: UInt64;' +
'begin P := nil; U := PtrUInt(P) end.';
procedure TPointerTests.TestSemantic_Pointer_FromInt_ReturnsPointerType;
var
Prog: TProgram;
Assign: TAssignment;
Cast: TFuncCallExpr;
begin
Prog := AnalyseSrc(SrcPointerFromInt);
try
Assign := TAssignment(Prog.Block.Stmts.Items[1]);
Cast := TFuncCallExpr(Assign.Expr);
AssertEquals('cast resolves to tyPointer',
Ord(tyPointer), Ord(Cast.ResolvedType.Kind));
finally
Prog.Free;
end;
end;
procedure TPointerTests.TestSemantic_PtrUInt_FromPointer_ReturnsUInt64Type;
var
Prog: TProgram;
Assign: TAssignment;
Cast: TFuncCallExpr;
begin
Prog := AnalyseSrc(SrcPtrUIntFromPtr);
try
Assign := TAssignment(Prog.Block.Stmts.Items[1]);
Cast := TFuncCallExpr(Assign.Expr);
AssertEquals('cast resolves to tyUInt64',
Ord(tyUInt64), Ord(Cast.ResolvedType.Kind));
finally
Prog.Free;
end;
end;
procedure TPointerTests.TestCodegen_Pointer_FromInt_EmitsExtuw;
var
IR: string;
begin
IR := GenIR(SrcPointerFromInt);
AssertTrue('Pointer(Integer) should zero-extend via extuw',
Pos('extuw', IR) >= 0);
end;
procedure TPointerTests.TestCodegen_PtrUInt_FromPointer_EmitsCopy;
var
IR: string;
begin
IR := GenIR(SrcPtrUIntFromPtr);
AssertTrue('PtrUInt(Pointer) should emit copy (l→l)',
Pos('copy', IR) >= 0);
end;
initialization
RegisterTest(TPointerTests);

View file

@ -50,6 +50,9 @@ procedure _ClassFree(UserPtr: Pointer);
function _HasClassAttribute(AClassTI, AAttrTI: Pointer): Boolean;
procedure _StringReleaseCheck(DataPtr: Pointer; RefCount, Length, Capacity: Integer);
procedure _AbstractMethodError;
procedure _LeakTrackerEnable;
procedure _LeakTrackerRegister(UserPtr: Pointer; ClassName: Pointer);
procedure _libc_atexit(Fn: Pointer); external name 'atexit';
implementation
@ -113,6 +116,191 @@ begin
DiagAbort('Runtime error: abstract method called.', 39);
end;
{ ------------------------------------------------------------------ }
{ Leak tracker — separate hash map, zero overhead when disabled }
{ ------------------------------------------------------------------ }
{
Open-addressing hash map: key = UserPtr (Pointer), value = className
string data pointer (immortal — points into the binary's typeinfo).
Enabled only when _LeakTrackerEnable is called (i.e. --debug builds).
_ClassAlloc registers every live object; _ClassRelease unregisters it.
On exit, _LeakTrackerReport walks all buckets and prints survivors.
Bucket layout (each bucket is two pointer-sized slots):
slot[0] key (UserPtr; nil = empty; $DEADBEEF = tombstone)
slot[1] value (class-name string data ptr)
Table is allocated via _BlaiseGetMem and never freed (debug mode only).
}
const
LT_BUCKETS = 1024; { must be power of two }
LT_BUCKET_SZ = 16; { 2 × 8-byte pointers }
var
GLTEnabled: Boolean;
GLTTable: PChar; { raw bucket array, LT_BUCKETS * LT_BUCKET_SZ bytes }
GLTCount: Integer;
GLTTombstone: Pointer; { sentinel for deleted slots, set to Pointer(1) at init }
function LTHash(P: Pointer): Integer;
var
V: PtrUInt;
begin
V := PtrUInt(P);
V := V xor (V shr 13);
V := V xor (V shr 7);
Result := Integer(V and (LT_BUCKETS - 1));
end;
procedure LTInsert(Key, Value: Pointer);
var
Idx, Step: Integer;
Slot: ^Pointer;
begin
Idx := LTHash(Key);
Step := 0;
while Step < LT_BUCKETS do
begin
Slot := Pointer(GLTTable + Idx * LT_BUCKET_SZ);
if (Slot^ = nil) or (Slot^ = GLTTombstone) then
begin
Slot^ := Key;
Slot := Pointer(Pointer(Slot) + 8);
Slot^ := Value;
Inc(GLTCount);
Exit;
end;
Idx := (Idx + 1) and (LT_BUCKETS - 1);
Inc(Step);
end;
end;
procedure LTDelete(Key: Pointer);
var
Idx, Step: Integer;
Slot: ^Pointer;
begin
Idx := LTHash(Key);
Step := 0;
while Step < LT_BUCKETS do
begin
Slot := Pointer(GLTTable + Idx * LT_BUCKET_SZ);
if Slot^ = nil then Exit;
if Slot^ = Key then
begin
Slot^ := GLTTombstone;
Dec(GLTCount);
Exit;
end;
Idx := (Idx + 1) and (LT_BUCKETS - 1);
Inc(Step);
end;
end;
procedure WriteStr(Msg: Pointer; Len: Integer);
begin
_libc_write(STDERR_FD, Msg, Int64(Len));
end;
procedure WriteNL;
var
NL: Byte;
begin
NL := 10;
_libc_write(STDERR_FD, @NL, 1);
end;
procedure WriteInt(V: Integer);
var
Buf: array[0..19] of Byte;
Pos: Integer;
N: Integer;
begin
if V = 0 then
begin
Buf[0] := Ord('0');
_libc_write(STDERR_FD, @Buf[0], 1);
Exit;
end;
Pos := 19;
N := V;
while N > 0 do
begin
Buf[Pos] := Ord('0') + Byte(N mod 10);
N := N div 10;
Dec(Pos);
end;
_libc_write(STDERR_FD, @Buf[Pos + 1], Int64(19 - Pos));
end;
procedure _LeakTrackerReport;
var
I: Integer;
Slot: ^Pointer;
NamePtr: ^Pointer;
ClassName: Pointer;
LenSlot: ^Integer;
NameLen: Integer;
begin
if not GLTEnabled then Exit;
if GLTCount = 0 then Exit;
WriteStr('Blaise leak report:', 19);
WriteNL;
WriteStr(' ', 2);
WriteInt(GLTCount);
WriteStr(' object(s) not released:', 24);
WriteNL;
for I := 0 to LT_BUCKETS - 1 do
begin
Slot := Pointer(GLTTable + I * LT_BUCKET_SZ);
if (Slot^ <> nil) and (Slot^ <> GLTTombstone) then
begin
NamePtr := Pointer(Pointer(Slot) + 8);
ClassName := NamePtr^;
WriteStr(' - ', 4);
if ClassName <> nil then
begin
LenSlot := ClassName - 8; { string header: Length at data_ptr-8 }
NameLen := LenSlot^;
if (NameLen > 0) and (NameLen < 256) then
WriteStr(ClassName, NameLen)
else
WriteStr('(unknown)', 9);
end
else
WriteStr('(unknown)', 9);
WriteNL;
end;
end;
end;
procedure _LeakTrackerRegister(UserPtr: Pointer; ClassName: Pointer);
begin
if not GLTEnabled then Exit;
if UserPtr = nil then Exit;
LTInsert(UserPtr, ClassName);
end;
procedure _LeakTrackerEnable;
var
TableSize: Integer;
Sentinel: PtrUInt;
begin
if GLTEnabled then Exit;
GLTEnabled := True;
GLTCount := 0;
{ Use address 1 as tombstone — never a valid heap pointer (heap starts
well above page 0, and address 1 is in the unmapped zero page). }
Sentinel := 1;
GLTTombstone := Pointer(Sentinel);
TableSize := LT_BUCKETS * LT_BUCKET_SZ;
GLTTable := PChar(_BlaiseGetMem(TableSize));
if GLTTable = nil then begin GLTEnabled := False; Exit end;
_libc_memset(GLTTable, 0, Int64(TableSize));
_libc_atexit(Pointer(@_LeakTrackerReport));
end;
{ ------------------------------------------------------------------ }
{ Class ARC }
{ ------------------------------------------------------------------ }
@ -145,6 +333,8 @@ begin
RC^ := RC^ - 1;
if RC^ = 0 then
begin
if GLTEnabled then
LTDelete(UserPtr);
_WeakZeroSlots(UserPtr);
CleanupSlot := PPointer(Base + 8);
if CleanupSlot^ <> nil then
@ -395,13 +585,13 @@ end;
typeinfo[ 6] +48 -> $vtable_<T> }
function _ClassCreate(TInfo: Pointer): Pointer;
var
SizeSlot: ^Int64;
PtrSlot: ^Pointer;
Size: Int64;
Cleanup: Pointer;
VTable: Pointer;
UserPtr: Pointer;
VTableSlot: ^Pointer;
SizeSlot: ^Int64;
PtrSlot: ^Pointer;
Size: Int64;
Cleanup: Pointer;
VTable: Pointer;
UserPtr: Pointer;
VTableSlot: ^Pointer;
begin
Result := nil;
if TInfo = nil then Exit;