Don't re-drop partially-moved sub-places (fix #121, #122)#155
Draft
coord-e wants to merge 1 commit into
Draft
Conversation
This was referenced Jun 28, 2026
12b6c5f to
43aaebb
Compare
43aaebb to
e5b8321
Compare
A local with a partial field move (e.g. `move (_2.0)`) was still dropped wholesale, so dropping it walked into the moved-out sub-place and resolved the `&mut` prophecy it owns a second time. The two resolutions contradict, making the clause body unsatisfiable, after which any assertion -- including false ones -- "verifies" (#121, #122). `Moves::collect` gathers all non-reference `move`d operands in one body traversal: whole-local moves (keyed by location, where the local also dies) and, keyed by parent local, the partial field moves. `DropSet { drops, except }` carries, per drop point, the whole locals to drop plus the moved-out sub-places to skip. A dying local is dropped whole, but its partial-move sub-places are passed to the drop as `except`: the drop walk resolves the still-owned siblings and skips the moved-out subtrees (resolved at the move destination), so the fix is both sound and complete. Drop targets stay whole locals; the closure environment restored by `RustCallVisitor` reaches its drop through a projection-less temporary. `Env::dropping_formula_for_term` threads the drop-walk `place` and `except` alongside the type/term walk and returns early on any subtree matching an excepted place. The moved-out places are elaborated to the walk's form via `Env::elaborated_path`, which inserts the `own`-box `Deref`s the type elaboration introduces (mut/reborrowed locals, and every tuple field) so the comparison lines up exactly -- without this a partially-moved local that gets box-elaborated (e.g. when a sibling is mutated through) would not be skipped, and the false assertion would verify vacuously. Adds regression tests: the two original shapes (#121, #122), plus a partially-moved local with a still-owned sibling (completeness) and a soundness guard where the moved part is used and the sibling is reborrowed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NU1g7aHMxcSEZgeKProDr1
e5b8321 to
4c88826
Compare
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 #121 and #122. Supersedes #124.
Bug
A local with a partial field move (e.g.
move (_2.0)) was still dropped wholesale, so the drop walked into the moved-out sub-place and resolved the&mutprophecy it owns a second time. The two resolutions contradict (final = 1∧final = 2), making the clause body unsatisfiable, after which any assertion — including false ones — "verifies".Approach
All drop information lives in
DropPoints; a dying local is dropped whole while its moved-out sub-places are skipped.drop_point.rs—Moves::collectgathers all non-referencemoved operands in one body traversal: whole-local moves (keyed by location, where the local also dies) and, keyed by the parent local, the partial field moves.DropSet { drops, except }carries, per drop point, the whole locals to drop plus the moved-out sub-places to skip. Drop targets stay whole locals (drops: HashSet<Local>) — the closure environment restored byRustCallVisitorreaches its drop through a projection-less temporary.env.rs—dropping_formula_for_termthreads the drop-walkplaceandexceptalongside the type/term walk (introduced in Fix enum-drop of aggregate mutable-reference fields #167) and returns early on any subtree matching an excepted place, so the drop resolves the still-owned siblings and skips the moved-out subtrees (which are resolved at the move destination). The fix is both sound and complete. The moved-out places are elaborated to the walk's form viaEnv::elaborated_path, which inserts theown-boxDerefs the type elaboration introduces (mut/reborrowed locals, and every tuple field), so the comparison lines up exactly — without this a partially-moved local that gets box-elaborated (e.g. when a sibling is mutated through) would not be skipped, and the false assertion would verify vacuously. WithPath::PlaceTygone (Fix enum-drop of aggregate mutable-reference fields #167),Pathnow derivesEq, so the match is a plain==.Testing
New pass/fail regression tests:
partial_move_drop.rs— Unsoundness: partially-moved locals are still implicitly dropped, resolving prophecies of moved-out&mutborrows #121's partial-move-into-local (let b = s.0;).partial_move_field_call.rs— Unsound: aggregate dropped wholesale after a partial field-move double-resolves the field's &mut prophecy #122's partial-move-into-call (owned(&mut i64,)field passed to a function).partial_move_sibling.rs— a partially-moved local with a still-owned sibling: the sibling's prophecy must be resolved by the parent's drop (completeness), and — when the moved-out part is used and the sibling is reborrowed — the moved-out sub-place must not be resolved twice (soundness).Each false-assertion variant reports
Unsat(rejected); the true companions verify. Full UI suite: 298 passed, 0 failed (pcsat via Docker, z3 4.15.4), plus doc-tests.cargo fmt --all -- --check,cargo clippy -- -D warnings, andgit diff --checkare clean.Note on #154
#154 proposed migrating drop points to MIR
Placeto support projected drops. That turned out to be unnecessary: drop targets are always whole locals (the closure-environment case reaches its drop through a projection-less temporary), so this PR keepsLocaldrops and only tracks the moved-out sub-places asexcept.🤖 Generated with Claude Code