Borrow held even though nothing is borrowed

I have some trouble getting the borrow checker to understand my intentions. The following is a simplified version of my code: a next method tries to generate an Object (which borrows data) from a byte array at an index, advances the index into the byte array, and returns the Object. Once no more Objects can be generated (Object::get returns None), the potential rest of the array is discarded and the index is reset. But even though Object::get returned None, the borrow checker still declares the data as borrowed, and disallows the discarding:

struct Buffer {
    content: Vec<u8>,
    index: usize,
}

impl Buffer {
    // Fails to compile
    fn next(&mut self) -> Option<Object> {
        if let Some(obj) = Object::get(&self.content[self.index..]) {
            self.index += 1; // Example consumes only 1 byte, real code can consume multiple
            return Some(obj);
        }

        // `Object::get` returned `None`, so I expect nothing to be borrowed at this point.
        self.content.clear(); // error[E0502]: cannot borrow `self.content` as mutable, since it is already borrowed
        self.index = 0;
        None
    }
}

#[derive(Debug)]
struct Object<'a>(&'a u8);

impl<'a> Object<'a> {
    fn get(buffer: &'a [u8]) -> Option<Self> {
        buffer.get(0).map(|b| Self(b))
    }
}

Instead of if-let I tried other control flow constructs like match and let-else, with the same (consistent) outcome. The following two variants do compile, but have their own issues:

    // Compiles: Does not return an `Object`, no borrow checker issues.
    fn next_no_return(&mut self) {
        if let Some(obj) = Object::get(&self.content[self.index..]) {
            println!("{obj:?}");
            self.index += 1;
            return;
        }

        self.content.clear(); // no error
        self.index = 0;
    }

    // Compiles: Parses the current object 2 times, now it's OK for the borrow checker.
    fn next_parse_twice(&mut self) -> Option<Object> {
        if let Some(_obj) = Object::get(&self.content[self.offset..]) {
            let reparsed_result = Object::get(&self.content[self.offset..]);
            self.index += 1;
            return reparsed_result;
        }

        self.content.clear(); // no error
        self.index = 0;
        None
    }

How can I modify the original example to get it past the borrow checker?

It's a known limitation of the current borrow checker.

You can recreate the borrow inside the if block instead.

        if let Some(obj) = Object::get(&self.content[self.index..]) {
            // Your minimized code had no `offset` field...
            self.offset += 1; 
            // ...if you meant `index` you might need to adjust this accordingly.
            return Object::get(&self.content[self.index..]);
        }
1 Like

Like next_parse_twice? That would mean parsing needs to occur twice, which is not ideal. Thanks for the link, I'll see if I can subscribe to the progress somewhere.

The current solution is to use unsafe code to perform lifetime extension. E.g., add in a line before return Some(obj) like

// SAFETY: this is lifetime extension for a Polonius-style early return
let obj = unsafe { std::mem::transmute::<Object<'_>, Object<'_>>(obj) };

There's also crates like polonius-the-crab to encapsulate it for you.

I also like to add a polonius feature to test any of my crates using that sort of lifetime extension; I have tests that enable the nightly-only Polonius borrow checker and enable the polonius feature of my crates, and I have my crates not use unsafe lifetime extension when polonius is enabled (so that the borrow checker confirms the soundness of the code absent lifetime extension, implying that the unsafe lifetime extension is sound).

2 Likes

Can confirm, this mutes the borrow checker error. Good tip about the feature as well! Thanks!