Repository navigation
-mm=own: a let given a new value after its value moved owns the new one (#528) - #530
Merged
Merged
Conversation
…ne (#528) `let b = a; a = new C()` and `h.c = a; a = new C()` were rejected: the assignment was a use of `a` after the move, because rc's assignment releases what the slot holds before storing - the moved value, which the receiver owns. checkSlotMove now treats an assignment the move reaches (ts.ReleaseSlot right before ts.Store of the slot) as a new value, as it treats the declaration: uses and releases reached only through one are the new value's. The assignment's own release goes with the move; the releases after it stay and destroy the new value. A release reached both holding the moved value and a newer one is "moved on some paths". Receivers of one slot that each read a value of their own (an assignment between every two of their reads) are decided one at a time, so `h.c = a; a = new C(); g.c = a` moves twice. readUsedAfter also kills at the read itself: around a loop, a path back through the read uses a value read again. own_move_reassign runs relay/field/twice/loop shapes under every model. own_err_assign_after_move (the old shape now compiles) becomes a read after an assignment on one path only; own_err_reassign_some_paths pins the new release check. own_err_borrow_{bound_method,catch_local, finally_local} used `let b = a; ... a = new C()` to make `b` borrow; that is now a move, so each reads `a` once after `let b = a` to keep `b` a borrow. Corpus under own unchanged (467/628 either way). Closes #528 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under -mm=none nothing is freed and the test's loops hold about 800 MB, too much for a CI run; rc and gc show the program means the same. The spec gets section 24: an assignment after a move is a new value, the "some paths" release rule, receivers decided per value, loops. 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.
Closes #528.
Problem
let b = a; a = new C()andh.c = a; a = new C()were rejected under-mm=own. Phase 1 deliberately counted the assignment as a use ofaafter the move. rc's assignment isReleaseSlot(a); Store(new, a), and after a move that release would free the value the receiver now owns. Rust accepts both shapes, and rc and gc print1 2.Fix (
OwnershipInferencePass.cpp)checkSlotMove: an assignment the move reaches (ats.ReleaseSlotright before ats.Storeto the same slot) is a new value, like the slot's declaration. It is now a kill for reachability. As a result:moved here on some paths only.decideSlot: receivers of one slot that each read a value of their own (an assignment between their reads on every path) are decided one at a time. Soh.c = a; a = new C(); g.c = ais two moves.readUsedAfter: a path that goes back through the read itself now uses a value read again. This is what letskeep.push(a); a = new C()work inside a loop.After the fix,
let b = a; a = new C(); print(b.value, a.value)becomes: the move erasesb'sRetainSlotand the assignment'sReleaseSlot, and keeps both scope-exit releases. Each object is freed exactly once.Tests
New:
own_move_reassign, covering:let, then a new value;It runs under own (JIT, AOT, no-counting) and under rc and gc. It isn't run under none: the loops would hold about 800 MB with nothing freed.
New:
own_err_reassign_some_paths: a new value on one path only, so the scope-exit release has no single answer.Changed:
own_err_assign_after_move. Its old shape now compiles, so it becomes a read after a new value given on one path only. The borrow error is unchanged.Changed:
own_err_borrow_{bound_method,catch_local,finally_local}. These usedlet b = a; … a = new C()to force a borrow, and that is now a valid move: the old versions compile and print the right values. Each now readsaonce afterlet b = a, sobstays a borrow and the intended error still fires at the same use.Spec: new section 24.
Verification
tests/*.tsunder-mm=own): 467 of 628 compile, the same set with and without the fix.own_move_reassign: own 4.4 MB, rc 4.5 MB.ProcessHeap.h: blocks never reused, freed memory poisoned, abort on double free). With the fix, every shape above and both issue repros run clean under JIT and AOT. With a temporary switch that keeps the assignment's release, all of them double free or read freed memory.Not changed
The issue also notes that the errors say
'this value'. That is by design: variable names come from--dilocations, astest-own-err-names-the-variablepins. Without--dithe pass has no name to print.🤖 Generated with Claude Code