Borrow checker false positive?

I angered the borrow checker with this seemingly-simple code that lazily populates a struct field:

struct Container {
    foo: Option<i32>,
}

impl Container {
    fn foo_mut(&mut self) -> &mut i32 {
        if let Some(foo) = &mut self.foo {
            return foo;
            // return self.foo.as_mut().unwrap();
        }

        self.foo = Some(0);
        self.foo.as_mut().unwrap()
    }
}
error[E0506]: cannot assign to `self.foo` because it is borrowed
  --> src/lib.rs:12:9
   |
6  |     fn foo_mut(&mut self) -> &mut i32 {
   |                - let's call the lifetime of this reference `'1`
7  |         if let Some(foo) = &mut self.foo {
   |                            ------------- borrow of `self.foo` occurs here
8  |             return foo;
   |                    --- returning this value requires that `self.foo` is borrowed for `'1`
...
12 |         self.foo = Some(0);
   |         ^^^^^^^^^^^^^^^^^^ assignment to borrowed `self.foo` occurs here

(Playground link)

You can fix it by using the commented return statement instead (or negating the if statement, etc…)

Correct me if I'm wrong, but it would be sound for this to compile, right? The line it's complaining about (line 12) doesn't run in the code path it's complaining about.

I believe this is the "problem case #3" from the non-lexical lifetimes RFC.

Unfortunately, although it is supposed to work, this is not currently supported:

We originally intended for NLL to accept examples like this: in the RFC, this was called Problem Case #3. However, we had to remove that support because it was simply killing compilation times, and there were also cases where it wasn’t as precise as we wanted.

If it's the same issue, a future version of the compiler may compile it. In the mean time, you just have to work around it. (Also if you're on nightly, try compiling it with -Zpolonius; that turns on an experimental borrow checking mode that should work, although you might not want to do it all the time.)

2 Likes

You're correct that it should be sound. Still, I'll walk through the borrow checker's "logic" here. It's a bit easier to understand if we add explicit lifetime annotations to the function.
With explicit lifetimes:

fn foo_mut<'a>(&'a mut self) -> &'a mut i32 {
        if let Some(foo) = &mut self.foo {
            return foo;
            // return self.foo.as_mut().unwrap();
        }

        self.foo = Some(0);
        self.foo.as_mut().unwrap()
    }

This basically says, given some lifetime 'a, the function will return a reference to an i32 that lives at least as long as 'a. Obviously, this means the lifetime has to last longer than the scope of the function, so 'a is a lifetime that encompasses this function (and more). Then when self.foo is mutably borrowed, foo is returned from that borrow. That means that the borrow of self.foo must live at least as long as the borrow of foo, which has the lifetime of 'a. This means that the borrow of self.foo must last to the end of the function, so when the assignment in line 12 occurs self.foo is still considered by the borrow checker to be mutably borrowed, producing the error.

The fix here is that you don't want to borrow the entirety of self.foo for the lifetime 'a, you just want to borrow the i32 if self.foo is Some. So instead of if let Some(foo) = &mut self.foo , you can use if let Some(ref mut foo) = self.foo and this will pass the borrow checker.

9 Likes

Spot on, thanks for the response!

Oh, interesting! I didn't think this would work. I'll have to think about it a while...

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.