Skip to content

A generator's loop local gives back what the last turn stored - #618

Merged
ASDAlexander77 merged 1 commit into
mainfrom
claude/quirky-jackson-00c6c6
Oct 19, 2026
Merged

ASDAlexander77 merged 1 commit into
mainfrom
claude/quirky-jackson-00c6c6

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

What

A local declared in a generator's loop is a field of the generator's state object. Its declaration stores into that field again on every turn, and nothing released what the previous turn had stored there. Under -mm=rc and -mm=own, every turn but the last leaked.

  • MLIRGenImpl.h registerVariable (allocateInContextThis branch): emit ts.ReleaseSlot(storage) after the retain/consume and before the store. The field starts out null, so the first turn releases nothing.
  • OwnershipInferencePass.cpp walkBorrowed: stop at a call marked __own_fresh_result. Without this, the release above made own reject a caller's for (const g of gen()):
    • Once next() may drop, the checker looked at the method read out of the generator's next field (its this is the generator).
    • The walk followed the {value, done} record that next() returns, so the record's final release counted as a use of that read, coming after the other it.next() call.
    • A call through a field that only ever holds one function gets __own_fresh_result, and its result is the caller's own block.
    • A broader rule (skip any call result that doesn't borrow) silenced own_err_field_function_reassigned and own_err_generator_place_overwritten, so the rule is limited to fresh results.

Measured (peak private bytes, AOT, 500000 runs)

program before after
const result: string[] = [label(i++), "x"] in while(true) (rc / own) leaks 0.6 MB / 0.6 MB
original repro with "a" + i++ (rc) 329 MB 151 MB

The remaining growth in the original repro comes from the number->string temporary of "a" + i++. OwnedReturnConsumptionPass skips whole generators (spec 18.5), so no temporary in a generator body is released. yield ("a" + i).length leaks the same way with no local at all. That is a separate change, being worked on separately.

Tests

  • own/own_generator_loop_local.ts runs under own (it's in the own_test list, so it also gets the no-counting check), and under rc, gc and none, both JIT and AOT. It covers:

    • a const in a for and in a while(true) with break
    • a caller for...of over a generator that yields fresh arrays
    • a record read after a second next()
    • a generator given up after one value

    Each local is read after a churn(), so a release of the wrong value reads garbage.

  • test-rc-generator-loop-local-release reads the IR: each generator's next has a ts.ReleaseSlot of its string[] field. It fails with the old compiler.

  • Full ctest -C Release -j 16: 4758 passed, 0 failed.

  • With the uncommitted own-param-markers work applied on top: -R "own|markers" passed 809/809, including own_generator_locals.

🤖 Generated with Claude Code

A local declared in a generator's loop is a field of the state object, and
its declaration stores into that field on every turn. Nothing released what
the previous turn stored there, so under rc and own every turn but the last
leaked (a const string[] in a while(true) grew by about 180 MB over 500000
runs). registerVariable now emits ts.ReleaseSlot on the field before the
store.

Under own that makes the generator's next() a call that may drop, and a
caller's for...of was rejected: walkBorrowed followed the {value, done}
record returned by it.next() as part of the method read out of `it`, so the
record's release counted as a use after the other next() call. A call marked
__own_fresh_result hands back a block of the caller's own, and the walk now
stops there.

Tests: own_generator_loop_local.ts under own, rc, gc and none (JIT and AOT),
and an IR check that each generator's next releases its loop local.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit 879f9df into main Oct 19, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the claude/quirky-jackson-00c6c6 branch October 19, 2026 21:37
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.

1 participant