Note: These are largely implemented, but freezing/lifetime enforcement is missing and they struggle across method calls.
References work differently than other types. The basic idea is simple: references carry an address that points to some value in memory. Using this reference, we can access or change the value the reference points to.
For example:
mut a = 3 imm ref = &mut a // ref holds a reference to a's contents imm val = *ref // val is now 3 *ref = 4 // a is now set to 4
The & operator borrows a mutable reference to the variable a, effectively pointing to the value it holds. The * (de-referencing) operator is used to obtain and then change the value pointed to by the reference.
References are useful, as they support:
- Shared access to the same value. Multiple references may point to the same value. Any of them can see (and possibly change) the latest value at that address. If one reference changes a value, another sees the updated value. By contrast, values copied around between functions do not work this way: changing a copy leaves the original value unchanged.
- Performance efficiency. For non-trivial data structures, it is faster to pass around a reference to the data rather than making a full copy for each function that needs it.
- Access to dynamically-allocated values. Values that have been allocated from a region (as opposed to local or global variables) are only accessible via references. We will talk about region-managed references later.
As the name suggests, borrowed references point to values that already exist in memory. A common scenario is when we have some existing value and, for some limited lexical scope, we want a reference able to work with that value. A borrowed reference is ideal for this requirement because:
- Borrowed references may point to nearly any value accessible to program logic, including any variable, locked value, field, array element, function, method, or any value pointed at by another reference. This versatility makes it possible to create polymorphic functions or methods that work with referred-to values no matter which region they were allocated in.
- A borrowed reference is as fast as any reference can be, as it never incurs any of the runtime bookkeeping cost borne by region-managed references.
Creating a Borrowed Reference
Here are several ways to borrow a reference to some existing value.
Borrowing from a variable
Place the ampersand operator in front of any variable's name to create a borrowed reference that points to the value held by that variable. This works equally well on any global, local or parameter variable:
imm glovar = 2 fn func(parm i32): mut localvar = 3 imm ref1 = &glovar // ref1's type: &i32 imm ref2 = &parm // ref2's type: &i32 imm ref3 = &localvar // ref3's type: &i32
Borrowing from another reference
To explicitly create a borrowed reference out of some other reference, place the ampersand operator in front of a de-referenced reference:
imm borref = &*someref // Borrow from someref
More often, however, borrowing a reference from an existing reference happens implicitly, as part of a function (or method) call or assignment. This happens when the receiver is given a region-managed reference but expects a borrowed reference. So long as the value type (and permission) match, the borrow is performed automatically.
// This function accepts a borrowed reference fn incr(nbr &i32): *nbr + 1 fn main(): imm allocref = +so 1 // An owning reference pointing to an allocated value of 1 incr(allocref) // Coerces the rc reference to a borrowed reference
Because any reference may be safely coerced to a borrowed reference, functions and methods typically accept borrowed references as parameters. This capability is what makes possible the benefits mentioned earlier:
- Functions and methods can be polymorphic across regions and permissions at no cost
- Using borrowed references avoids the runtime performance overhead of reference counting (for the rc region) and reference tracing (for a tracing GC region) within these functions.
Referring to a field or array element
A borrowed reference may point to a value within some composite value, such as a field in a struct or an element in an array. Again, all we need to do is apply the ampersand operator:
imm ref1 = &apoint.x // a field within a struct imm ref2 = &vec[a] // an element within an array
This kind of internal borrow is quite powerful, as it supports:
- Internal reference chains.
One can reference values within values within values, by chaining together
fields or indexing:
imm ref3 = &vec[3].point.x
Parentheses should be used, where needed, to clearly demark where an internal reference chain ends.
- Methods in place of fields.
A method name may be specified (instead of a field name) after the dot operator.
This calls the method, passing it the reference evaluated so far (as self),
and returning a borrowed reference calculated by the method's logic.
For example:
imm ref4 = &pointer.someref(key) // passes &pointer as self to someref method. Returns a reference
Similarly (for some types), using the indexing operator [] will invoke the type's [] method to obtained the borrowed reference.
- Auto-dereferencing a reference source.
When an internal reference expression begins with a reference as the source, it is automatically dereferenced:
// pointref is a reference to a Point structure imm ref3 = &pointref.x // equivalent to: &*pointref.x
Pattern-matching and each loops
One may obtain borrowed references during each iteration or pattern matching by using a borrowed reference with the required permission as the source:
// increment every number in list each x in &mut list: ++*x // x is a mutable reference
Permissions
A reference permission may be specified when a reference is borrowed. The permission governs what may be done with the reference: May we read its value? May we change its value? May we make a copy of the reference?
This permission is specified after the ampersand operator. If none is specified, ro is assumed.
mut a = 3 imm ref = &mut a // Creates a mutable borrowed reference to a's value *ref = 4 // a is now set to 4
The permission requested for the borrowed reference must be allowed by the source we are borrowing from. For example, a mutable borrowed reference may not be obtained from an immutable variable.
Permission mechanics are rich enough to be more fully covered on their own page.
Lifetime Constraints
Every borrowed reference has a lifetime constraint implicitly imposed on it. This lifetime constraint indicates how long the borrowed reference can stay alive before it must expire.
- A reference borrowed from a global variable or function is granted a 'global lifetime. This means it can last for as long as it wants.
- A reference borrowed from a local variable or another reference has its lifetime constrained to the block where the borrow took place. The compiler prohibits it from surviving beyond the end of the block.
This example demonstrates the importance of this lifetime constraint to memory safety:
fn getval() i32: mut ref &i32 { imm a = 5 ref = &a // Bad idea! This violates the lifetime constraint } *ref // Oops!
The scope of the variable a lasts only for the duration of the inner block. So when we are done executing that block, the variable a ceases to exist. However, when we try copying the borrowed reference into ref, we effectively extend its life beyond the scope it was created in and beyond the life of the value it points to. When we then dereference it, we are trying to retrieve a value that is no longer there. That's a memory safety hazard we must avoid.
The compiler protects against this by throwing an error on this line: ref = &a. This is illegal because we are storing a borrowed reference into a memory location whose lifetime is longer than the borrowed reference. A borrowed reference may only be stored in a place whose lifetime is the same or smaller than the lifetime of the borrowed reference.
Most of the time, the compiler has all the information it needs to infer and enforce lifetime constraints. However, sometimes it needs help in the form of lifetime annotations.
Freezing access to the source of a borrow
When a borrowed reference is created, its source is always a named variable. That source variable either holds or points to the value we are borrowing a pointer to. When we borrow from the source, we are not just gaining access to the value, we are also gaining some or all of its access permissions.
Whenever we borrow from a non-global source, the source variable is made inaccessible for the lifetime of the borrowed reference in order to preserve permission guarantees:
fn freeze(n i32) i32: if n > 1: imm borref = &n // borrowing makes 'n' inaccessible in this block imm m = n // oops! n is frozen and may not be accessed n // Ok because the borrow has expired.
Reference Operations
The de-referencing and comparison operators may be directly applied to references.
De-referencing
The most common use of references is to access and work with the value it points to. Accessing this value is called de-referencing. To explicitly de-reference, use the * operator. Within expressions, dereferencing retrieves the value the reference points to. Used to the left of an assignment operator, dereferencing changes that value.
mut count = 3 imm ref = &mut count imm val = *ref // val is now 3 *ref = 4 // ref now points to the value of 4 (in count)
De-referencing is only permitted if the reference's permission allows it. Certain permissions, such as opaq and the locked permissions, disallow de-referencing.
Comparison
Two references may be compared for equality, even if they do not have the same type signature. What this test determines is whether both references point to the same value:
// Do ref1 and ref2 point to the same value? if ref1 == ref2: // do some stuff
It is also possible to compare whether one reference is "greater" than another. This compares their respective memory addresses. This might only be meaningful if both refer to somewhere within the same object.
Reference Handling
References are values. As such, they can be stored and passed around a program. Whether such transfers are simple copies or moves depends on the reference's permission (move semantics apply to all references having the uni permission). Reference transfers also check reference type information to ensure everyone is in agreement about what you can do with any passed-around reference.
Even though references are typed values, they do sometimes get some special treatment to make them more convenient to use, particularly to enable them to act as stand-ins for the values they point to.
Reference Method Definitions
Methods may be defined that accept references as the type for self. However, importantly, such methods must be defined as part of the value type's collection of methods. Thus, if we want to define a method that works on a reference to a Point, that method must be defined within the collection of methods for the Point type:
struct Point: // .. other Point methods here // method that accepts a reference to a Point fn normalize(self &mut Point): imm len = self.len x /= len y /= len z /= len
Implicit De-referencing
Since a reference is generally understood to be a stand-in for the value it references, de-referencing often happens implicitly. For example, when the dot operator is applied to a reference to access a field, the reference is automatically de-referenced:
imm y = ref.y // equivalent to: (*ref).y
Automatic de-referencing might also happen when applying a method to a reference. For example:
imm pos = ref.abs // pos is 4. Equivalent to: (*ref).abs imm sum = ref + 4. // sum is 0. Equivalent to *ref1 + 4.
With methods, the decision on whether or not to implicitly de-reference depends on what is supported by implemented methods. If an existing method can accept the reference as a reference, it is used. If not, the reference is de-referenced before applying the method.
Nullable References
References only ever point to valid values. However, it is possible to explicitly declare and use nullable references, much like any other nullable value.
imm ref4 ?&imm i32 // nullable reference to an integer
In addition to pointing to a valid value, a nullable reference may also have the value Null, which means the reference does not point to any valid value. To ensure safety, access to a nullable reference's value is only possible if the code first ensures the reference does not have the value of Null:
// This condition is true only if maybePoint is not null ... if maybePoint: imm point = *maybePoint // ... allowing us to obtain its value imm point2 = *maybePoint // **ERROR** We don't know if maybePoint is null here
Note: A special optimization ensures that nullable references are the same size as regular references.
Array and Virtual References
In addition to regular references, there are also special-purpose "fat" references which are handled somewhat differently:
- Array references point to a collection of identically-typed elements. These elements are contiguous in memory with the reference pointing to the first element. The number of elements is carried as part of the array reference. Such references may be indexed and are subject to bounds checks.
- Virtual references offer an abstract view of some value instantiated by one of several variant types. In addition to pointing at the value's data, the reference also points to runtime type information that facilitates field access or method dispatch.