E0499 and function signatures extending mutable borrow lifetimes

I've recently come across Ben Hoyt's post comparing counting words in different programming languages. Since text processing is what I'm most familiar with and what I hope to start using Rust more for in the near future, I decided to spend some time with the various Rust solutions kindly provided by none other than Andrew Gallant (learn from the best, right?).

Along the way, I wasn't exactly sure why Cell is needed in this function and why switching to a mutable ref leads to E0499 ("cannot borrow as mutable more than once at a time"). I understood why Cell helps with the issue, but I was hazy on the exact mechanism that causes it.

Hoping that getting to the bottom of it would help me better understand borrowing and lifetimes, I originally wanted to post about this under the help tag. But having spent a few evenings on it (and (re)learnt about reborrowing and a bunch of other things), I think I've figured out some answers. So I cleaned up my code experiments, added some explanatory prose for future me (who'll inevitably forget all of this) and I'm submitting it instead under code review, as I would sincerely appreciate any feedback/comments on:

  • the (in)correctness and phrasing of my explanations (maybe some of them are correct in spirit, but phrased in a way that uses terms too informally -- I remember using "blanket impl" wrong in the past, that sort of thing)
  • the idiomaticity and performance characteristics of the three listed ways to avoid E0499 in this context (and possibly any other ways that come to mind)

Here's the gist, as well as a playground link for convenience. Thank you to anyone who has read so far and is considering providing some feedback!

    // Using distinct variable names for clarity (though you could keep
    // reassigning the same binding):
    let mut_x1 = &mut x;
    let mut_x2 = bind_and_ret(mut_x1, &mut y);
    let mut_x3 = bind_and_ret(mut_x2, &mut y);

    // What happens here (I think): when mut_x1 is passed into the
    // function, it's reborrowed. That reborrow's lifetime is then bound
    // to the lifetime of y, as previously. But unlike previously, we
    // then return the reborrow and store it in mut_x2. That means that
    // when we next need to call the function and provide a mutable
    // borrow of x, we don't need to borrow again (which would be an
    // error, since the first mutable borrow is still active): we can
    // just use the active borrow, which we happen to have a handle onto
    // (mut_x2), unlike previously.

Rust doesn't check that you're returning what you passed in. Rather, because you never use mut_x1 again, the variable is dead after the call, which means the borrow is dead. The same thing happens here:

let mut mut_x = &mut x;
loop {
    mut_x = bind_and_ret(mut_x, &mut y);

The assignment of a new value to the variable "kills" the old value. No single value of mut_x "completes a loop" -- it starts at the assignment and then gets consumed in the function call, before the next assignment. (Being assigned over isn't a use.)

Here's some discussion from the NLL RFC, which you may find informative generally. It's also covered in Niko's introduction to Polonius (the linked section and the one following); that post is somewhat dense with information and you may need to read the article to understand the full context of those two sections.

1 Like

Thank you very much for the clarification! And for the links as well, I've added them to my reading list.

Rust doesn't check that you're returning what you passed in.

I had a hunch this interpretation was assuming more magic on the part of the Rust compiler than there already is :slight_smile: The contract is in the function's signature, and that simply says that a &'a mut X goes in and another comes out. There's no way to specify that these should be one and the same, so there's no way for Rust to know that and rely on it, right?

Rather, because you never use mut_x1 again, the variable is dead after the call, which means the borrow is dead. [...] The assignment of a new value to the variable "kills" the old value.

How come I can't do this then?

let mut x = X(0);
let mut y = None;
let mut_x1 = &mut x;
bind_lifetimes_together(mut_x1, &mut y);
let mut_x2 = &mut x;
bind_lifetimes_together(mut_x2, &mut y);
let mut_x3 = &mut x;

Is it because, while the situation w.r.t. the liveness of mut_x1 is the same (it's used once and never again), there's a difference in how the subsequent mutable borrows are acquired? As in, doing &mut x a second time is illegal, but obtaining that mutable borrow as a function's return value is legal?

In which case, it sounds like not only does Rust not keep track of the fact that the borrow points to the same object, it's precisely because Rust doesn't do so that this is even allowed at all? Because if Rust had this information, then it should consider it as equivalent to doing another &mut x, which isn't allowed.

And what keeps this from breaking (= what keeps me from using the multiple mutable borrows in mut_x1, mut_x2 and mut_x3 at leisure) is then a different mechanism: not E0499 (prohibiting multiple mutable borrows), but E0506 (prohibiting assignment to a borrowed value). As in:

let mut_x1 = &mut x;
let mut_x2 = bind_and_ret(mut_x1, &mut y);
let mut_x3 = bind_and_ret(mut_x2, &mut y);
mut_x2.0 = 42;

Which fails to compile with the following error:

error[E0506]: cannot assign to `mut_x2.0` because it is borrowed
   --> multi_borrow.rs:157:5
156 |     let mut_x3 = bind_and_ret(mut_x2, &mut y);
    |                               ------ borrow of `mut_x2.0` occurs here
157 |     mut_x2.0 = 43;
    |     ^^^^^^^^^^^^^
    |     |
    |     assignment to borrowed `mut_x2.0` occurs here
    |     borrow later used here

This makes sure that I can only ever use the last borrow in the chain (mut_x3 here). Sound more or less right?

My first reply was definitely not the full picture. I'm still working out what the full picture is, too. :slight_smile:

Right. It could conceivably do some sort of analysis based on the body, but it's a conscious decision not too -- because the signature is the contract. You can replace your function body with todo!(), or unsafely fabricate a &mut from a pointer or static mut, and it will behave the same.

I believe this is right. Although as far as I know this isn't well documented, a function that takes an exclusive borrow and returns something "transfers" that exclusive borrow to the returned value. So it's possible to still have access through that shared value.

After playing with this awhile, I believe what is happening is similar to the transfer above. Only in this case, it's being transferred to the y via &mut y, hence the error about y being the "use of &mut x". (Using yy in the link allows y to be dropped before the second call to bind_lifetimes_together.) This makes perfect sense as you may be "returning by mutable reference".

In fact, for bind_and_ret it goes into both. The compiler can't know if you transferred the exclusivity to the return or y (or neither), so it has to restrict things as if it is either.

I think it's still fine if it it knows. Being the same object is one of the possibilities, after all. The implication therefore is that it's not equivalent to doing another &mut x. It's the same borrow, not a second borrow. That's what makes it ok.

I'm going and find time to take a second pass at your gist at some point. I'd love to see what others in the forum have to say, too.

1 Like