Borrow checker error in loop with early return - shouldn't NLL handle this case?

Hi everyone,

I'm encountering a borrow checker error that I believe should be resolved by Non-Lexical Lifetimes (NLL), but it's failing to compile in Rust 1.88 (2024 Edition). I'd appreciate some insight into whether this is expected behavior or if I'm missing something.

Minimal reproducing example:

struct Test {
    current: Option<(Vec<u8>, usize)>,
}

impl Test {
    fn peek(&mut self) -> Option<&[u8]> {
        loop {
            match &self.current {
                Some((bytes, pos)) => {
                    if *pos < bytes.len() {
                        return Some(&bytes[*pos..]);  // immutable borrow here
                    }
                }
                None => return None,
            }
            self.next(); // ERROR: cannot borrow `*self` as mutable
        }
    }
  
    fn next(&mut self) {
        self.current = None;
    }
}

Compiler error:

error[E0502]: cannot borrow `*self` as mutable because it is also borrowed as immutable

My understanding:

According to NLL, the immutable borrow &bytes[*pos..] should end at the return statement, so self.next() should be able to mutably borrow self since the immutable borrow is no longer live.

Questions:

  1. Is this expected behavior, or should NLL handle this case?
  2. If this is a limitation, is there a specific reason why the borrow checker can't prove safety here?
  3. Are there any known issues or discussions about this pattern?

I've tried various workarounds, but I'm curious about the fundamental reason why this doesn't compile and whether it's considered a soundness issue or just a limitation of the current borrow checker.

Thanks for any insights!

I think this is the problem case #4 in the NLL rfc, which is a known issue. this case is tackled in the new polonius borrow checker, which can be enabled with the -Zpolonius compiler flag.

see this blog post for an overview: Polonius update | Inside Rust Blog

see also the docs of the polonius-the-crab crate for a solution on current stable rust and more detailed descriptions.

1 Like

Thanks for the quick reply! I have tried polonius solver and it works!

But it seems that I cannot workaround it with NLL rfc's case #4 (to rename self to self1 in loop), it still errored. So is there any way to work around it?

the workaround mentioned in nll case #4 does not apply to your example, because the you have states being modified that is persistent across method calls, unlike the one in the rfc.

for this particular example, the workaround is simple, just move the return statement out of the loop, something like this:

	fn peek(&mut self) -> Option<&[u8]> {
		let mut success = false;
		loop {
			match self.current.as_ref() {
				Some((bytes, pos)) => {
					if *pos < bytes.len() {
						success = true;
						break;
					} else {
						self.next();
					}
				}
				None => {
					break;
				}
			};
		}
		if success {
			let (bytes, pos) = self.current.as_ref().unwrap();
			Some(&bytes[*pos..])
		} else {
			None
		}
	}

P.S. this is more or less the same technique as the workaround in the canonical polonius motivating example get_default(map: &'a mut Map, key: &Key) -> &'a mut Value, also noted in problem case #3 in the nll rfc. both incurs redundant work: in the nll case #3, the workaround needs a redundant hash look up for map.contains() and map.insert(), while in this example, it's an extra check of the Option discriminant (hidden inside the Option::unwrap() call).

1 Like

Works like a charm! Thanks again for kindly and useful reply!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.