Context
#211 (OSR observed-assign per-iteration leak) was fixed conservatively: the OSR scanner (jit_supported_prefix, entry_offset > 0) now stops at OBSERVE_ASSIGN / OBSERVE_ASSIGN_LOCAL, so a hot loop that reassigns an observed variable in its body falls back to the interpreter instead of OSR-compiling. Correct and leak-clean, but it forfeits JIT acceleration for exactly the canonical loop while improving: x is refine(x) shape.
Measured cost: ≈16% on an observed-var-reassigned-in-hot-OSR-loop microbench (n=5: ~2.70s OSR-active vs ~3.13s interpreter); 0% elsewhere (the bail only triggers on OBSERVE_ASSIGN). This is the optimization to recover.
Root cause (from #211 diagnosis)
The OSR loop-thunk's observe-assign path (jit_helper_observe_assign[_local], run from native code) orphaned a lifted stack slot when the observed variable's prior binding was a heap pointer (e.g. best is pop[i]). The interpreter later re-lifted and leaked it via the CALL arg path (vm_slot_lift). Data-dependent: pointer rebinds leaked, number rebinds were clean — through the same emitted code — so it's an sp/refcount interaction at the native boundary, not a plain emitter typo. From-zero function thunks (entry_offset == 0) are leak-clean.
Repro / bisection technique
- Minimal repro: an observed variable conditionally assigned a list element inside a loop, forced to OSR with
EIGS_JIT_OSR_THRESHOLD=1.
EIGS_JIT_OFF=1 → clean. High EIGS_JIT_OSR_THRESHOLD → clean. Both isolate the bug to OSR loop-thunks.
ASAN_OPTIONS="detect_leaks=1:malloc_context_size=40:fast_unwind_on_malloc=0" for deep traces.
Task
Find the exact sp/refcount imbalance in the OSR thunk's observe-assign + CALL handoff (likely needs live thunk disassembly: EIGS_JIT_DEBUG=1 EIGS_JIT_DUMP_NATIVE=1 + objdump -D -b binary -m i386:x86-64), fix it, and remove the entry_offset > 0 bail in jit_supported_prefix so observed-assign loops OSR-compile again.
Acceptance
- The
entry_offset > 0 bails for OBSERVE_ASSIGN / OBSERVE_ASSIGN_LOCAL are removed.
tests/test_optimize.eigs (genetic_optimize, 120 generations) stays leak-clean under ASan detect_leaks=1.
- The observed-assign microbench recovers toward the OSR-active timing.
- Full suite green release + ASan; harness leak tally unchanged (10).
Context
#211 (OSR observed-assign per-iteration leak) was fixed conservatively: the OSR scanner (
jit_supported_prefix,entry_offset > 0) now stops atOBSERVE_ASSIGN/OBSERVE_ASSIGN_LOCAL, so a hot loop that reassigns an observed variable in its body falls back to the interpreter instead of OSR-compiling. Correct and leak-clean, but it forfeits JIT acceleration for exactly the canonicalloop while improving: x is refine(x)shape.Measured cost: ≈16% on an observed-var-reassigned-in-hot-OSR-loop microbench (n=5: ~2.70s OSR-active vs ~3.13s interpreter); 0% elsewhere (the bail only triggers on
OBSERVE_ASSIGN). This is the optimization to recover.Root cause (from #211 diagnosis)
The OSR loop-thunk's observe-assign path (
jit_helper_observe_assign[_local], run from native code) orphaned a lifted stack slot when the observed variable's prior binding was a heap pointer (e.g.best is pop[i]). The interpreter later re-lifted and leaked it via the CALL arg path (vm_slot_lift). Data-dependent: pointer rebinds leaked, number rebinds were clean — through the same emitted code — so it's ansp/refcount interaction at the native boundary, not a plain emitter typo. From-zero function thunks (entry_offset == 0) are leak-clean.Repro / bisection technique
EIGS_JIT_OSR_THRESHOLD=1.EIGS_JIT_OFF=1→ clean. HighEIGS_JIT_OSR_THRESHOLD→ clean. Both isolate the bug to OSR loop-thunks.ASAN_OPTIONS="detect_leaks=1:malloc_context_size=40:fast_unwind_on_malloc=0"for deep traces.Task
Find the exact
sp/refcount imbalance in the OSR thunk's observe-assign + CALL handoff (likely needs live thunk disassembly:EIGS_JIT_DEBUG=1 EIGS_JIT_DUMP_NATIVE=1+objdump -D -b binary -m i386:x86-64), fix it, and remove theentry_offset > 0bail injit_supported_prefixso observed-assign loops OSR-compile again.Acceptance
entry_offset > 0bails forOBSERVE_ASSIGN/OBSERVE_ASSIGN_LOCALare removed.tests/test_optimize.eigs(genetic_optimize, 120 generations) stays leak-clean under ASandetect_leaks=1.