fix(codegen): for-in over enumerators — promoted loop var and leaked enumerator
Two bugs in the enumerator-protocol for-in lowering, found via a user
program iterating a TList<Integer>:
- QBE: the mem2reg promotion scan did not treat the for-in loop
variable as a store target, so an otherwise-unaddressed scalar loop
var was promoted to an SSA temp while the lowering wrote the element
with storew %t, %_var_<name> — invalid IR ('invalid type for second
operand ... in storew'). The loop variable now always keeps a stack
slot.
- Both backends: GetEnumerator's result (an owned +1 constructor
return) was re-retained when stored into the synthetic enumerator
slot, while the function epilogue releases the slot exactly once —
leaking one enumerator per loop (rc=1 in every --debug leak report).
The owned reference is now transferred into the slot.
e2e leak-check tests run the same list-summing loop under --debug on
both backends and assert clean output with no leak report.
This commit is contained in:
parent
eabf20ae7f
commit
1dfc693fe5
|
|
@ -193,6 +193,11 @@ begin
|
|||
end
|
||||
else if AStmt is TForInStmt then
|
||||
begin
|
||||
{ The loop variable is written through its slot address by the
|
||||
for-in lowering (storew/storel into %_var_<name>) — it must stay
|
||||
a stack slot, never a promoted SSA temp. }
|
||||
if ASet.IndexOf(TForInStmt(AStmt).VarName) < 0 then
|
||||
ASet.Add(TForInStmt(AStmt).VarName);
|
||||
CollectAddressTakenExpr(TForInStmt(AStmt).CollExpr, ASet);
|
||||
CollectAddressTakenStmt(TForInStmt(AStmt).Body, ASet);
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7975,10 +7975,11 @@ begin
|
|||
else
|
||||
Self.Emit(#9'callq ' + Sym);
|
||||
|
||||
{ ARC-assign the enumerator into the synthetic slot }
|
||||
{ Move the enumerator into the synthetic slot. GetEnumerator's
|
||||
result is an owned +1 (constructor/call result) — transfer it;
|
||||
an extra AddRef here leaks one enumerator per loop because the
|
||||
function epilogue releases the slot exactly once. }
|
||||
Self.Emit(#9'pushq %rax');
|
||||
Self.Emit(#9'movq %rax, %rdi');
|
||||
Self.Emit(#9'callq _ClassAddRef');
|
||||
Self.Emit(Format(#9'movq %s, %%rdi', [EnumOp]));
|
||||
Self.Emit(#9'callq _ClassRelease');
|
||||
Self.Emit(#9'popq %rax');
|
||||
|
|
|
|||
|
|
@ -3247,10 +3247,12 @@ begin
|
|||
else
|
||||
EmitLine(Format(' %s =l call %s(l %s)', [EnumT, FuncName, SelfT]));
|
||||
|
||||
{ ARC-assign the enumerator into the synthetic slot }
|
||||
{ Move the enumerator into the synthetic slot. GetEnumerator's
|
||||
result is an owned +1 (constructor/call result) — transfer it; an
|
||||
extra AddRef leaks one enumerator per loop because the function
|
||||
epilogue releases the slot exactly once. }
|
||||
OldT := AllocTemp();
|
||||
EmitLine(Format(' %s =l loadl %s', [OldT, EnumSlot]));
|
||||
EmitLine(Format(' call $_ClassAddRef(l %s)', [EnumT]));
|
||||
EmitLine(Format(' call $_ClassRelease(l %s)', [OldT]));
|
||||
EmitLine(Format(' storel %s, %s', [EnumT, EnumSlot]));
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,11 @@ type
|
|||
procedure TestDebug_ClassFieldFromCall_NoLeak;
|
||||
{ Multiple same-named typed handlers share one slot — no over-release. }
|
||||
procedure TestDebug_MultiHandlerVar_NoOverRelease;
|
||||
{ for-in over a TList: GetEnumerator's +1 result must be transferred
|
||||
into the loop slot, not re-retained — one enumerator leaked per
|
||||
loop otherwise. }
|
||||
procedure TestDebug_ForInEnumerator_NoLeak;
|
||||
procedure TestDebug_ForInEnumerator_NoLeak_Native;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -231,6 +236,50 @@ const
|
|||
|
||||
{ ------------------------------------------------------------------ }
|
||||
|
||||
const
|
||||
SrcForInEnum = '''
|
||||
program P;
|
||||
uses generics.collections;
|
||||
var
|
||||
L: TList<Integer>;
|
||||
I, Total: Integer;
|
||||
begin
|
||||
L := TList<Integer>.Create;
|
||||
L.Add(5);
|
||||
L.Add(4);
|
||||
Total := 0;
|
||||
for I in L do
|
||||
Total := Total + I;
|
||||
WriteLn(Total);
|
||||
end.
|
||||
''';
|
||||
|
||||
procedure TE2ELeakCheckTests.TestDebug_ForInEnumerator_NoLeak;
|
||||
var
|
||||
Output: string;
|
||||
ExitCode: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
|
||||
AssertTrue('compile+run',
|
||||
CompileAndRunWithRTLDebug(SrcForInEnum, Output, ExitCode, True));
|
||||
AssertEquals('exit 0', 0, ExitCode);
|
||||
AssertEquals('stdout', '9' + LE, Output);
|
||||
AssertTrue('no leak report, got: ' + Output, Pos('leak', Output) < 0);
|
||||
end;
|
||||
|
||||
procedure TE2ELeakCheckTests.TestDebug_ForInEnumerator_NoLeak_Native;
|
||||
var
|
||||
Output: string;
|
||||
ExitCode: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit end;
|
||||
AssertTrue('compile+run',
|
||||
CompileAndRunWithRTLDebugOn(beNative, SrcForInEnum, Output, ExitCode, True));
|
||||
AssertEquals('exit 0', 0, ExitCode);
|
||||
AssertEquals('stdout', '9' + LE, Output);
|
||||
AssertTrue('no leak report, got: ' + Output, Pos('leak', Output) < 0);
|
||||
end;
|
||||
|
||||
procedure TE2ELeakCheckTests.TestDebug_NoLeak_NoReport;
|
||||
var
|
||||
Output: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue