Skip to content

A generator's yielded value and its temporaries before a yield are given back - #620

Merged
ASDAlexander77 merged 2 commits into
mainfrom
generator-yield-leaks
Oct 19, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
generator-yield-leaks

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

Follow-up to #619. That PR left three cases leaking. This handles them:

Case Before (rc / own) After
Temporary before a statement that holds a yield (const n = ("b"+i).length; if (n > 0) { yield n; }), 500000 runs 165 MB / 165 MB 4.4 / 4.4 MB
Yielded value yield "a" + i 50.5 MB / 4.4 MB 4.4 / 4.4 MB
Yielded object yield new C(i) 50.5 MB / 4.4 MB 4.4 / 4.4 MB
own/own_generator.ts 11.2 MB (spec table) / 4.4 MB 4.4 / 4.4 MB
Temporary used after a yield cannot occur: MLIRGen keeps locals that cross a yield in the state object —

Changes (all in OwnedReturnConsumptionPass unless noted)

  • Release point: releaseRangeEnd now stops at the first op that is a yield or resume point, or that has one inside it. The release goes in front of that op.
  • rc, yielded record: retainYieldedRecords retains the {value, done} record before each ts.YieldReturnVal, the way a return statement does (§9.24). The record then owns its fields, and a fresh value made for it is an ordinary temporary released before the yield. Under own the value still moves into the record, and the store rule (escapesThroughStore) stays for every model except rc.
  • rc, classification: functionReturnsOwned classifies a generator's next like any function, with yields counted as returns.
  • rc, caller: calleeNameOf resolves next read out of a field of the state object that only ever holds it. The map is own_facts::FieldFunctions, moved out of OwnershipSignaturePass (own's behavior is unchanged), so the caller takes the record over.

Side effect worth a look

Under rc, a module with a generator (including any .map/.filter, which MLIRGen builds as generators) used to count as having an unclassified owning return. That kept the whole-module rule (closedWorld) off for it. Now it can be on, so calls through plain values in those modules take over their results, following the +1 convention. The full suite agrees.

Tests

  • 00generator_temporaries.ts gains items (yield new Item), next() called by hand and kept past the next call, and a declared C function, so the whole-module rule can't stand in for the field resolution.
  • generator-temporaries.cmake checks:
    • the record retain and both releases before each yield;
    • the releases before the if in branchYield;
    • that every next call in main whose record holds a heap value takes it over. This fails when the field resolution is turned off (checked).
    • that under own strings still keeps the concatenation and passes the verifier.
  • The check fails on the old compiler.
  • Full ctest -C Release -j 16: 4767/4767 pass.

Not done: no DefaultLib rc rebuild with this compiler. Nothing about how calls cross a library boundary changes: the caller's takeover needs the state object's constant in its own module, which an imported generator never has, so such a call leaks as before rather than being freed early.

Separate bug found: ("a" + i) + (yield 1) crashes the compiler in every model (proposed as its own task).

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 19, 2026 22:59
…ven back

Two leaks left after #619:

- A temporary made in front of a statement with a yield inside it (`const n =
  ("b" + i).length; if (n > 0) { yield n; }`) was never released, under rc and own:
  165 MB over 500000 runs, now 4.4 MB. The release now goes in front of the first
  yield, resume point or statement holding one, where the definition still dominates.

- Under rc a yielded value (`yield "s" + i`, `yield new C(i)`) kept the reference it
  was born with: 50 MB over 500000 runs, now 4.4 MB. A yield now retains its
  `{ value, done }` record the way a return does, a generator's `next` is classified
  as returning owned, and its caller takes the record over: `next` is resolved through
  the state object's field that only ever holds it (own_facts::FieldFunctions, moved
  out of OwnershipSignaturePass). own already moved the value into the record.

A temporary used after a yield cannot occur: MLIRGen keeps a generator's locals in
its state object.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the field, not the closed world

A declared C function in the file keeps a call through a plain value from being
taken as owned by the whole-module rule, so the main check fails when the field
resolution is turned off (checked). Comments and the own spec's found-not-fixed
list updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit f828f0e into main Oct 19, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the generator-yield-leaks branch October 19, 2026 22:26
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