rust
rust copied to clipboard
Wrapper variable considered live after all of its constituents are consumed
I tried this code:
use either::Either;
struct A;
struct B;
struct AW<'a>(&'a mut A);
impl<'a> Drop for AW<'a> {
fn drop(&mut self) {
}
}
fn cap(_: &mut A) -> Either::<AW<'_>, B> {
unimplemented!()
}
pub fn main() {
let mut a = A;
let b = cap(&mut a);
match b {
Either::Left(a) => {
drop(a);
},
Either::Right(y) => {
a = A; // <- doesn't compile
}
}; // No way is the compiler going to drop b at this point in either case
}
In this piece of code, the compiler keeps b alive for the entire duration of match, preventing me from assigning to a in the Right arm. The explanation it gives is that it needs to run drop on b at the end of the match.
error[E0506]: cannot assign to `a` because it is borrowed
--> src/main.rs:22:13
|
15 | let b = cap(&mut a);
| ------ borrow of `a` occurs here
...
22 | a = A;
| ^^^^^ assignment to borrowed `a` occurs here
...
26 | }
| - borrow might be used here, when `b` is dropped and runs the destructor for type `Either<AW<'_>, B>`
However, that's not the case at all. b is partially moved in both arms, its drop cannot be called at the end of match.
Meta
rustc --version --verbose:
rustc 1.50.0-nightly (f76ecd066 2020-12-15)
binary: rustc
commit-hash: f76ecd0668fcdb289456cdc72a39ad15467cc454
commit-date: 2020-12-15
host: x86_64-unknown-linux-gnu
release: 1.50.0-nightly
I think this is a well-known borrow-checker bug — see https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/borrow.20error.
@rustbot label: T-compiler
Update based on discussion happened on zulip.
Some suggested using polonius, but the example code doesn't compile with polonius enabled either.
@danielhenrymantilla reduced it to
struct A<'a>(&'a i32);
impl<'a> Drop for A<'a> {
fn drop (&mut self)
{}
}
fn main ()
{
let mut x = 10;
let r = (A(&x), );
let a = r.0;
drop(a);
x = 11; // Ok if A wasn't Drop
}
So there needn't be a match to trigger this problem.
Quoting:
No branches, and it still fails (so I really doubt it's polonius). The key thing is that dropping a field (even if it's the only field!) of a
struct/enumdoes not seem to tag the wrapper as consumed,dropck-wise, even if, at first glance, it looks to me like it could.
For people stumbling across this looking for a workaround, one option is putting the Drop variable in a ManuallyDrop, and make sure to drop it manually.
Using this as an example:
use std::task::Poll;
struct A<'a>(&'a i32);
impl<'a> Drop for A<'a> {
fn drop(&mut self) {}
}
fn main() {
let mut x = 10;
let r: Poll<_> = Poll::Ready(A(&x));
match r {
Poll::Ready(a) => {
drop(a);
x = 11; // Ok if A wasn't Drop
}
Poll::Pending => {}
}
}
Change it to:
use std::task::Poll;
use std::mem::ManuallyDrop;
struct A<'a>(&'a i32);
impl<'a> Drop for A<'a> {
fn drop(&mut self) {}
}
fn main() {
let mut x = 10;
let r: Poll<_> = Poll::Ready(ManuallyDrop::new(A(&x)));
match r {
Poll::Ready(a) => {
ManuallyDrop::into_inner(a);
x = 11;
}
Poll::Pending => {}
}
}
Another workaround is to not partially move out of b and unconditionally drop b in every match arm.
match b {
Either::Left(AW(&mut ref mut a)) => { // or just Either::Left(_)
drop(b);
},
Either::Right(ref y) => { // just Either::Right(_)
drop(b);
a = A;
}
};
I wouldn't call this a bug in the borrow checker. The borrow checker can't determine where drops / drop glue runs; it is given that information (or tentative information). I'd say it's a shortcoming of the drop checker (or whatever it is that determines a value which has been unconditionally moved doesn't need drop glue at the end of scope). Presumably with regards to partial moves and/or moves out of enum variants "adding up" to an unconditional move.