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
coord-e
force-pushed
the
claude/pr-154-mergeable-vt1l0f
branch
5 times, most recently
from
July 2, 2026 04:54
12b6c5f to
43aaebb
Compare
coord-e
force-pushed
the
claude/pr-154-mergeable-vt1l0f
branch
2 times, most recently
from
July 8, 2026 10:55
e5b8321 to
4c88826
Compare
coord-e
force-pushed
the
claude/pr-154-mergeable-vt1l0f
branch
from
August 9, 2026 02:19
4c88826 to
cd98f92
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 `path` and `except` alongside the type/term walk and returns early on any subtree matching an excepted path. 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> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A3zd4nCdz5atbgNuqXEJTc
coord-e
force-pushed
the
claude/pr-154-mergeable-vt1l0f
branch
from
August 9, 2026 02:34
cd98f92 to
a87b28e
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes prophecy double-resolution when partially moved locals are dropped.
Changes:
- Tracks moved sub-places as drop exceptions.
- Skips excepted paths during drop-formula generation.
- Adds pass/fail regression coverage for local, call, and sibling cases.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/analyze/basic_block/drop_point.rs |
Collects moves and builds exception-aware drop sets. |
src/analyze/basic_block.rs |
Applies drop sets during block analysis. |
src/analyze/local_def.rs |
Propagates lifetime-aware drop-point types. |
src/refine/env.rs |
Skips moved paths while generating drop formulas. |
tests/ui/pass/partial_move_drop.rs |
Tests a valid local partial move. |
tests/ui/fail/partial_move_drop.rs |
Rejects the unsound local variant. |
tests/ui/pass/partial_move_field_call.rs |
Tests a valid field move into a call. |
tests/ui/fail/partial_move_field_call.rs |
Rejects the unsound call variant. |
tests/ui/pass/partial_move_sibling.rs |
Tests correct sibling prophecy resolution. |
tests/ui/fail/partial_move_sibling.rs |
Guards against double-resolution with a sibling. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+164
to
+165
| if let Some(moved) = self.moves.partial.get(&local) { | ||
| set.except.extend(moved.iter().copied()); |
Comment on lines
+1223
to
+1225
| // A field is reached through a matcher selector rather than a | ||
| // projection, so the walk stays at the enum's path and a | ||
| // moved-out field is not skipped. |
|
|
||
| // Regression test for #122: a `&mut`-bearing field moved out of an aggregate | ||
| // into a call must not be re-dropped when the parent is dropped wholesale. | ||
| // `w.0` (an owned `(&mut i32,)`) is moved into `bump`; dropping `w` afterwards |
Comment on lines
+1119
to
+1123
| // Peel the `own` boxes the walk would deref before this projection. | ||
| while ty.ty.is_own() { | ||
| path = path.deref(); | ||
| ty = ty.deref(); | ||
| } |
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: BTreeSet<Local>) — the closure environment restored byRustCallVisitorreaches its drop through a projection-less temporary.env.rs—dropping_formula_for_termthreads the drop-walkpathandexceptalongside the type/term walk (introduced in Fix enum-drop of aggregate mutable-reference fields #167) and returns early on any subtree matching an excepted path, 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 on the rebased branch: 308 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