2

I was working on a big file but this is a small toy example that causes the same issue. Sorry if the example itself makes no sense.

#![feature(nll)]
struct S(i32);

impl S {
    fn foo(&mut self) -> Option<&i32> {
        if let Some(val) = self.bar() {
            return Some(val);
        }
        let y = &mut self.0;
        None
    }

    fn bar(&mut self) -> Option<&i32> {
        None
    }
}

fn main() {
    S(0).foo();
}

This doesn't pass the borrow checker:

error[E0499]: cannot borrow `self.0` as mutable more than once at a time
 --> test.rs:9:17
  |
6 |         if let Some(val) = self.bar() {
  |                            ---- first mutable borrow occurs here
...
9 |         let y = &mut self.0;
  |                 ^^^^^^^^^^^ second mutable borrow occurs here
  |
note: first borrowed value must be valid for the anonymous lifetime #1 defined on the method body at 5:5...
 --> test.rs:5:5
  |
5 | /     fn foo(&mut self) -> Option<&i32> {
6 | |         if let Some(val) = self.bar() {
7 | |             return Some(val);
8 | |         }
9 | |         let y = &mut self.0;
10| |         None
11| |     }
  | |_____^

Shouldn't this be valid (even without #![feature(nll)]) since it is returning in the if let block? It's worth noting that if I change the if let block to the following, it compiles fine

if self.bar().is_some() {
    return self.bar();                                                                                      
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Mohammed
  • 313
  • 2
  • 9

1 Answers1

3

Let's look at the lifetimes in detail here. The function foo() is desugared to

fn foo<'a>(&'a mut self) -> Option<&'a i32>

i.e. the returned value lives at most as long as self; similarly for bar().

In foo(), the line

if let Some(val) = self.bar() {

creates a borrow of self that lives for some lifetime 'b, and the returned reference val also has this lifetime 'b. Since you then return Some(val), the lifetime 'b must outlive the lifetime 'a of the self parameter to foo(), which is definitely longer than the runtime of foo(). This means you can't borrow self again at any later point in foo().

I think what is surprising in this example is that the borrow of self even happens if bar() returns None. Intuitively, we feel that no reference is returned in this case, so we don't need a borrow. However, lifetimes in Rust are checked by the type checker, and the type checker does not understand the meaning of different values of the type. The value returned by bar() has the type Option<&'b i32> regardless of whether it returns None or Some, and the lifetime 'b hast to be at least as long as 'a – given the constraints, there is no other solution, so the borrow checker has to refuse this.

With non-lexical lifetimes, the compiler can introduce more flexible lifetimes that are not bound to lexical scopes and can overlap in ways that wasn't possible before. However, if there simply is no lifetime that satisfies all constraints, NLLs won't help you.

The last code snippet you give is quite different. Let's add lifetimes:

if self.bar<'b>().is_some() {
    return self.bar<'c>();                                                                                      
}

Now we call bar() twice, and each of these calls can have a different lifetime. Only the lifetime 'c needs to outlive 'a now, but the lifetime 'b only needs to be long enough to call is_some() on the result. The borrow with the lifetime 'c only happens when the branch is taken, and no conflicts occur.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • But the branch that keeps the reference returns! Otherwise why does the case with simple if statement work? it makes the same borrows. – Mohammed Oct 28 '18 at 18:52
  • @Mohammed There is only one lifetime `'b`, and the borrow checker tries to find a suitable value for it. There aren't two different lifetimes for each branch of the `if`. I'll add a comment to the answer why the other version works. – Sven Marnach Oct 28 '18 at 18:58
  • Is there a safe way to rewrite it without borrowing twice? The code is logically safe as I see, correct me if I'm wrong – Mohammed Oct 28 '18 at 20:32
  • 1
    *NLLs won't help you* — they will, when the next iteration of NLL is enabled. See the linked duplicate for more information. – Shepmaster Oct 28 '18 at 20:41
  • @Mohammed I think the code is safe as it stands. I can't think of an easy solution – I guess I would need to understand more of the context to make a suggestion how to redesign this. – Sven Marnach Oct 29 '18 at 08:19