One concept, four copies: which variant tags carry a refcounted object
The four
| site | form | tags it knows |
|---|---|---|
EmitVariantClear / EmitVariantRetain, compiler/ir_codegen.inc |
hand-emitted x86-64 | 7, 8, 9, 10 |
PXXVarClear / PXXVarRetain, compiler/builtin/builtinheap.pas |
portable Pascal | 7, 8, 9, 10 |
PyVarSlotIsObj, compiler/builtin/pylib.pas |
Pascal predicate | 7, 8, 9, 10 |
ClearVariantSlot, compiler/builtin/promocore.pas |
Pascal | none — see below |
They agree today only because [[bug-nilpy-bound-fn-closure-objects-are-never-freed]] just went through and made them agree. Nothing keeps them in step.
Why this is worth a ticket rather than a comment
The failure is silent and the only symptom is RSS. A tag added to the
emitters and missed in the portable twin does not crash, does not produce a
wrong value, and does not fail any test in the suite — the slot is simply never
released. That is exactly how the parent bug survived four investigation rounds:
VT_BOUNDFN_TAG was added to the emitters, the object was given a refcount, and
the leak persisted because PXXVarClear — the routine that prepares the
hidden-destination temp of a variant-returning call, once per loop iteration —
still tested 7, 8, 9. Two earlier sessions committed "fixed, verified" changes
that did not move the slope for want of the fourth copy.
The same file's own history has the prior instance: PXXVarClear's comment
records that the portable body "previously missed" the promo-tag range, "a
cross-target leak". Same routine, same class of omission, twice.
ClearVariantSlot is a live suspect, NOT a confirmed bug
promocore.pas's ClearVariantSlot releases a VT_STRING/promo payload and
otherwise just zeroes the slot — so an object payload passing through it is
dropped without a release. A fix was written during the parent ticket, measured,
found to change nothing on that repro (the routine is not on that path), and
reverted rather than shipped, because an unmeasured change to a shared
runtime clear path is how that ticket accumulated its wrong fixes.
It still looks wrong by inspection. It needs a repro that actually reaches it —
a promo-tagged variant slot that has held an object — before anything is
changed. Note promocore is a leaf unit and cannot see builtinheap, so the
release would have to arrive as a hook, the way PXXObjFinalizeHook already
does.
Shape of a fix
Make it one list. The tags are contiguous (7..10) and the emitters already
range-test the promo block, so the honest version is a single named range —
VT_OBJ_FIRST / VT_OBJ_LAST in defs.inc, mirrored once per builtin unit
(builtin units cannot see defs.inc, so a mirror is unavoidable — but ONE
mirror per unit, with the "must match defs.inc" note, beats four ad-hoc lists).
Then adding a tag is one edit plus a range bump, and a missed site becomes a
compile-time absence rather than a silent leak.
devdocs/dev/normalise-dont-special-case.md is the governing note: when a
construct is reachable through several shapes, normalise rather than growing a
second path, "because the second path is the one that stays broken."
Gate
Self-host fixedpoint byte-identical + tools/gate.sh quick; the closure and
object-reclamation RSS repros in the parent ticket stay flat; cross targets,
since the portable body is what the non-x86-64 backends use.