Skip to content

Don't re-drop partially-moved sub-places (fix #121, #122)#155

Draft
coord-e wants to merge 1 commit into
mainfrom
claude/pr-154-mergeable-vt1l0f
Draft

Don't re-drop partially-moved sub-places (fix #121, #122)#155
coord-e wants to merge 1 commit into
mainfrom
claude/pr-154-mergeable-vt1l0f

Conversation

@coord-e

@coord-e coord-e commented Jun 28, 2026

Copy link
Copy Markdown
Owner

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 &mut prophecy it owns a second time. The two resolutions contradict (final = 1final = 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.rsMoves::collect gathers all non-reference moved 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 by RustCallVisitor reaches its drop through a projection-less temporary.
  • env.rsdropping_formula_for_term threads the drop-walk place and except alongside 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 via Env::elaborated_path, which inserts the own-box Derefs 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. With Path::PlaceTy gone (Fix enum-drop of aggregate mutable-reference fields #167), Path now derives Eq, so the match is a plain ==.

Testing

New pass/fail regression tests:

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, and git diff --check are clean.

Note on #154

#154 proposed migrating drop points to MIR Place to 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 keeps Local drops and only tracks the moved-out sub-places as except.

🤖 Generated with Claude Code

@coord-e coord-e force-pushed the claude/pr-154-mergeable-vt1l0f branch 5 times, most recently from 12b6c5f to 43aaebb Compare July 2, 2026 04:54
@coord-e coord-e force-pushed the claude/pr-154-mergeable-vt1l0f branch from 43aaebb to e5b8321 Compare July 7, 2026 12:25
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
@coord-e coord-e force-pushed the claude/pr-154-mergeable-vt1l0f branch from e5b8321 to 4c88826 Compare July 8, 2026 10:55
@coord-e coord-e changed the title Track drop points by MIR Place, splitting partially-moved locals (fix #121, #122) Don't re-drop partially-moved sub-places (fix #121, #122) Jul 8, 2026
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.

Unsoundness: partially-moved locals are still implicitly dropped, resolving prophecies of moved-out &mut borrows

2 participants