Why does this code not compile?

I've come across a lifetime issue that should compile according to my internal model of how lifetimes work.
I tried to minimize the code so that it's hopefully easier to understand.

enum Enum {
    A(usize),
    B, 
//  ^^ 1
}

impl Enum {
    fn get<'this>(&'this mut self) -> Option<&'this usize> {
        //               ^^^ 2
        if let Self::A(val) = self {
            if val.is_power_of_two() {
        //  ^^^^^^^^^^^^^^^^^^^^^^^^^^---------+
                return Some(val); //           +-- 3
            }// ^^^^^^^^^^^^^^^^^ 4            |
        //  ^----------------------------------+
        }

        match self {
            Self::A(_) => None, 
        //  ^^^^^^^^^^^^^^^^^^^ 5
            _ => None,
        }
    }
}

This code (playground) fails with the error message:

error[E0503]: cannot use `*self` because it was mutably borrowed
  --> src/main.rs:18:15
   |
8  |     fn get<'this>(&'this mut self) -> Option<&'this usize> {
   |            ----- lifetime `'this` defined here
9  |         //               ^^^ 2
10 |         if let Self::A(val) = self {
   |                        --- `self.0` is borrowed here
...
13 |                 return Some(val); //           +-- 3
   |                        --------- returning this value requires that `self.0` is borrowed for `'this`
...
18 |         match self {
   |               ^^^^ use of borrowed `self.0`

My intuition here is that the borrow created in the if let should not extend beyond it, since it is not used beyond it (except if the function returns, but that should be independent of the code path in which it doesn't return).

If I remove any of the code section 1-5, the code compiles again.

Any insight into why this code cannot compile is appreciated.

While my main goal asking this is to understand why this doesn't work, some hints on how the code could be fixed while keeping the semantics (the idea is that self should be updated in the match, before returning None, but that made the error message more complicated) are also appreciated.

It's a known shortcoming of the current borrow checker; effectively the returned borrow lasts for the rest of the function body.

2 Likes

Regarding how to fix this issue: The commonly working fix is to create a new borrow of self inside the inner if of the early return that used to make our borrow checker assume a too extensive borrow of self:

Instead of

return Some(val);

it’s

return Some(val.get_a().unwrap())

with an accessor like

    // private helper
    fn get_a(&self) -> Option<&usize> {
        match self {
            Enum::A(r) => Some(r),
            _ => None,
        }
    }

Rust Playground

which you could of course also inline. Or you could use the crate assert_matches::assert_matches - Rust

return Some(assert_matches!(self, Enum::A(val) => val));

Rust Explorer


In the case of this particular code, you could also just re-factor the control flow to avoid the multi-step if let / match and use a single match; in particular if nothing happens in-between the two; but that might be an artifact of your minimal example.

match self {
    Self::A(val) if val.is_power_of_two() => Some(val),
    Self::A(_) => None,
    _ => None,
}

For more complicated situations, there are further possible workarounds, see here for more information ^^

3 Likes

Thank you both for your answers, that clears things up for me. I guess I've always heard about limitations in the borrow checker, but I've never encountered them before.