Repository navigation
rc: a folded const's reference is not taken over by every receiver that meets it - #400
Merged
Merged
Conversation
`const a = new C()` has no storage: the name is the `new`'s own result, marked __owned_result, so every later mention offered that one reference to its receiver, and each took it. An inner `let` gave it back while the name still read it; two `let`s, or a push then a `let`, gave one count two holders; a push or a field store in a loop took it on every iteration. `let b = h.c = new C()` took one `new` twice. Under rc these double-freed or read freed memory (00owned_named_consts.ts: eight cases, each failing alone under -mm=rc before this change). - MLIROwnedReference.h: carriesUnclaimedReference (owned, not yet consumed) and mayTakeOverReference (also not a folded const), used by every receiver. - MLIRGen marks a folded const's producer __owned_result_named. Storing receivers retain it; a return or a delete still takes it over. - OwnedReturnConsumptionPass keeps a named call result's receiver retain unless it is the one a return took. - -mm=own: a taker plus the temporary's later release in the same block is a move when rc gave the taker its own reference, so `const a = new C(); let b = a;` stays accepted. NewInterface, which rc gives no reference, is still a second owner. Docs: reference-counting-evaluation.md 9.79; own spec 11.1 and 11.4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… first The first return marked the producer consumed, so a second return on another path (`if (x) return c; return c;`) retained instead and handed its caller two references: 1M calls grew from 4.7 MB to 20.1 MB under -mm=rc. A return or delete may take a named value over even when it is marked consumed, since no storing receiver takes a named value and returns on different paths exclude each other. OwnedReturnConsumptionPass likewise takes over every retain that feeds a return of a named call result, where it took only the first (that half predates this). Records as open: a named const still leaks on the path that does not return. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at its block end
Section 9.30 released an unclaimed reference only when every use sat in the producer's
own block, and a statement body is a region of an op in that block. With named consts
now retained by their receivers, `const a = new C(); if (x) { let b = a; }` never gave
`a`'s own reference back: 1M calls reached 89.2 MB at -O1 under rc (none 89.2; -O3
hid it by removing the allocation). A use nested in an if, loop, switch, label or try
body now counts as in the block, since those bodies finish before the next op runs;
a nested function or an async.execute body still does not. After: 4.7 MB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the rc over-releases that the
-mm=ownphase-0 review found (#399). The same programs double-free or read freed memory under-mm=rctoday.The bug
const a = new C()gets no storage: the name is thenew's result, marked__owned_result. Every receiver that met a later mention ofatook that one reference over:{ let b = a; } … a.vletreleases it at scope exit;areads freed memoryfor (…) { let b = a; }let b = a; let c = a;arr.push(a); let d = a;for (…) arr.push(a)for (…) h.s = s(fresh string)const r = make(); { let b = r; }let b = h.c = new C()newtaken by the field and the localtest/tester/tests/00owned_named_consts.tshas all eight cases. Before this change each one failed on its own under-mm=rc, in JIT and AOT.The fix
MLIROwnedReference.hgives every receiver the same question. A storing receiver may take a reference over only if the producer carries one, it hasn't been taken yet, and it isn't a foldedconst.MLIRGen marks a folded
const's producer__owned_result_named. Storing receivers retain it, and §9.30 gives the name's reference back at the end of its block.A
returnordeletestill takes it over, on every path (if (x) return c; return c;).OwnedReturnConsumptionPass takes over every return retain of a named call result, not just the first.
-mm=own: one taker plus the temporary's later release in the same block count as a move when rc gave the taker a reference of its own. Soconst a = new C(); let b = a;stays accepted.NewInterfacegets no reference from rc, so it is still rejected as a second owner.A leak the change exposed is fixed too. §9.30 released a temporary only if every use was in its own block, but an
if/loop body is a nested region.const a = new C(); if (x) { let b = a; }therefore never releaseda: 1M calls reached 89.2 MB at-O1under rc, and 4.7 MB after the fix. Uses insideif/loop/switch/label/trybodies now count as in-block. Nested functions andasync.executebodies still don't.Cost: a retain/release pair for
const a = new C(); arr.push(a), which used to be free.Verification
~AsyncTokendebug-heap assert (fails under-mm=nonetoo), and the linknameDictionaryAttrassert, which now lands in verifier shard 4.const p = make(); let q = p;: rc 4.2 MB, own 4.2 MB, none 50.3 MB.Still open (recorded in docs)
if (x) return c; use(c). This predates the branch.Docs:
reference-counting-evaluation.md§9.79; own spec §11.1 and §11.4.🤖 Generated with Claude Code