RFC 0130: box-not-special

lang (typesystem | borrowck | allocation)

Summary

Remove special treatment of Box<T> from the borrow checker.

Motivation

Currently the Box<T> type is special-cased and converted to the old ~T internally. This is mostly invisible to the user, but it shows up in some places that give special treatment to Box<T>. This RFC is specifically concerned with the fact that the borrow checker has greater precision when dereferencing Box<T> vs other smart pointers that rely on the Deref traits. Unlike the other kinds of special treatment, we do not currently have a plan for how to extend this behavior to all smart pointer types, and hence we would like to remove it.

Here is an example that illustrates the extra precision afforded to Box<T> vs other types that implement the Deref traits. The following program, written using the Box type, compiles successfully:

struct Pair {
    a: uint,
    b: uint
}

fn example1(mut smaht: Box<Pair>) {
    let a = &mut smaht.a;
    let b = &mut smaht.b;
    ...
}

This program compiles because the type checker can see that (*smaht).a and (*smaht).b are always distinct paths. In contrast, if I use a smart pointer, I get compilation errors:

fn example2(cell: RefCell<Pair>) {
    let mut smaht: RefMut<Pair> = cell.borrow_mut();
    let a = &mut smaht.a;
    
    // Error: cannot borrow `smaht` as mutable more than once at a time
    let b = &mut smaht.b;
}

To see why this, consider the desugaring:

fn example2(smaht: RefCell<Pair>) {
    let mut smaht = smaht.borrow_mut();
    
    let tmp1: &mut Pair = smaht.deref_mut(); // borrows `smaht`
    let a = &mut tmp1.a;
    
    let tmp2: &mut Pair = smaht.deref_mut(); // borrows `smaht` again!
    let b = &mut tmp2.b;
}

It is a violation of the Rust type system to invoke deref_mut when the reference to a is valid and usable, since deref_mut requires &mut self, which in turn implies no alias to self or anything owned by self.

This desugaring suggests how the problem can be worked around in user code. The idea is to pull the result of the deref into a new temporary:

fn example3(smaht: RefCell<Pair>) {
    let mut smaht: RefMut<Pair> = smaht.borrow_mut();
    let temp: &mut Pair = &mut *smaht;
    let a = &mut temp.a;
    let b = &mut temp.b;
}

Detailed design

Removing this treatment from the borrow checker basically means changing the construction of loan paths for unique pointers.

I don't actually know how best to implement this in the borrow checker, particularly concerning the desire to keep the ability to move out of boxes and use them in patterns. This requires some investigation. The easiest and best way may be to "do it right" and is probably to handle derefs of Box<T> in a similar way to how overloaded derefs are handled, but somewhat differently to account for the possibility of moving out of them. Some investigation is needed.

Drawbacks

The borrow checker rules are that much more restrictive.

Alternatives

We have ruled out inconsistent behavior between Box and other smart pointer types. We considered a number of ways to extend the current treatment of box to other smart pointer types:

  1. Require compiler to introduce deref temporaries automatically where possible. This is plausible as a future extension but requires some thought to work through all cases. It may be surprising. Note that this would be a required optimization because if the optimization is not performed it affects what programs can successfully type check. (Naturally it is also observable.)

  2. Some sort of unsafe deref trait that acknowledges possibility of other pointers into the referent. Unappealing because the problem is not that bad as to require unsafety.

  3. Determining conditions (perhaps based on parametricity?) where it is provably safe to call deref. It is dubious and unknown if such conditions exist or what that even means. Rust also does not really enjoy parametricity properties due to presence of reflection and unsafe code.

Unresolved questions

Best implementation strategy.