Note: Much of this capability is implemented except for error handling on bad allocations.
Imagine that a program's memory is conceptually partitioned into regions, each with its own strategy for allocating and reclaiming memory for values. Every program value is wholly located within and governed by its owning region.
By way of illustration (even though we have not said so until now), the memory space for every variable's value is automatically allocated in one of these regions:
- Global region. All global variable values are stored at fixed locations in the global region.
- Execution Stack regions. Local variable values are allocated and de-allocated in LIFO order from a stack region as execution unfolds. Each execution thread has its own stack region.
These are not the only kinds of regions supported by Cone. A broad range of region strategies may be used to dynamically allocate space for new values, such as &rc (reference counting), &gc (tracing GC), &ar (arena) and &so (single owner). The next page describes and evaluates different region strategies in more detail.
Before we get to that, let's focus first on owning references, the handles we use to work with values dynamically allocated by regions. All owning references point to some valid, allocated value. This page describes how owning references are created and used.
Creating Owning References
Let's ask a region to allocate a new value and give us back an owning reference:
imm nbrref = &rc imm 4
This uses the ampersand operator to dynamically allocate and initialize a new value, specifying:
- Region which allocates the value and manages its lifetime. In this case, the value is allocated in the rc region.
- Permission. This governs what you may do with the reference. imm allows one to see the reference's value, but not change it. (Note: uni is assumed for an owning reference when no permission is specified.)
- Initial Value. A copy of this value is stored in the newly allocated memory location. In this case, it is the integer 4.
What we get back from this allocation is an owning reference, which this example stores in the variable nbrref. The inferred type of this reference looks similar to the allocation: &rc imm i32. This type signature includes the type of the initial value as the reference's value type.
A type's constructor or initializer may be specified as the initial value.
imm gcref = &gc mut Point3[1f, 2f, 3f] // Allocate a new Point3 value using a constructor
Creating Multiple References to the Same Value
The creation of the first owning reference to a new value always comes from an allocation, as shown above. Additional owning references to that value may then be created by copying an existing reference via assignment or function calls:
imm nbrref = &rc imm 4 imm nbrref2 = nbrref // nbrref2 and nbrref point to same value
How References and Allocations Expire
Owning references keep the value they point to alive. So long as a value has at least one owning reference that points to it, the value's region will not dispose of the value and reclaim its memory space. Only when all references to a value expire does the region have the right to free the value (although how quickly this happens varies by region).
So, when does the lifetime of an owning reference expire? When the data structure it is bound to expires or changes its value. If the reference is never stored anywhere, it is consumed by the expression it is part of and expires nearly immediately. If the reference is stored in a variable, it expires no later than the end of the lexical scope that variable is declared within (and sooner, obviously, if the variable takes on a new value). If the reference is pointed at by another reference, then the lifetime of the former depends on the lifetime of the latter.
When all references to an allocated value expire and the region has decided to reclaim the value's space, the region will automatically invoke the value's finalizer, if it has been defined.
The de-referencing and comparison operators may be directly applied to references.
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.
imm ref = &rc mut 3 imm val = *ref // val is now 3 *ref = 4 // ref now points to the value of 4
De-referencing is only permitted if the reference's permission allows it. Certain permissions, such as opaq and the locked permissions, disallow de-referencing.
Similarly, de-referencing to obtain a value is prohibited if move semantics apply. This would happen if the reference uses the uni permission or points to a value of a non-copyable type. This prohibition is because we likely have no way to comprehensively deactivate the source of this value. To get around this limitation and extract the value anyway, swap the value the reference points to with some other valid value.
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.
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 &rc mut Point): imm len = self.len x /= len y /= len z /= len
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 ref = &rc mut Point[1f, 2f, 3f] imm y = ref.y // equivalent to: (*ref).y
Automatic de-referencing might also happen when applying a method to a reference. For example:
imm ref = &rc mut -4. 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.
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 ?&rc 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.
Memory Allocation Errors
Although unlikely, it is still possible for memory allocation requests to fail. This typically happens when not enough contiguous, free memory exists to satisfy the request.
Failure to allocate memory will trigger a different response depending on whether the owning reference is marked as nullable.
- Non-nullable reference: the program will panic and shut down.
- Nullable reference: A failed allocation returns the value null. Any of the mechanisms provided for handling nullable values may be applied in an attempt to recover from this problem.