2

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:

ddulaney
  • 843
  • 7
  • 19
  • 2
    Indeed, with `-Zpolonius` your code compiles. I think you are right and this is a case-3 scenario. – rodrigo Dec 23 '20 at 09:03
  • Can I ask how you ran it with `-Zpolonius`? I tried `cargo +nightly build -Zpolonius` and it just said `error: unknown `-Z` flag specified: polonius`. – ddulaney Dec 23 '20 at 16:43
  • 1
    Sure, copied the code to a `test.rs`, added a dummy `main` and run `rustc +nightly -Zpolonius test.rs`. There are ways to use it with `cargo` but for a compile only test, it is good enough. – rodrigo Dec 23 '20 at 17:14
  • This isn't a full answer, but for anybody who needs to manipulate lots of byte buffers: the [bytes](https://crates.io/crates/bytes) crate recently hit 1.0 and ended up making this question irrelevant for my project. I can recommend it for the specific case of "I need to handle some byte arrays with as little copying as possible, but it's hard to prove some of these edge cases to the borrow checker". – ddulaney Dec 28 '20 at 08:01

0 Answers0