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:
parent
506ada7916
commit
a231c7d043
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -76,3 +76,4 @@ CLAUDE.md
|
|||
|
||||
# Local bug-tracking notes — not committed
|
||||
bugs.txt
|
||||
.gstack/
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]));
|
||||
|
|
|
|||
200
compiler/src/test/pascal/cp.test.e2e.leakcheck.pas
Normal file
200
compiler/src/test/pascal/cp.test.e2e.leakcheck.pas
Normal 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.
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue