Skip to content

Capture partial reborrows of &mut aggregate fields in mut_locals#127

Merged
coord-e merged 6 commits into
mainfrom
claude/sleepy-archimedes-iu2cyw
Jun 28, 2026
Merged

Capture partial reborrows of &mut aggregate fields in mut_locals#127
coord-e merged 6 commits into
mainfrom
claude/sleepy-archimedes-iu2cyw

Conversation

@coord-e

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

Copy link
Copy Markdown
Owner

Summary

Fixes #125.

Analyzing a function that takes an aggregate parameter (tuple/struct) and reborrows a &mut-typed field out of it panicked with deref unbound var in Env::locate_place:

fn bump(_r: &mut i64) {}
fn f(w: (&mut i64,)) {
    bump(w.0); // reborrow of the `&mut` field `w.0`
}

Root cause

The base local of the aggregate (_1) was never recognized as being (partially) mutably borrowed, so it was neither box-elaborated nor flow-decomposed, and resolving the _1.0 projection during reborrow elaboration failed.

reassign_local_mutabilities derives local mutability from mut_locals, whose operand rule only matched Operand::Move:

if let mir::Operand::Move(place) = operand {
    if place.ty(..).ty.is_mutable_ptr() { self.locals.insert(place.local); }
}

But a &mut field read out of an aggregate lowers to Operand::Copy, so _1 was never marked. This is despite ReborrowVisitor::visit_operand reborrowing any &mut-typed operand place (operand.place(), Copy or Move) — the two passes were inconsistent.

Why a &mut read is Operand::Copy

Operand::Copy vs Operand::Move is not about whether the type is Copy — it records whether the source place stays valid after the read. Per the rustc doc comment on Operand::Copy: "Before drop elaboration, the type of the place must be Copy. After drop elaboration there is no such requirement." The MIR thrust consumes (optimized_mir) is post-drop-elaboration, so a non-Copy &mut is legally read via copy. rustc emits copy whenever the source is dead/reborrowed, which is essentially every &mut read:

reborrow  bump(w.0):  _3 = copy (_1.0: &mut i64);  _2 = bump(move _3)
move-out  ret  w.0:   _0 = copy (_1.0: &mut i64);  return

So Move-only matched a case that almost never occurs for &mut. (Top-level &mut locals still worked because bind_local has a separate rty.ty.is_mut() type-check; a tuple isn't is_mut(), so the aggregate field fell through both.)

Fix

Match Copy as well, and narrow the type test from is_mutable_ptr() to a &mut reference:

if let mir::Operand::Move(place) | mir::Operand::Copy(place) = operand {
    if matches!(place.ty(..).ty.kind(), TyKind::Ref(_, _, m) if m.is_mut()) {
        self.locals.insert(place.local);
    }
}

The narrowing is required: is_mutable_ptr() also matches *mut T raw pointers, which genuinely are Copy and are read by copy constantly, but are never reborrowed. The old Move-only gate excluded them for free (a *mut is Copy, so it never appeared as Move); adding Copy without restricting to references would over-mark every raw-pointer copy. This now aligns exactly with what ReborrowVisitor reborrows.

This makes the type-based contains_mut workaround from earlier revisions unnecessary; it was reverted and removed.

Tests

Added pass/fail UI regression tests (reborrow_mut_field_of_aggregate_param). Full suite green (234 passed, 0 failed) with z3 4.13.0 and the COAR docker image, matching CI.

References

🤖 Generated with Claude Code

claude added 4 commits June 14, 2026 12:34
Reborrowing a `&mut`-typed field out of an aggregate (tuple/struct)
parameter panicked with "deref unbound var" in `Env::locate_place`.
Such an aggregate parameter is an immutable local whose type is a tuple,
so it took the `immut_bind` path and was stored without flow bindings.
The reborrow elaboration then failed to resolve the field projection to
the inner `&mut`.

Detect mutable references reachable through aggregate and pointer
projections via a new `Type::contains_mut` and route such parameters
through the flow-decomposing `mut_bind` path, matching how a top-level
`&mut` parameter is already handled.

Fixes #125

https://claude.ai/code/session_01MQbByXbDZ8FxkhRra4nPfN
Follow the repo convention of pairing each ui test in pass/ and fail/.
Drive the test from main so verification has a concrete assertion to
check, and add the fail counterpart asserting the negated condition.
@coord-e coord-e force-pushed the claude/sleepy-archimedes-iu2cyw branch from 42b206f to bc72f58 Compare June 28, 2026 08:47
#125 panicked ("deref unbound var") when reborrowing a &mut field out of
an aggregate parameter. Root cause: the partial reborrow of `w.0` was
never detected, so the base local was neither box-elaborated nor flow-
decomposed, and resolving the field projection during reborrow failed.

A &mut field read out of an aggregate appears as `Operand::Copy(_1.0)`
(the pointer value is copied), but `mut_locals`' operand rule only matched
`Operand::Move`, so the base local `_1` was never marked. ReborrowVisitor
reborrows any &mut-typed operand place regardless of Copy/Move, so the two
were inconsistent.

Match Copy as well, narrowing the type test from `is_mutable_ptr()` to a
&mut reference: `is_mutable_ptr()` also covers `*mut` raw pointers, which
are Copy and are never reborrowed, so widening to Copy without the
reference restriction would over-mark every raw-pointer copy.

This makes the type-based `contains_mut` workaround unnecessary; revert it
and remove the helper. Keep the pass/fail regression tests.

Fixes #125

https://claude.ai/code/session_01MQbByXbDZ8FxkhRra4nPfN
@coord-e coord-e changed the title Bind aggregate params containing &mut with flow bindings Capture partial reborrows of &mut aggregate fields in mut_locals Jun 28, 2026
@coord-e coord-e marked this pull request as ready for review June 28, 2026 13:04
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

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

Fixes a panic in the MIR analysis pipeline when reborrowing a &mut-typed field out of an aggregate (tuple/struct) parameter by ensuring the base local is recognized as (partially) mutably borrowed in mut_locals.

Changes:

  • Extend mut_locals operand handling to treat both Operand::Move and Operand::Copy of &mut references as indicating a mutable borrow.
  • Narrow the mutability detection to &mut references (excluding *mut raw pointers) to avoid over-marking locals.
  • Add UI pass/fail regression tests covering reborrowing a &mut field from an aggregate parameter.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
tests/ui/pass/reborrow_mut_field_of_aggregate_param.rs Adds a passing regression case ensuring aggregate-field &mut reborrow no longer panics and behaves as expected.
tests/ui/fail/reborrow_mut_field_of_aggregate_param.rs Adds a failing regression case ensuring the analysis reports Unsat (rather than panicking) for an inconsistent postcondition.
src/analyze/local_def.rs Updates mut_locals to detect &mut reborrows through Operand::Copy as well as Operand::Move, aligned with ReborrowVisitor.

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

@coord-e coord-e merged commit eaa87d1 into main Jun 28, 2026
7 checks passed
@coord-e coord-e deleted the claude/sleepy-archimedes-iu2cyw branch June 28, 2026 13:30
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.

Panic: "deref unbound var" when reborrowing a &mut field out of an aggregate parameter

3 participants