[Idea] References are not borrows, their usages are (better reborrowing)

Summary: We should allow multiple mutable references to the same value within a function, and those references should not be considered borrows except for where the references are used.

Rust currently allows reborrowing in some limited cases. This is a step towards achieving this feature. Here is a minimal example that compiles:

let mut x = vec![1, 2, 3];
let y = &mut x;
let z: &mut Vec<i32> = y;

(copied from better documentation of reborrowing · Issue #788 · rust-lang/reference · GitHub)

We call this a "reborrow", meaning that y's borrow of x is relinquished to z and then reclaimed when z is done. But the semantics I am proposing here is a different model - y does not borrow x for its scope - it only borrows x when it is used. So z is free to borrow x in between usages of y.

Here is an example that does not work with reborrowing, but would compile with the proposed feature:

let mut x = 0;
let mx = &mut x; // here we take a reference, but it is not a borrow (yet)
let rx = &x; // here we take a reference, but it is not a borrow (yet)
dbg!(rx); // x is borrowed here
dbg!(mx); // x is borrowed here

The most motivating example is closures. Closures would be way more useful with this feature:

let mut acc: Vec<Result<Item, Error>> =  Vec::new();
// `add` contains a mutable reference to `acc`,
// but it does not borrow `acc` until it is used or invoked
let mut add = |v| acc.push(Ok(v));
if unlucky {
    acc.push(Err(e)); // `acc` is borrowed here (this doesn't compile today)
if cond1 {
    add(x1); // `acc` is borrowed here
    add(x2); // `acc` is borrowed here

(copied from Local closures borrowing ergonomics with edits)

Things get complicated when a reference can borrow one of multiple things. But I believe this is still possible.

let mut x = if condition { &mut a } else { &mut b };
if another_condition { x = &mut c }
dbg!(x); // `a` or `b` or `c` may be borrowed here

Okay now commence shooting holes in this.

Edit: Derived values

Given x, any values "derived" from x must be dropped before x may be mutably borrowed (through a &mut) again. A derived value is anything whose type contains a lifetime that is inherited from x or some projection of x (a reference, field, index, etc.). Perhaps these may be called "projected references".

let mut x: [Option<u32>; 1] = [Some(1)];
let mx = &mut x;

// this is okay because `mx` is not borrowing `x` yet,
// and `mx` references `x` itself, not an inner or derived value;
// there are no values derived from `x` in scope
x = [Some(2)];

// `inner` is "derived" from `x` since its lifetime is inherited from a reference to `x`
// so it must be dropped before a mutable borrow of `x` may occur
let inner: &u32 = mx[0].as_ref().unwrap();

// this is okay because `inner` is dropped
// (perhaps `drop()` is not required since `inner` is not a `Drop` type,
// but `inner` is certainly not usable after `x` is mutably borrowed below)
*mx = [None];

This looks very similar to Will the borrow checker ever be able to handle this simple loop with a mutable HashMap logic? - help - The Rust Programming Language Forum

The problem is that this approach is not always sound, for example:

use std::cell::RefCell;
let mut x = RefCell::new(Some(Box::new(1)));
let mut_ref = &mut x;
let use_mut_ref: &Box<i32> = mut_ref.get_mut().as_ref().unwrap(); // points to the contents of the Option
let immut_ref = &*mut_ref;
*immut_ref.borrow_mut() = None;
println!("{}", use_mut_ref); // there's no Box anymore, but use_mut_ref is still pointing to it

I think in general this is sound only when the second borrow (rx in your example) can reborrow from the first borrow (tx in your example). In the case of closures this is not possible due to their interfaces, but if you represented it as a struct containing its captures then it would be possible. But then this can just be replaced with a field access or a function call, so there's no need to change the borrow checker.

What if we say you can't borrow a value derived from x after x was mutably borrowed?

let mut x = RefCell::new(Some(Box::new(1)));
let mut_ref = &mut x; // references `x`
let use_mut_ref: &Box<i32> = mut_ref
    .get_mut() // references `x` because `get_mut` returns `&self` lifetime
    .as_ref() // references `x` because `as_ref` returns `&self` lifetime
    .unwrap(); // Option<&T> -> &T lifetime still references `x`
let immut_ref = &*mut_ref; // references `x`
*immut_ref.borrow_mut() = None; // mutable borrow of `x`
println!("{}", use_mut_ref); // error: borrowed a value derived from `x` after `x` was mutably borrowed

How do you define a "value derived from x"?

A projection (field access, slice index), a reference (to a projection), or a value with the same lifetime as such a reference, as determined by function signatures like fn(&'a self) -> &'a Something.

You should also count any function call since I can do any of the above inside it. And at this point how is this different than the current rules?

I'm still holding that the examples in the OP compile. Basically I can do this:

let (b, c) = (&mut a, &mut a);

but not this:

let (b, c) = (&mut a, &mut a);
let d = do_anything(b); // the return type has a lifetime matching the ref of `a`
do_anything(c); // error: cannot borrow `a`;
                // `d` is in scope, and is derived from a mutable borrow of `a`

but this is okay:

let (b, c) = (&mut a, &mut a);
let d = do_anything(b);

So in sum, you may alternate between different mutable references of a value, but any values derived from one reference must be dropped before using the other reference.

Edit: I addressed this conversation in the OP

Then I don't see how this would be helpful in the general case. Most of your examples require the first borrow to be able to be reborrowed when the second borrow is created, so why not do that? It's more explicit and easier to read. The only exception I can see are closures, but that's only because the captures are not accessible from the outside, if they were then this would also be solved without adding new rules.

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