Restrict pointers

24 October 2012

I am considering whether we should add a way to borrow something but retain uniqueness. This would address a shortcoming of the borrowing system that has been bothering me for some time, and it would enable a few patterns that are difficult or awkward today.

The Problem

I described the problem in this paper review I wrote, but I will repeat it here, because it’s relevant, and perhaps people don’t read and remember every single word that I write. In our system, a ~T type is always owned. So if you write:

fn foo(bar: ~Map<K,V>) {
    bar.insert(k, v);
    for bar.each |k, v| {
        ...                 // (bar is immutable here)
    }
}

This is a function which takes in a map, modifies it some, iterates over it, and then (implicitly) frees it. The same function could, if it wanted to, continue by sending bar to another task, or doing whatever. But, of course, it doesn’t here, and perhaps what I really wanted was just to do some work on behalf of the caller foo().

Typically, the way that one expresses the idea that you want to take a Map but not consume it is to use a borrowed pointer:

fn foo(bar: &Map<K,V>) {
    bar.insert(k, v);            // (Error)
    for bar.each |k, v| {
        ...                 // (bar is immutable here)
    }
}

Unfortunately, as indicated in the comments, this code will not compile. This is because when you borrow a freezable data structure like a map, you must select if it is immutable (&Map<K,V>) or mutable (&mut Map<K,V>). But this example uses insert(), which requires mutability, and iteration, which requires immutability (modifying the map while iterating is a no-no).

The only static way to solve this problem is to move the map into the function foo() and then return it back to the caller when foo() has finished:

fn foo(mut bar: ~Map<K,V>) -> ~Map<K,V> {
    bar.insert(k, v);
    for bar.each |k, v| {
        ...                 // (bar is immutable here)
    }
    return map;
}

This is a bit annoying, but it works. It’s similar to other affine systems like Alms.

A solution

Currently borrowed pointers have the form & mq where mq is a mutability qualifier, like mut or const (the default is immutable). I would propose to add a fourth qualifier, called restrict (the keyword is borrowed from C99). An &restrict pointer is still a borrowed pointer, so it does not convey ownership, but it is unique, meaning that it is the only (live) pointer that points at that particular memory. It would also imply that this memory is mutable. Unlike other & pointers, &restrict pointers cannot be copied (they are affine), though they can be re-borrowed temporarily, just like owned pointers.

Note the distinction between &~T and &restrict T. The former is a borrowed pointer to an owned pointer to a T (double indirection). The latter is a borrowed pointer to a T (single indirection) that is guaranteed to be unique.

So, we could rewrite our troublesome function foo() as follows:

fn foo(bar: &restrict Map<K,V>) {
    bar.insert(k, v);
    for bar.each |k, v| {
        ...
    }
}

Now it is perfectly legal to insert into the map and iterate over it. The borrow checker knows that there are no aliases of bar, so it will permit it to be treated as both mutable and immutable, as long as those regions do not overlap. You would call foo like this foo(&restrict map).

The reason that restrict implies mutability is that, if the memory is immutable, you might as well just do an &T pointer. There is no need to have a unique pointer to immutable memory, since aliases cannot interfere with one another. We could force you to say &restrict mut but that seems rather verbose!

How can you enforce this?

The existing borrow checker mechanisms can easily be enhanced to support a restricted borrow. The main difference between this and other borrows is that a restricted borrow is not compatible with any other borrows; moreover, a restricted borrow prohibts reads, writes, and moves.

Neat. What else can you do with it?

A lot of parallel algorithms require dividing up an array and processing each of its parts in parallel. For this to be safe, you want to know that you have the only copy of the array. I was thinking that one could write a function like:

fn subdivide<T>(values: &restrict [T]) -> ~[&restrict [T]]

subdivide() would take a unique slice and divide it up into a set of other slices, each of which are guaranteed to be disjoint from one another (and thus safe to process in parallel tasks). You could imagine a similar function:

 fn partition<T>(values: &restrict [T], mid: uint) -> (&restrict [T], &restrict [T])

This would divide an array about some mid point, which is common in divide-and-conquer algorithms.

There would still be plenty of work to be done before you could make good use of these functions though. For example, we’d need to build up some parallel constructs that fork and join together. Such functions would be permitted access to & pointers, so long as they did not mutate them (as discussed in this post on purity; the current Rust pure keyword would be a good match here, but maybe we can find other nice solutions too).

What’s weird here?

One odd thing is that, as I am envisioning it, passing an &restrict to another function consumes the original &restrict, just as with any other affine value. That’s necessary for subdivide() and so forth to work. Unfortunately, that means that you may have to re-borrow an &restrict pointer to make use of a helper.

So, to make this more concrete, two functions like this would not compile:

fn foo(x: &restrict Map<K, V>, ...) {
    bar(x, ...);
    ...
    x.insert(...); // Error: x was given away!
}

fn bar(x: &restrict Map<K, V>, ...) {
    ...
}

The problem is that calling bar() gave away the x pointer. You’d have to write foo() like so:

fn foo(x: &restrict Map<K, V>, ...) {
    bar(&restrict *x, ...);
    ...
    x.insert(...); // OK!
}

Basically this just “re-borrowed” x.

Can you compare this to fractional permissions?

Ok, so you probably didn’t ask for this comparison really. But for those of you who are familiar with fractional permissions, I think a useful metaphor to explain borrowing goes something like this: first, before a value is borrowed, it has a permission of 1.0. When the value is borrowed with a non-restrict pointer, its permission drops to something between 0 and 1 exclusive. When the value is borrwed with a restrict pointer, the permission drops to 0.

The analogy is not exact. Our borrowing system does not track and account for permissions but rather simply remembers the largest dynamic extent of any borrowed pointer and assumes that 1.0 is returned at the end of that. This is less flexible in theory—you can’t stash a permission into a heap structure, for example—but it’s often more flexible in practice, since most of the actual fractional permissions since I’ve seen paper over fractional permissions with a borrowing-like system, and they usually don’t have the full power of regions.

Another difference is that our permissions can’t actually be summarized as number between 0 and 1. In a traditional fractional permission system, mutability and moves are both tied to having the full 1.0 permission, but having less than 1.0 means you are limited to reads and cannot move. But for us there is the possibility that you borrow with &mut, which permits writes but not move.