Note: Although most of these permissions are supported, only mutability and coercion are enforced.
Every reference's type specifies a permission which controls what may, and may not, be done with that reference. Permissions affect such capabilities as the reference's right to view (or modify) the value it points to, create multiple copies of that reference, and use locks or atomics for synchronizing access to the reference's value.
The primary purpose of permissions is to improve program safety by preventing data races, where multiple concurrent threads step on each other when trying to access the same value. Each permission uses a different strategy to make it impossible for multiple threads to perform lifetime-overlapping atomic transactions on the same value.
Prudent use of permissions can also help make code easier to understand and improve execution performance.
There are two kinds of permissions:
- Static. The safety guarantees for static permissions are enforced entirely by the compiler. This means there is no runtime overhead to use static permissions. Static permissions facilitate permission transitions, as described later.
- Lock. The safety guarantees for locked permissions are enforced by a combination of compiler and runtime (lock) mechanisms. To access the value pointed at by a locked reference, one must first acquire the lock. This is done by borrowing a reference. Only region-managed references can specify a lock permission.
This page focuses only on static permissions. Lock permissions are discussed later.
List of Permissions
There are only five static permissions:
- uni
- The uni (unique) permission is quite versatile.
It may be used to view or mutate the value it points to.
It may hop safely from thread to thread without any runtime penalty,
making it the fastest way to move mutable data from one thread to another.
The name, "unique", signals its one restriction: there can only be one active reference to its value at a time. Because of this, its use is governed by move semantics. The reference may be moved to another variable, function, or thread, but doing so makes it inaccessible at its former home.
uni is ideal when we start off only needing one reference to a value (or isolated graph). It allows us to safely move a value's reference around between scopes and threads. Later we can transition it to shared mutable or immutable. Performance optimization benefits may also result from knowing the reference is unique.
- imm
- The imm (immutable) permission declares that a reference's value will never change. Multiple references may point to the same immutable value, even across multiple threads. An imm reference may view its value, but not alter it.
- mut
- The mut (mutable) permission
allows multiple references to freely access or change the same value at any time.
However, all references to the same value are restricted to a single thread.
A mut array reference may not re-size its array. Similarly, a mut reference may not alter a variant value.
- ro
- A ro (readonly) reference may read, but not modify, the value it points to.
This may sound like imm, but it is not the same.
imm guarantees that no other mutable reference to the same object exists,
making it safe to share between threads.
ro makes no such guarantee.
Therefore, ro references cannot be safely shared with another thread.
The primary use for ro rests in how it allows a function to take in a reference of nearly any permission, simply by promising not to change the value the reference points at. This polymorphic flexibility is why a borrowed reference's default permission is ro, if no permission is explicitly specified.
- opaq
- An opaq (opaque) reference may never be used to read or modify the value it points to. This restriction is useful for references to functions or opaque values, where it makes no sense to access the value they point to.
Variable vs. Reference Permissions
Initially, it may feel confusing, distinguishing the role of a permission on a reference vs. on a variable, particularly if the variable holds a reference. Quite simply, a variable's permission governs whether you can give the variable a new value. A reference's permission governs what you can do with the reference. These are separable concerns.
To illustrate this, consider these variations:
imm ref1 = &imm x imm ref2 = &mut x mut ref3 = &imm x mut ref4 = &mut x
Because of the mut variable permission, ref3 and ref4 may be changed to hold a different reference. ref1 and ref2 cannot.
ref1 = &imm x // ** ILLEGAL! the ref1 variable cannot be changed ** ref2 = &mut x // ** ILLEGAL! the ref2 variable cannot be changed ** ref3 = &imm x // ok ref4 = &mut x // ok
In contrast, because of the mut reference permission, the references in ref2 and ref4 may be changed to point to a different value. The values pointed to by ref1 and ref3 cannot be altered.
*ref1 = 5 // ** ILLEGAL! The value pointed at by ref1 cannot be changed ** *ref2 = 5 // ok *ref3 = 5 // ** ILLEGAL! The value pointed at by ref3 cannot be changed ** *ref4 = 5 // ok
Permission Transitions
A new copy of a reference typically has the same permission as the original reference, which is often necessary to preserve the reference's safety guarantees. In certain cases, however, we may safely create a reference copy carrying a different permission.
To 'ro'
It is safe to borrow a ro reference from a reference of any permission other than opaq. This quality is what makes it attractive for functions to polymorphically accept ro references to any values they do not intend to modify.
From 'uni'
At some point in the lifetime of a uni reference's value, one may need to transition to allowing multiple, shared references to that value. On a temporary basis (for some lexical lifetime), this transition may be accomplished by using borrowed references.
Using borrowed references does make it possible to temporarily transition a uni reference to some shared reference permission. During the lifetime of a borrowed reference, the source for the borrowed reference may not be used. However, once the lifetime of the borrowed reference expires, the original source becomes usable again:
imm uniref = +rc 5 { imm bref1 &mut i32 = uniref // Coerce owning reference to a 'mut' borrowed reference imm bref2 = ref2 // Share with another borrowed reference *bref2 = *bref1 + 1 *uniref = 6 // Error, uniref is unusable during scope of borrowed 'bref1' } *uniref = 7 // Allowed, since borrowed references have expired
It is also possible to permanently transition a uni reference to references holding shared permissions, such as mut or imm. This is accomplished by moving the `uni` reference value to a reference that holds a shared permission. The original uni reference is consumed (because of move semantics), leaving us with a new sharable reference, from which copies can be made.
imm ref = +rc 5 // Allocate new value, return 'uni' reference imm newref1 &mut i32 = ref // Move reference to newref with 'mut' permission imm newref2 &mut i32 = newref1 // Two 'mut' references to the same object *newref2 = *newref1 // Either reference may be used (but not ref any longer)
The transition of an region-managed reference from uni to a shared permission reference is normally a one way trip. Once this transition has happened, you cannot safely transition a mut reference back to a uni reference (or even to an imm reference). The reference is now "frozen" to the restrictions of its new permission: As mut, it can never be shared across threads. As imm, it can never be altered.
Permission Recovery
Permission transitions are not actually always a one-way trip. Under certain circumstances, it is possible to safely recover "lost" permissions.
- Borrowed reference recovery. Borrowing a reference temporarily transitions to a new permission. When the borrowed reference expires, the original permission on the source reference is restored. So, a uni can be borrowed as mut or imm. When the borrowed references expire, you once again again have the original uni reference. Similarly, one can borrow as ro and get back the original reference's permission later.
- Pure method calls Sometimes it is polymorphically convenient to be able to call a method on an object where we have a uni reference, and we don't want the calling function to lose the object to the called method due to move semantics and we don't want to borrow. This is possible to do if the called method is pure, and no other arguments to the method call have mut or ro references.
- Recovery blocks With this feature, described below, it is possible to unwind a some permission transition that happens inside the block for any value returned by the block.
Recover Blocks
The recover block is a helpful way to transition a permission backwards. Any mutable permission can be recovered as any permission. Similarly, any readonly permission can be recovered as any read-only permission or opaque. This is useful for many situations:
- Create some shared, mutable object inside the block and receive it back as a uni reference.
- Create a cyclic immutable data structure. Inside the block, create a shared, mutable data structure and then recover it as imm.
- Temporarily transition uni to mut, perform complex mutable operations on it and return it as uni again.
- Extract a mutable field from a uni and return it as an uni.
This example demonstrates the third scenario:
recover: imm ref1 = uniref as &mut i32 imm ref2 = ref1 *ref1 = 5 ref2 as &uni i32
The recover block imposes several restrictions in order to ensure the reverse transition is safe:
- You have no access to any mut, ro or lock permission reference/object outside the recover block, whether local or global.
- Any functions you call must be pure.
These restrictions exist to stop unsafe references from leaking out of the recover block. Given that created reference aliases expire within the block, we know any returned reference is unique.
When struct fields are references
There is a symbiotic, two-way relationship between the attributes of a composite type (like a struct) and the permissions on its field references:
- Permissions are infectious. When struct fields are references, the permissions of those references can infect the properties of the struct type.
- Viewpoint adaptation. Conversely, the permissions on some reference to the composite type can restrict the permissions we effectively get on any field's reference.
Let's go into detail on both these phenomena.
Permissions are infectious
Let's say we define a new struct type with fields that are references:
struct Car: engine &uni Engine owner &mut Person
Since the engine field is a uni reference, move semantics apply not only to the engine field, but also to any value of type Car! If any part of a type has move semantics, the whole type does as well.
Similarly, since the owner field is a mut reference, that permission forbids the reference from being shared across multiple threads. If the field must remain thread-local, so must any Car value (or reference to a Car value). This principle applies to all the thread-local permissions: mut, ro and rwcell.
Viewpoint Adaptation
Using the example above, let's imagine we have a borrowed reference to a Car:
fn handleCar(car &Car): if car.owner.age < 18: car.owner.age = 18 // illegal mutation
Even though the car owner field is mutable, the reference we have to car is not. If we don't have permission to mutate Car, we also can't use the car reference to mutate any object that Car points at.
This is called viewpoint adaptation: the permissions we get are the intersection of all permissions we need to go through to get to the object we want. To read the value, all references must grant permission to read. To mutate the value, all reference must grant permission to mutate.
Sadly, this also mean that our car reference cannot see anything about the engine. A shared reference cannot see through a uni reference, because that would effectively be sharing what is supposed to be a unique reference.
We can only see engine if our car reference is also uni. But if we do that, then we can no longer see anything about the owner because it is shared, mutable. It would violate the guarantee of uni to move this reference to another thread, if this could result in mut references to the same Person sprinkled across several threads.
Externally-isolated graphs
Wouldn't it be nice if we could somehow guarantee that when we moved car from one thread to another, that all mutable references to owner would travel with it? If we could guarantee this, it would be safe to perform the move.
In effect, what we want to be able to do is have a graph of multiple objects that is externally isolated, so that we could draw a boundary line around the graph of nodes and there is only pointer into that graph that originates outside that graph. This would be the owning uni reference to the graph's root node. Within the graph could be multiple references to the same node, but none of them originate from outside the boundary line.
To accomplish this, and have visibility into shared references inside the graph, use the recover block as described above. Transition the original uni reference to mut inside the block. Now, we can see and mutate all shared, mutable nodes, including reading or extracting values from them. When done, the block can safely return the original reference as uni.