Why does the borrow checker fail to realize that conditional borrow did not occur?

Viewed 88

I have some Rust code that takes a reference and returns a Result that holds a reference in the Ok case and doesn't in the Err case. (This is simplified; in reality the output is a struct holding multiple references to the data and the error is something fancier.)

fn try_subslice(data: &[u8]) -> Result<&[u8], ()>

I have a struct that holds a Vec<u8>. I want to call try_subslice and either return the successful value or increase the size of the internal Vec, passing along the error.

pub struct Owner(Vec<u8>);

impl Owner {
    pub fn subslice_or_grow(&mut self) -> Result<&[u8], ()> {
        let result: Result<&[u8], ()> = try_subslice(&self.0);
        
        match result {
            Ok(value) => Ok(value),
            Err(_) => {
                self.0.resize(self.0.len() * 2, 0);
                Err(())
            }
        }
    }
}

The full example is on the Rust Playground.

When I do this, the compiler complains that the mut borrow required by resize overlaps with the non-mut borrow made when calling try_subslice. I know that by the time the Err branch of the match is taken, the non-mut borrow is definitely over, because there is no way for () to hold a reference.

error[E0502]: cannot borrow `self.0` as mutable because it is also borrowed as immutable
  --> src/lib.rs:10:17
   |
4  |     pub fn subslice_or_grow(&mut self) -> Result<&[u8], ()> {
   |                             - let's call the lifetime of this reference `'1`
5  |         let result: Result<&[u8], ()> = try_subslice(&self.0);
   |                                                      ------- immutable borrow occurs here
...
8  |             Ok(value) => Ok(value),
   |                          --------- returning this value requires that `self.0` is borrowed for `'1`
9  |             Err(_) => {
10 |                 self.0.resize(self.0.len() * 2, 0);
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here

If I modify the code to not return the value (e.g. Ok(value) => panic!()), then it compiles. Also, if I modify try_subslice to copy instead of borrow, then it also compiles.

When I squint, RFC 2094 Case #3 looks pretty similar to my situation. In their case, they were able to resolve it by adding a separate check to see which case they are in ahead of time. I don't see a way to do that without redesigning try_subslice (or calling it twice, once just to see which scenario we're in and otherwise discarding the result).

I have two questions:

  1. Am I correct that this is a gap in the current NLL borrow checker and that the code is fundamentally sound?
  2. Is there a sound way to work around this limitation (potentially using unsafe) without redesigning the try_subslice interface or double-calling?

Similar questions:

0 Answers
Related