rust-lang / rust-lang/rust

RTITT with strange interaction between consuming-let-else bindings and borrow checker

Open
#154,695 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-borrow-checker needs-triage
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

Hi. I'm not sure if this is a bug or fundamental limitation:

I have, at the high level, two almost identical snippets, but the first one fails borrow checking, while the second is fine (rustc 1.94)

fn test1() {
    let (foo, mut attachment) = init();
    let foo = foo.instrument(&mut attachment);

    let mut foo = Poll::Pending(foo);
    while !foo.is_done() {
        foo = foo.poll_once();
    }
    let Poll::Done(x) = foo else {
        unreachable!();
    };
    let _att = &mut attachment; // compilation error
}


fn test2() {
    let (foo, mut attachment) = init();
    let foo = foo.instrument(&mut attachment);

    let mut foo = Poll::Pending(foo);
    while !foo.is_done() {
        foo = foo.poll_once();
    }
    // ! The only difference ! Extra move for foo !
    let Poll::Done(x) = ({ foo }) else {
        unreachable!();
    };
    let _att = &mut attachment; // fine
}

// impl details
// --------------------------------------------------

enum Poll<T: Task> {
    Pending(T),
    Done(T::Output),
}

trait Task {
    type Output;
    fn poll(self) -> Poll<Self> where Self: Sized;
}

impl<T:Task> Poll<T> {
    fn is_done(&self) -> bool {
        matches!(self, Poll::Done(_))
    }
    fn poll_once(self) -> Self {
        match self {
            Poll::Pending(x) => x.poll(),
            Poll::Done(y) => Poll::Done(y)
        }
    }
}


trait Foo {}
trait Instrument {}
impl <I: Instrument + ?Sized> Instrument for &mut I {}

trait FooInit: Task<Output = Self::Foo> {
    type Foo : Foo;
    // instrument doesn't change the type of output!
    // this is important to not keep data borrowed after task is Done
    fn instrument(self, data: impl Instrument) -> impl FooInit<Foo = Self::Foo>;
} 


struct FooImpl;
impl Foo for FooImpl {}
impl Task for FooImpl {
    type Output = FooImpl;
    fn poll(self) -> Poll<Self> {
        Poll::Done(self)
    } 
}
impl FooInit for FooImpl {
    type Foo = FooImpl;
    fn instrument(self, _data: impl Instrument) -> impl FooInit<Foo = Self::Foo> {
        self
    }
}

struct InstrumentImpl;
impl Instrument for InstrumentImpl {}


fn init() -> (impl FooInit, impl Instrument) {
    (FooImpl, InstrumentImpl)
}

https://godbolt.org/z/Exa1jGYY9

foo here is Poll<impl Task> where opaque type capture the reference lifetime

let mut foo = Poll::Pending(foo);

Looks like, for the first case,

  let Poll::Done(x) = foo else {
        unreachable!();
    };

Borrow checker still assumes that the Poll::Pending variant, capturing the lifetime, is still active. But it should be consumed -- value is moved out.

And for the second case

    let Poll::Done(x) = ({ foo }) else {
        unreachable!();
    };

borrow checker agrees that it became consumed and no longer active. This is weird.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by running the minimized reproducer from the issue on rustc 1.94 or the linked Godbolt example, comparing the direct let-else pattern with the extra move. Trace how consuming let-else bindings are handled by borrow checking and add a regression test for the differing cases. Done means the borrow checker consistently accepts the valid consumed-value pattern.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.