While talking about references, several unfamiliar permissions have been mentioned in passing. It is now time for a more detailed treatment. This is important not only because the list of permissions is incomplete, but also because many of these permissions offer fluid, polymorphic, and safe transitions.
The primary role of permissions is to improve program safety by preventing race conditions, where multiple concurrent threads step on each other when trying to access the same value. Prudent use of permissions can also help make code easier to understand and improve execution performance.
Every variable, field and reference declares its permission, which enables and constrains its use. For example, different permissions grant or deny the right to read a value, modify a value, share references to the same value, or share access to values across threads.
Permissions come in two flavors:
- Static permissions are completely enforced when the program is compiled, Since they need no runtime enforcement mechanisms, they do not slow down a program. There are a fixed number of these built-in, static permissions.
- Runtime permissions are enforced partly by the compiler and partly by runtime mechanisms. It is possible to define additional runtime permissions beyond those provided by the language.
imm and mut
Let's begin with two static permissions introduced earlier. The rules for these permissions also apply to references:
- imm (immutable) declares that a reference's value will never change after it is initialized. Concurrent threads may share access to immutable values.
- mut (mutable) allows multiple references to a value to freely access or change that value at any time. However, all references to any specific value are restricted to a single thread.
Let's add a new static permission to our collection. The static uni (unique) permission allows its value to be read or changed (similar to mut). However, uni carries a significant restriction: there can only be one active reference to its value at a time. Ownership of such a reference may not be shared, it can only be transferred (moved) to another variable, function, or thread.
The uni permission, and its single reference restriction, may seem a bit unusual. However, the concept is incredibly valuable and not new to Cone. Rust's &mut reference, C++'s unique_ptr<>, C's restrict keyword, and Pony's iso reference capability take advantage of this same pattern. Let's explore the implications of using this permission.
uni as the first permission
uni is best understood as the first permission an allocated reference gets in the early days of its existence. Whenever an allocated reference is created, it has the uni permission by default. This makes sense: since the just-created reference is the only one in existence, it naturally complies with the single reference restriction.
So long as the reference keeps its uni permission, this single reference to an object can be freely and safely moved around a program, hopping from function-to-function or even thread-to-thread. When a uni reference is assigned to another variable or passed to a function (or returned), the reference moves to its new owner. Any subsequent attempt to use the previous owner of the reference will trigger an error:
imm ref = &rc 5 // Allocate a new value and return a uni reference imm newref = ref // Move uni-based reference to newref imm x = ref.x // ERROR! ref is no longer usable
uni as the universal donor
In many cases, the single reference restriction of uni poses no hardship and the reference keeps its uni permission throughout the allocated value's lifetime. In addition to flexibility of movement, there can be other benefits to keeping a mutable reference as uni, such as improved performance optimizations and variant type safety.
However, the single reference restriction makes uni references unsuitable for many data structures that require the use of multiple references to the same value. Should a program's logic need multiple references to the same value, the uni reference may be transitioned to a new reference with a sharable permission, such as mut or imm:
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)
Note: This transition may be more concisely performed when the reference is created:
imm newref1 = &rc mut 5 // Allocate new 'mut' integer
uni and temporary transitions
The ownership transition from uni to a shared 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.
However, using borrowed references does make it possible to temporarily transition a uni reference to another permission. During the lifetime of a borrowed reference, the original uni reference may not be used. However, once the lifetime of the borrowed reference expires, the original uni reference becomes usable again:
imm uniref = &rc 5 do imm bref1 &mut i32 = uniref // Coerce allocated 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
The const (constant) static permission has already been mentioned, as it is the default permission assumed when declaring a borrowed reference. A const reference may read its value, but may not modify that value. This description may sound like imm, but it is not 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 value of const is the way that functions, who promise to not change the value, can be made more polymorphic. By declaring a reference parameter as const, the function or method can safely accept references which the caller sees as mut, imm, uni, const, or mut1.
Whereas uni is the universal donor, const is the effectively the opposite: it is the commonly-used, universal receiver for other static permissions. This is why a new borrowed reference defaults to the const permission when no permission has been explicitly specified.
The mut1 permission is the mutable counterpart of const. A mut1 reference may inspect or change its value. During its lifetime, it promises that references borrowed from it are either a) multiple const references or b) a single usable mut1 reference. mut1 may not be shared or moved across threads.
mut1 is typically the best permission to specify when a function or method needs a reference able to change its value. This is because mut1 can safely accept references that are declared as mut, uni, or mut1. This is more flexible than if we defined a mutable borrowed reference parameter as mut, which would not be able accept uni allocated references, references to variant types, or mut1 references.
mut1 for sum type allocated references
mut1 may also be used on allocated references. Its main purpose is to allow multiple references to a value that guarantee that only one can modify the value at a time and only via a mut1 borrowed reference. This can be useful when a program needs multiple mutable allocated references to sum types which, for safety reasons, may never be mut.
Sum types, variable-sized arrays and other types need restrictions on shared mutability for memory safety, even in single-threaded situations. Otherwise, it might be possible for one reference to alter the structure of the object while another reference holds an interior pointer now invalidated by the structural change. Unsafe things could happen if the interior pointer was then used.
Enforcement of this guarantee requires a run-time mechanism. As with all runtime permissions, this means that mut1 must be specified as the permission when allocating the first reference to that value.
id (identifier) declares that this reference will never be used to read or modify its value. It may be compared with another reference. It may be derived (coerced) from any reference. Its primary value is for invokable references to functions and other executables, without the ability to see or change their state.
Runtime (Synchronization) Permissions
In addition to the static permissions, Cone also offers runtime permissions. Runtime permissions enable the use of shared, mutable references that are not limited by the restrictions placed on mut references. Using runtime permissions, it becomes possible to share value references across threads or to obtain interior references to shared references on sum or array types.
The penalty for this added freedom is that reference use carries a small runtime performance hit. This is because runtime permissions wrap every use of the reference to access its contents with that runtime permission's synchronization mechanism which ensures that only one reference at a time can read or change its value. The other potential drawback is that use of runtime permissions can sometimes suffer from deadlocks or runtime panics.
Runtime Permission Coercions
Runtime permissions do not allow coercion to or from any other permission. Thus, the runtime permission must be specified when creating a new allocated reference to a value. All copies of that reference carry the same permission.
This means that objects protected by runtime permissions cannot use functions that accept const or mut1 references. They can only use functions whose reference parameters declare the same runtime permission.
The Lock permission enables multiple, mutable references to be shared and used across threads. It makes use of hardware intrinsics to ensure only one reference at a time can read or modify the contents of the reference.
imm point &Lock Point = &Gc Lock Point(x:2, y: 3) thread.sendPoint(point) // Now another thread has a copy of this reference point.x += point.y // Access to point safely protected by Lock twice
It is possible to define additional runtime permissions ... TBD
perm NewLock ....
Permissions and Threads
Only imm, id, and the runtime "locked" permissions may be shared freely between threads. Furthermore, a uni reference may be moved from one thread to another. References with any other permission (mut, const, and mut1) restrict access to their values to a single thread.
Permissions and Struct fields