NLL and early return not allowed

My naive understanding of borrow checking with NLL is that it could analyze the code flow graph rather then being tied to the lexical scope. I expected (wrongly) the follow code to be correct under that model, since we have branching control flow.

fn inner(i: &mut i32) -> Result<(), &i32> {
    if i.is_positive() {
        Err(i)
    } else {
        Ok(())
    }
}

fn outer(i: &mut i32) -> Result<(), &i32> {
    inner(i)?;
    inner(i)
}

fn main() {
    outer(&mut 1).unwrap();
}

However this fails to type check:

error[E0499]: cannot borrow `*i` as mutable more than once at a time
  --> src/main.rs:11:11
   |
9  | fn outer(i: &mut i32) -> Result<(), &i32> {
   |             - let's call the lifetime of this reference `'1`
10 |     inner(i)?;
   |     ---------
   |     |     |
   |     |     first mutable borrow occurs here
   |     returning this value requires that `*i` is borrowed for `'1`
11 |     inner(i)
   |           ^ second mutable borrow occurs here

I understand what the error is saying. It is saying that i must be borrowed for the entire function, but that does not work with an early return, since the lifetime would have two sizes. However this seems like it should work with lifetimes analysis based on control flow.

Is this just limitation of the borrow checker, or is this preventing fundamental unsoundness? If it is a borrow checker limitation, why does NLL control-flow analysis not work with this?

1 Like

Unfortunately, the borrow checker has an edge case where it is too strict, which you hit when you try to return references early. The main work-around is to make sure that the returned reference is not even created until you're in the branch where you know for sure that you will return it.

A future compiler would hopefully accept this code.

2 Likes

This is a limitation of the current borrow checker. Polonius should be able to handle this case eventually but that's still a long way off. Refactoring your code to avoid taking a reference until you're about to return is the best option. If you can't do that, and are really desperate, circumventing the borrow checker with unsafe can be done, but be extremely careful with how you handle the unbounded lifetimes and document what you are doing thoroughly.

1 Like

The polonius-the-crab crate may also be useful. It uses unsafe code internally, but this code can be written without unsafe of you use polonius, so it should be sound.

2 Likes

This is the issue and as others said, it was intended to, but the analysis for that case is to heavy and perhaps imprecise under the current implementation.

2 Likes