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. For some of these permissions, their guarantees can be enforced entirely by the compiler, which means they are free from runtime overhead. Others are enforced by a combination of compiler and runtime mechanisms (e.g., locks).
Prudent use of permissions can also help make code easier to understand and improve execution performance.
List of Permissions
Let's introduce each permission, one at a time.
The uni (unique) permission is quite permissive. 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.
imm ref = &so 5 // uni is the default permission for owning reference imm newref = ref // Move uni-based reference to newref imm x = ref.x // ERROR! ref is no longer usable, as its value moved to newref
uni is the default permission on an owning reference, if no permission is explicitly specified.
uni is ideal when we never need multiple owning references to the same value (a situation that would also benefit by using the so region). uni is also valuable when multiple, owning references are not needed early in the lifetime of the object, but we do want it to be able to move from thread to thread. Performance optimization benefits may also result from knowing the reference is unique.
The imm (immutable) permission declares that a reference's value will never change after it is initialized. Multiple references may point to the same immutable value, even across multiple threads. An imm reference may view its value, but not alter it.
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.
A const 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. const makes no such guarantee. Therefore, const references cannot be safely shared with another thread.
The primary use for const 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 const, if no permission is explicitly specified.
The mutex permission enables multiple, mutable references to be shared across threads. A mutex reference itself cannot directly access or change the value it points to, except by using atomic operations.
To obtain direct access to the value, one must first obtain a borrowed reference from the mutex reference. Obtaining the borrowed reference automatically acquires a runtime lock that ensures only one reference at a time can view or modify the value the reference points to.
imm point = &gc mutex Point(x:2, y: 3) do pointref = &mut *point // Obtain borrowed ref to unlock access to the point pointref.x += pointref.y // Access to point safely protected by mutex
When the borrowed reference expires, so does the acquired lock.
The rwlock permission works very much the same way as mutex, except that it allows multiple read-only borrowed references to be obtained at the same time. However, only one mutable borrowed reference may be obtained at a time.
The rwcell permission is quite similar to rwlock, except that all its reference copies are constrained to say in a single thread. This is useful when one withes to serialize at runtime the acquisition of interior references to variant or collection values.
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 = &rc imm 3 imm ref2 = &rc mut 3 mut ref3 = &rc imm 3 mut ref4 = &rc mut 3
Because of the mut variable permission, ref3 and ref4 may be changed to hold a different reference. ref1 and ref2 cannot.
ref1 = &rc imm 5 // ** ILLEGAL! ** ref2 = &rc mut 5 // ** ILLEGAL! ** ref3 = &rc imm 5 // ok ref4 = &rc mut 5 // 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! ** *ref2 = 5 // ok *ref3 = 5 // ** ILLEGAL! ** *ref4 = 5 // ok
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.
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.
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 owning reference from uni to a shared permission reference is 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.
It is safe to create a const copy of any mut or imm owning reference. Even better, it is safe to borrow a const reference from a reference of any permission other than opaq. This quality is what makes it attractive for functions to polymorphically accept const references to any values they do not intend to modify.
When Type Fields are References
If a type has a uni reference field, values of that type are likely also governed by move semantics.
If a type defines a reference field whose permission stops it from being shared across multiple threads (mut, const and rwcell), values of that type (and references to values of that type) are also prevented from being shared across multiple threads.
If we have a reference that prohibits mutation of its pointed-at value, that mutability restriction also applies to any reference fields it holds, even if they might otherwise allow mutation of their values.