Skip to content

fix: for-in snapshots iteration length at loop entry (#491)#516

Merged
InauguralPhysicist merged 1 commit into
mainfrom
fix/491-for-in-snapshot-length
Jul 9, 2026
Merged

fix: for-in snapshots iteration length at loop entry (#491)#516
InauguralPhysicist merged 1 commit into
mainfrom
fix/491-for-in-snapshot-length

Conversation

@InauguralPhysicist

Copy link
Copy Markdown
Collaborator

for x in xs re-read the sequence's live length on every ITER_NEXT, so mutating xs in the loop body was a footgun:

  • appending never terminated — the loop grew the list it was iterating (unbounded memory → OOM);
  • removing from the front skipped elements and could read past the end.

SPEC left mutation-during-iteration undefined.

Fix

make_iter_state records the length at loop entry as a third element of the iterator state. ITER_NEXT (interpreter) and jit_helper_iter_next (JIT) now bound the iteration by min(snapshot, live length):

  • appending can't extend the loop (capped at the snapshot);
  • removing stops at the live length instead of reading a freed slot.

for over a mutated sequence is now well-defined: it visits exactly the indices 0..N-1 that existed at entry, read live. Applies to both the interpreter and JIT tiers and to list comprehensions (same ITER_SETUP/ITER_NEXT path). SPEC updated.

xs is [1, 2, 3]
for x in xs:
    append of [xs, x * 10]   # was infinite; now runs 3 times
    print of x
# prints 1 2 3; xs ends as [1, 2, 3, 10, 20, 30]

Notes

  • One extra make_num/decref per loop setup (not per iteration) — negligible; the hot per-iteration path (NUM_REUSE index bump) is unchanged.
  • The JIT calls jit_helper_iter_next rather than inlining the count, so the single helper fix covers native code; verified identical output JIT-on vs EIGS_JIT_OFF.

Tests

New suite section [117]test_for_in_mutation.eigs, 9 checks (append-terminates, remove-is-OOB-safe, JIT-tier 500-iter snapshot, buffer, empty, listcomp, plain iteration).

  • Release suite: 2655/2655 (pre-rebase); rebased onto current main (with batch-2b): ASan + detect_leaks=1 2669/2669, leak tally 0.

Closes #491

🤖 Generated with Claude Code

`for x in xs` re-read the sequence's *live* length on every ITER_NEXT, so a
body that appended to xs looped forever (unbounded memory, OOM) and one that
removed from the front skipped elements past the shrink. SPEC left
mutation-during-iteration undefined.

make_iter_state now records the length at loop entry as a third element of
the iterator state; ITER_NEXT (interpreter) and jit_helper_iter_next (JIT)
bound the iteration by min(snapshot, live length). Appending can no longer
extend the loop; removing stops at the live length instead of reading a
freed slot. `for` over a mutated sequence is now well-defined: it visits
exactly the indices 0..N-1 that existed at entry, read live. Applies to both
tiers and to list comprehensions (same ITER_SETUP/ITER_NEXT path). SPEC
updated.

Regression: suite section [117] (test_for_in_mutation.eigs, 9 checks —
append-terminates, remove-is-OOB-safe, JIT-tier snapshot, buffer, empty,
listcomp). Release suite green (2655/2655).

Closes #491

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@InauguralPhysicist InauguralPhysicist merged commit 82c533b into main Jul 9, 2026
17 checks passed
@InauguralPhysicist InauguralPhysicist deleted the fix/491-for-in-snapshot-length branch July 9, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

for-in uses live list length: append during iteration never terminates

1 participant