RTITT with strange interaction between consuming-let-else bindings and borrow checker
Nobody has claimed this yet.
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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