Skip to content

Weaken closure parameter for FnMut closures called via FnOnce#120

Merged
coord-e merged 5 commits into
mainfrom
claude/issue-46-4zfjsz
Jun 13, 2026
Merged

Weaken closure parameter for FnMut closures called via FnOnce#120
coord-e merged 5 commits into
mainfrom
claude/issue-46-4zfjsz

Conversation

@coord-e

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

Copy link
Copy Markdown
Owner

Fixes #46 (the remaining reopened case: calling an FnMut closure via FnOnce::call_once).

Problem

Calling an FnMut closure through FnOnce::call_once resolves to the closure function taking &mut {closure}, while the caller's MIR passes the closure environment by value. This hit the unimplemented!() "case 3" in RustCallVisitor.

The draft patch in the issue comment fixed the panic but lost completeness: after rewriting the call to pass &mut {closure}, the environment local stays live in the caller, yet the drop-point analysis (computed on the original MIR, where the local was moved into the call) never schedules a drop for it. The prophecies of its captured mutable borrows were therefore never resolved (final = current), leaving the captures' final values unconstrained.

Fix

  • RustCallVisitor case 3: insert a mutable borrow of the closure environment and pass it as the argument. Since FnOnce::call_once consumes the closure, schedule both the borrow and the environment local to be dropped right after the call, which resolves the prophecies of the captured mutable borrows and restores completeness.
  • DropPoints: the scheduled drops are stored next to the liveness-derived sets, behind DropPoints::insert_after_terminator and an Analyzer::drop_after_terminator helper. They are kept as a Vec because the locals are created during elaboration and lie outside the bitset domains sized to the original body's locals.
  • reassign_local_mutabilities: mark the closure argument of a FnOnce::call_once call as mutable when the closure's kind is FnMut (implementing the TODO from the issue comment by checking the closure kind instead of the resolved signature), so the environment is boxed in the refinement environment and can be mutably borrowed.

Tests

  • New tests/ui/{pass,fail}/closure_param_weaken_3.rs with the example from the issue: assert!(result == 3 && x == 2) is now proven (the issue's draft patch could not), and the incorrect variant (x == 1) is rejected with Unsat.
  • Full cargo test passes locally (230/230 UI tests, including the pcsat/COAR-based ones), as do cargo fmt --check and cargo clippy.

https://claude.ai/code/session_012451PZJH2QqdqE5Zyxna5y

claude and others added 3 commits June 12, 2026 15:20
Calling an FnMut closure through FnOnce::call_once resolves to the
closure function taking &mut {closure}, while the caller passes the
closure environment by value, which hit the unimplemented case 3 in
RustCallVisitor.

Implement case 3 by mutably borrowing the closure argument, and restore
completeness by dropping the borrow and the environment right after the
call: FnOnce::call_once consumes the closure, so dropping the
environment at the call site resolves the prophecies of its captured
mutable borrows. Without these drops the captured borrows' final values
stay unconstrained and assertions about the mutated captures cannot be
proven.

To make the environment local borrowable, reassign_local_mutabilities
now also marks the closure argument of such calls as mutable, so it is
boxed in the refinement environment.

https://claude.ai/code/session_012451PZJH2QqdqE5Zyxna5y
Instead of a separate Vec field on basic_block::Analyzer, store the
extra after-terminator drops in DropPoints next to the liveness-derived
sets, behind DropPoints::insert_after_terminator and an Analyzer helper.
They are kept as a Vec because the locals are created during elaboration
and lie outside the bitset domains sized to the original body's locals.

https://claude.ai/code/session_012451PZJH2QqdqE5Zyxna5y
style fix
@coord-e coord-e force-pushed the claude/issue-46-4zfjsz branch from 421ec68 to 5a96655 Compare June 13, 2026 02:52
…rand

The guard mirrors `moved_locals`, which only removes the drop obligation
for whole-local moves; a projected operand's obligation was never stolen,
so re-adding it would double-drop. Expand the comment to make this
explicit rather than reading as an arbitrary restriction.

https://claude.ai/code/session_012451PZJH2QqdqE5Zyxna5y

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses an incompleteness/panic when an FnMut closure is invoked through FnOnce::call_once, by rewriting the call to introduce an implicit mutable borrow of the closure environment and ensuring the corresponding drop obligations are recorded even though the locals are created during elaboration (outside the original MIR liveness domains).

Changes:

  • Add a RustCallVisitor “case 3” transform to handle {closure} -> &mut {closure} for FnOnce::call_once, and schedule implicit drops after the call.
  • Extend DropPoints with an “extra after-terminator drops” list to support elaboration-created locals.
  • Update local mutability inference so FnOnce::call_once receiving an FnMut closure treats the closure argument as mutable, enabling boxing/borrowing as needed.
  • Add new UI pass/fail tests covering the reopened scenario from issue #46.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/ui/pass/closure_param_weaken_3.rs Adds a passing UI test exercising FnMut closure called via FnOnce::call_once.
tests/ui/fail/closure_param_weaken_3.rs Adds a failing UI test variant expected to be Unsat.
src/analyze/local_def.rs Marks closure arguments as mutable for FnOnce::call_once when the closure kind is FnMut.
src/analyze/basic_block/visitor/rust_call.rs Implements the previously unimplemented!() rust-call “case 3” rewrite and schedules drops after the call.
src/analyze/basic_block/drop_point.rs Adds support for “extra” after-terminator drops (for locals introduced during elaboration).
src/analyze/basic_block.rs Wires the new after-terminator drop list into terminator typing and adds an analyzer helper to schedule those drops.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/analyze/basic_block/visitor/rust_call.rs
Comment thread src/analyze/basic_block/drop_point.rs
The same local must not be scheduled for an implicit drop twice. Use a
BTreeSet so duplicate insertions collapse, while keeping a deterministic
index order consistent with the DenseBitSet-derived drops.

https://claude.ai/code/session_012451PZJH2QqdqE5Zyxna5y
@coord-e coord-e merged commit bcd2428 into main Jun 13, 2026
6 checks passed
@coord-e coord-e deleted the claude/issue-46-4zfjsz branch June 13, 2026 07:01
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.

Weaken the closure parameters in calls using rust-call

3 participants