Reborrow for exclusive code paths

Hi, I am trying to understand why the borrow checker prevents me to write this code. While I understand that the borrowed value is still in the scope, si there anyway to "unborrow" it? The if and else code path are exclusive so wouldn't it be possible to get this code to compile? I tried dropping the value manually before a re borrow but it does not work.

Is this a case of case of conditional control flow across functions?

Here is a simplified version of the code:

pub struct Wrapper(u8);

impl Wrapper {
    pub fn new() -> Self {
        Wrapper(0)
    }
    
    pub fn get(&mut self) -> &u8 {
        self.0 += 1;
        &self.0
    }
    
    pub fn write(&'_ mut self) -> Result<&u8, ()>{
        let val = self.get();
        if val > &1{
            /// Val is borrowed, thus self is, but can I unborrow it?
            // drop(val); // Doesn't help
            Ok(self.get())
        } else {
            Ok(val)
        }
    }
}
   Compiling playground v0.0.1 (/playground)
error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/main.rs:17:16
   |
13 |     pub fn write(&'_ mut self) -> Result<&u8, ()>{
   |                  - let's call the lifetime of this reference `'1`
14 |         let val = self.get();
   |                   ---------- first mutable borrow occurs here
...
17 |             Ok(self.get())
   |                ^^^^^^^^^^ second mutable borrow occurs here
18 |         } else {
19 |             Ok(val)
   |             ------- returning this value requires that `*self` is borrowed for `'1`

For more information about this error, try `rustc --explain E0499`.
error: could not compile `playground` due to previous erro

Playground as reference

Yes -- here's the issue. The workaround is usually to recreate the borrow in the arm where you're trying to return it (the else branch). E.g. if you had a function that returns whatever write returned last time.

Dropping a reference never helps as that's just another code point it has to be valid and alive for. (Dropping something with a destructor can sometimes help as it changes where the destructor is ran, but that doesn't apply here.) There's no safe way to force an inferred lifetime to change.

2 Likes

I try to modify the method write as the following:

    pub fn write(&'_ mut self) -> Result<&u8, ()>{
        let val = self.get();
        if val <= &1{
            return Ok(val)
        }
        Ok(self.get())
    }

However, it still does not work. The reason is whatever we place the value in any position in the function body, the lifetime for the reference val should be alive for '_ in the signature. The reborrow in the second self.get() therefore overlaps with val in '_. Is this correct understanding? Which has the same reason as this simplified example

struct Wrapper(u8);
fn main(){
   'a:{
         let mut w = Wrapper(0u8);
        '_:{
             let val:&'_ u8 = &'_ w.0;   // ----------- lifetime of val starts here
             'rb:{
               let reborrow = & mut w;  // overlaps with val in `'_`
           }
             println!("{val}");
       }  // ------------------------------------------- lifetime of val ends here
    }
}

Oops, I meant if you had a function that returns whatever get returned last time, in case that was what you were going for.


As for your code, the two cases aren't the same. The first one is accepted by flow-sensitive analysis, just like the OP is. Either only one (re-)borrow of self happens and val is returned, or the first borrow can end after the if conditional.

In the second code block, let reborrow must kill val's borrow, and there is no branching. Flow-sensitive analysis makes no difference.

My understanding of this case is, the borrow val is ended once we enter the branch of the if. The exception is that the compiler does give special treatment to the if, in the sense, the second borrow occurred in Ok(self.get_current()) should be alive for '_ that would overlap with the lifetime of val that has a smaller lifetime.

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.