pyeval boxes a FRESHLY BUILT container into a variant and then retains it
Nine sites in compiler/builtin/pyeval.pas build an object, write its handle
raw into a variant slot (VType := 7; Payload := ...) and then call
PXXObjRetain on it. Every one carries the same comment -- "slot owns +1
(magic-guarded)" -- and the comment is right about the SLOT and wrong about
where the +1 comes from: the object was just constructed and already carries
rc=1, so the retain makes it rc=2 against the single release the slot's clear
performs. Net +1 per evaluation, forever.
This is the SAME SHAPE as five defects already fixed on 2026-09-14
(17462f54e -- the four eager pair builders and TPyDict.itemlist; 17e5731a7
-- the cursor's own FBox; bug-n-a-user-operator-on-a-variant-operand-leaks-its-result
-- PyUserArithCall1). The discriminator in every case is one question:
is the object the retain is applied to BORROWED from somewhere that keeps its own reference, or was it CONSTRUCTED on the line above?
A borrowed one needs the retain. A constructed one does not, and the retain is the leak.
THE SITES, NOT YET INDIVIDUALLY MEASURED
| file:line | object | constructed on the spot? |
|---|---|---|
| pyeval.pas:3477 | li list literal |
yes -- TPyList.Create above |
| pyeval.pas:3489 | dd dict literal |
yes -- TPyDict.Create |
| pyeval.pas:3508 | dd dict literal |
yes |
| pyeval.pas:3522 | li set literal |
yes |
| pyeval.pas:3565 | li tuple literal |
yes |
| pyeval.pas:3949 | r range result |
yes |
| pyeval.pas:4347 | gres |
probably |
| pyeval.pas:4387 | by from pyint_to_bytes |
fresh call result |
| pyeval.pas:4442 | b2 from pystr_encode |
fresh call result |
| pyeval.pas:600 / 1243 / 1399 / 1482 | host-call results / field reads | MIXED -- 1482 is documented as a field READ, which genuinely borrows |
pylib.pas:14205 (cur := pyiter_v(...)) belongs to the same question and is
not settled either.
Do not bulk-remove these. pylib.pas:5476, 18075 and PyObjAsVar
(20608) look identical and are CORRECT -- their object is the caller's, and
removing the retain there hands back a net release of a live object. That was
measured once already and the note above PyObjAsVar records the SIGSEGV.
Each site needs its own answer to the question above.
WHY THIS IS NOT PRIO 85
pyeval is the interpreted eval()/exec() path. It is not on the frame path
of any demo we currently run, and the leaks that were 95% of lekkerzeilen's
histogram are elsewhere and fixed. Real, mechanical, and worth a session --
just not ahead of anything a running program touches.
HOW TO MEASURE ONE
-dPXX_OBJTRACE, a loop of N evaluations, count allocations with no matching
F. Assert the trace is NON-EMPTY before counting -- an empty trace makes
"unmatched allocations" report zero, which reads exactly like a clean result.
That cost a wrong conclusion on 2026-09-14 (a pinned-compiler control invoked
without the flag reported the fix had CAUSED the leak).