Borrowing mutable and immutable in loops

I've hit some compiler error that does seem like a limitation of the borrow checker to me. But maybe I am overlooking something. I would be happy if someone could have a look at it.

I've reduced my specific case to the following code (Playground):

use std::collections::VecDeque;

struct BufferQueue {
    queue: VecDeque<Vec<u8>>,
    index: usize,
}

impl BufferQueue {
    fn advance_buffer(&mut self) {
        self.queue.pop_front();
        self.index = 0;
    }
    
    fn first_buffer_with_data(&mut self) -> Option<&[u8]> {
        while let Some(buffer) = self.queue.front() {
            if self.index < buffer.len() {
                return Some(&buffer[self.index..]);
            } else {
                self.advance_buffer();
            }
        }

        None
    }
}

If I compile this with the currently stable compiler or with the nightly 2024 edition, I get the following error:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable
  --> src/lib.rs:19:17
   |
14 |     fn first_buffer_with_data(&mut self) -> Option<&[u8]> {
   |                               - let's call the lifetime of this reference `'1`
15 |         while let Some(buffer) = self.queue.front() {
   |                                  ---------- immutable borrow occurs here
16 |             if self.index < buffer.len() {
17 |                 return Some(&buffer[self.index..]);
   |                        --------------------------- returning this value requires that `self.queue` is borrowed for `'1`
18 |             } else {
19 |                 self.advance_buffer();
   |                 ^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

For more information about this error, try `rustc --explain E0502`.

In my head, this should be fine. The first branch in the loop uses the borrowed buffer, but is returning and therefore should not make any issues. And the second branch does not need the buffer. Therefore, with non-lexical lifetimes, it should not be a problem to again borrow self mutable. But the compiler does not seem to share my point of view :man_shrugging:

The thing is, I can modify the code more or less slightly and the compiler accepts it (Playground):

fn first_buffer_with_data(&mut self) -> Option<&[u8]> {
        while let Some(buffer) = self.queue.front() {
            if self.index < buffer.len() {
                break; // CHANGED
            } else {
                self.advance_buffer();
            }
        }

        self.queue.front().map(|buffer| &buffer[self.index..]) // CHANGED
    }

Why is that different (except slightly less efficient) than the version before?

The compiler says that self.advance_buffer() uniquely borrows self (i.e. its receiver is &mut self). In addition, self.queue.front() sharedly borrows self.

Those 2 things clash, because when a method borrows self, it locks all of self, not just the fields used by the method.

That's also why your 2nd example works: the borrows are not happening at the same time.

Looks like a conditional return of a borrow, which the next-gen borrow checker (Polonius) is expected to accept.

You need branch-specific lifetimes, which was dropped from NLL (but not Polonius).

1 Like

I honestly don't understand, why in any case the borrows would happen at the same time, at least not with non-lexical lifetimes that end as soon as they are not needed anymore :man_shrugging:

Ah, that makes sense. I already tried to use Polonius, but just activating nightly and edition 2024 in playground probably is not sufficient. I will try it with the nightly compiler and that special Polonius Flag.

Since I already tried to research into Polonius a little bit before asking the question: Can someone tell, why Polonius still is in progress, but the repository is already years without progress? Was the implementation moved somewhere else?

I tried it with the nightly compiler and the -Zpolonius flag and yes, the compiler now accepts it. Seems like @quinedot was right. Thank you all :slight_smile:

I think it's just in-tree; consider this changeset for example. Working group repos do seem to wither out even when the work is continuing, sadly.

1 Like