Reborrow error despite variable being unused

struct Str {
    vec: Vec
}

impl Str {
    pub fn change(&mut self) -> Option<&u32> {
        if let Some(a) = self.vec.get() { // first using
            return Some(a);
        }
        self.vec.change(); // second reborrowing error
        None
    }
}

pub struct Vec {}
impl Vec {
    pub fn get(&self) -> Option<&u32> {
        None
    }
    pub fn change(&mut self) {
    }
}

run in playground

Here, output the error:

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

I‘ve thought about it a lot, but I am still clueless.

this is a know limitation of current borrow checker, search keyword polonius for full explanation. the short answer is, when a borrowed value is conditional returned, the borrow checker thinks the value must be alive across the entire function body, and extends the borrow lifetime accordingly.

it requires unsafe to work around it, but it is recommended to use this crate instead of writing the unsafe yourself:

Wow, the crate is great!
But how do we write it if we use unsafe code?

use a raw pointer to "cheat" the lifetime analysis.

pub fn change(&mut self) -> Option<&u32> {
    // save `self` as a raw pointer for later use
    // raw pointers don't have lifetime associated with them
    let selfp = self as *mut Self;
    if let Some(a) = self.vec.get() {
        return Some(a);
    }
    // be careful when dereferencing the raw pointer
    // especially for *exclusive* reborrow, a.k.a. `mut` reference
    // must not break the aliasing rule
    unsafe {
        (*selfp).vec.change();
    }
    None
}

in this example, it may seem simple enough, but always use miri to check you didn't break the rule.

UPDATE:

there's a better solution than my method, see the follow up by @robofinch:

Got it. When can we expect a version for this in NLL?

I prefer to do lifetime extension within the branch before the return

Well, the answer to your literal question is never, AFAIK; I don't think the new Polonius borrow checker will be called NLL. Personally, I consider NLL to refer to the current pre-Polonius borrow checker.

Here's the tracking issue: Stabilize and model Polonius Alpha · Issue #118 · rust-lang/rust-project-goals · GitHub

I didn't follow the recent progress, last time I checked was this video from EuroRust 2024:

both will do, I just think the code would be more concise and readable if I put it in the else branch for this particular example.

if I were to put it in the early return path, I either add an addition nested scope like this:

    let selfp = self as *mut Self as *const Self;
    unsafe {
        if let Some(a) = (*selfp).vec.get() {
            return Some(a);
        }
    }
    ...

or I need to spell some magic sigils which might be confusing for some people like this:

    let selfp = self as *mut Self as *const Self;
    if let Some(a) = unsafe{&*selfp}.vec.get() {
        return Some(a);
    }
    ...

unless you have a different method in mind that's different than mine.

These docs are helpful.

Could you show your code for it?

I constrain the unsafe to within the if-let, and to check my work I usually add a polonius feature that fails to compile unless (and should compile if) the Polonius borrow checker is enabled:

    if let Some(a) = self.vec.get() {
        // SAFETY: This is only a lifetime cast. The aliasing rules are satisfied,
        // as proven by how this compiles safely under Polonius.
        #[cfg(not(feature = "polonius")]
        let a = unsafe {
            transmute::<&u32, &u32>(a)
        };
        return Some(a);
    }

this is much cleaner than my method, thanks for sharing.

Thanks for all your answers, I understand perfectly.