← board

What

TakeI(MkIntf(k)) leaked one object per call, frees=0. So did the by-value parameter and a method call straight on the result. Only storing the result in a variable was clean.

FPC's heaptrc says 0 unfreed memory blocks for the same program, so this is a divergence from the oracle, not a policy choice.

Two independent defects — neither fix works alone

Measured by building each on its own; each alone still reads frees=0.

1. The sret temp had no owner. An interface-returning call lands its result in a hidden caller-side temp. The argument path then RETAINS out of that temp into its own and releases that one at end of statement, so the call site reconciled and the factory's own reference did not.

2. A bare loop body never flushed. IRFlushPostCallIntf ran at AN_SEQ (per statement) and per if arm, and nowhere else. A loop is ONE statement, so a body that is not a BEGIN/END block never reached AN_SEQ: every by-value interface and managed-record argument temp the body created was finalized ONCE, after the loop, on the slot's LAST occupant. Every earlier occupant was overwritten with no release at all.

Defect 2 is the same mechanism bug-a-managed-temps-for-an-untaken-branch-are- still-init-and-finalized fixed for if arms, one statement kind over — and the comment at that site already spells out why an enclosing-statement boundary is the wrong one. Loops were not covered.

The control that separated them. With a printing destructor, for k := 1 to 3 do TakeI(MkIntf(k)) destroyed only N=3 while FPC destroyed all three; wrapping the IDENTICAL body in begin ... end made pxx destroy all three. Same temp, same call, different flush boundary.

Measured

arm before after allocs
TakeI(MkIntf(k)) const param 921, frees=0 1, frees=920 921
TakeIv(MkIntf(k)) value param 921, frees=0 1, frees=920 921
MkIntf(k).Id method on result 921, frees=0 1, frees=920 921
g := MkIntf(k)control 921/919 921/919 921
g := MkIntf(k); TakeI(g)control 921/919 921/919 921

test/test_interface_result_temp_leaks.pas: 2503 → 3 against a bound of 50, on a4c67a5e6cc8 vs the fixed binary, allocs 4274 either way. Rejected by the pre-fix binary (rc=1). Identical on x86-64/i386/aarch64/arm32/riscv32, and the pre-fix binary prints the same sink=1003000 on all five while leaking.

All three loop kinds are in the test because the flush had to be added to each separately, and a fix present in two of three is the shape that stays broken. The managed-record by-value arm is there because it rides the same queue: a wrong move for it is a double finalize rather than a leak, so the test also runs under -dPXX_HEAP_DEBUG, clean, and matches FPC's output.

pxx releases the temp at the end of the statement containing the call; FPC defers it to the next statement. Both destroy the same objects the same number of times, which is what the destructor-print control checks.

Log