Lifetime and borrow rules in an if else branch

I got trouble compile the following codes:

struct Bar {}
impl Bar {
    pub fn some_expensive_computation(&self) -> String {
        "42".to_string()
    }
}

struct Foo {
    m1: Option<i32>,
    m2: Bar,
}
impl Foo {
    pub fn f2 (&mut self) -> Option<&i32> {
        match &self.m1 {
            Some(f1) => Some(f1),
            None => None,
        }
    }
    pub fn f1(&mut self) -> Result<&i32, String> {
        if let Some(f1) = self.f2() {
            Ok(f1)
        } else {
            Err(self.m2.some_expensive_computation())
        }
    }
}

And the compiler report errors:

error[E0502]: cannot borrow `self.m2` as immutable because it is also borrowed as mutable
  --> src\main.rs:23:17
   |
19 |     pub fn f1(&mut self) -> Result<&i32, String> {
   |               - let's call the lifetime of this reference `'1`
20 |         if let Some(f1) = self.f2() {
   |                           ---- mutable borrow occurs here
21 |             Ok(f1)
   |             ------ returning this value requires that `*self` is borrowed for `'1`
22 |         } else {
23 |             Err(self.m2.some_expensive_computation())
   |                 ^^^^^^^ immutable borrow occurs here

I'm confused about this error. I think in this case only one branch will actually got run, I don't understand why the self in if branch will affect the self in else.

And if I wrote like this:

    pub fn f1(&mut self) -> Result<&i32, String> {
        let err = Err(self.m2.some_expensive_computation());
        if let Some(f1) = self.f2() {
            Ok(f1)
        } else {
            err
        }
    }
        

It will compile. But as the function name suggests, it's expensive and should not do that out of "else" branch.

I appreciate it if someone could help to tell if this is a limitation of borrow checker or there's indeed a potential safety problem.

If it's a limitation, I'm curious what's the lifetime/borrow rules for a if let Some(v) = f() {...} else {...}

It compiles fine if you change f2 to accept a &self rather than &mut self.

Is your example minimized from something more complex, where f2 does indeed need a &mut self receiver?

Yes, pub fn f2 (&mut self) -> Option<&i32> { is far more complicated in the real case.

This is an unfortunate limitation of the current borrow checker, which will be solved in the next one. In the meantime there are workarounds you can use, like the polonius-the-crab crate.

If the actual f2 code only accesses m1 then you can avoid borrowing all of Self in f2 by changing its signature. I realize this may not work for your actual use case, it's just something to consider, if you don't want to use Polonius.

    pub fn f2(m1: &mut Option<i32>) -> Option<&i32> {
        match m1 {
            Some(f1) => Some(f1),
            None => None,
        }
    }
    pub fn f1(&mut self) -> Result<&i32, String> {
        if let Some(f1) = Self::f2(&mut self.m1) {
            Ok(f1)
        } else {
            Err(self.m2.some_expensive_computation())
        }
    }

Is polonius-the-crab suitable to my case? I tried like this:

    pub fn f1(&mut self) -> Result<&i32, String> {
        polonius!(|self| -> &'polonius Result<&i32, String>{
            if let Some(f1) = self.f2() {
                polonius_return!(Ok(f1))
            }
        });
        Err(self.m2.some_expensive_computation())
    }

But the error message is even more confusing:

error: `mut` must be followed by a named binding
  --> src\main.rs:22:9
   |
22 | /         polonius!(|self| -> &'polonius Result<&i32, String>{
23 | |             if let Some(f1) = self.f2() {
24 | |                 polonius_return!(Ok(f1))
25 | |             }
26 | |         });
   | |__________^
   |
   = note: `mut` may be followed by `variable` and `variable @ pattern`
   = note: this error originates in the macro `polonius` (in Nightly builds, run with -Z macro-backtrace for more info)
help: remove the `mut` prefix
  --> C:\Users\lleo\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\polonius-the-crab-0.4.2\src\macros.rs:89:14
   |
89 -             |mut $var: &mut _| {
89 +             |$var: &mut _| {
   |

The macro doesn't seem to be happy with self as an identifier. This is ugly but I can get it working with this:

pub fn f1(mut slf: &mut Self) -> Result<&i32, String> {
    polonius!(|slf| -> Result<&'polonius i32, String> {
        if let Some(f1) = slf.f2() {
            polonius_return!(Ok(f1))
        }
    });
    Err(slf.m2.some_expensive_computation())
}
1 Like

To preserve f1 as a method, you could have it forward the call to f1_workaround -- the function you posted that works by using &mut Self.

1 Like

Thanks, it works. I'm curious how safe it is. Can I consider that as long as the code compiles with polonius-the-crab, it is safe without any dangerous?

I trust that crate author with unsafe, whatever that's worth. They're also a Rust contributer, e.g. If you have any concerns they'd probably be responsive.

1 Like

It is a small library with only one line of unsafe, and the soundness of the implementation is covered in the docs. Albeit the explanation is more handwavey than formal, and it relies on higher-order lifetime tricks, but it carries a lot of weight that a RustSec advisory has never been filed against it despite its popularity.

1 Like

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.