Instead of the more traditional approaches like reference counting, garbage collection, or borrow checking, Vale achieves its memory safety through something we call the Linear-Aliasing Model.
The Linear-Aliasing Model is where every object's lifetime is determined by exactly one owner, yet we can also have as many mutable references to it as we want. In other words, each type is linear, yet freely aliasable (we'll explain these further below).
This model offers efficient memory safety, plus be able to use shared mutability wherever we want. 0
The rest of this page is an overview, but if you'd like to see it in action, these articles will be helpful:
There are four basic types:
Owned values, written like AStruct, which inhabits exactly one variable, struct field, or array element. When that containing stack frame, struct, or array goes out of scope, this AStruct is destroyed.
struct Ship { fuel int; }
exported func main() {
ship = Ship(42);
println(ship.fuel);
destruct ship;
}
42
Owning pointers, written like ^AStruct, which lives on the heap, yet is still "owned" by a containing stack frame, struct, or array. When that containing thing goes out of scope, the AStruct lives on. 1
struct Ship { fuel int; }
exported func main() {
ship = ^Ship(42);
destruct ship;
}
42
References, written like &AStruct, which doesn't affect the object's lifetime or when it's destroyed, yet can be used to read or write to the AStruct.
struct Ship { fuel int; }
exported func main() {
ship = Ship(42);
ship_ref = &ship;
println(ship_ref.fuel);
destruct ship;
}
42
And lastly, copy types, such as (bool, float, etc.), which are copied freely, similar to most languages.
The Linear-Aliasing Model adheres to single ownership:
We can transfer an owned value or owning pointer from one place to another via a move operation. 2 Specifically:
These are a certain flavor of move semantics, which uphold single ownership for a language.
It uses linear typing in that the user is required to explicitly destroy an owned value or owning pointer, and the compiler throws an error if the user forgets. The compiler won't automatically destroy something for us. 4 5
Linear typing has a lot of benefits:
However, linear typing alone has historically had some drawbacks:
These drawbacks are because linear typing doesn't allow mutable aliasing.
We address that by adding mutable aliasing via generational references, which can do a run-time check to see if the target object is still alive. These checks are elided for owned values and owning pointers. In Vale, these checks are also eliminated by static analysis and regions.
Generational references are particularly suited to this task because they can read and write to the target data without affecting its lifetime.
Suddenly, we have all the benefits of mutable aliasing:
...while still having the benefits of linear typing.
This addition isn't free; every allocation (though not every object 6) needs an 8-byte random generation number at the top. This could soon be obviated with unique references or isolates.
We end up with a model that's fast, safe, and most importantly, flexible. We can decide to use references only where necessary, or perhaps we can decide to drop into a more linear style where profiling suggests we should optimize.
For an example of a program that uses both the Linear-Aliasing Model and regions, see this tiny roguelike game. 7
If you'd like to see more, check out these articles:
The Linear-Aliasing Model is one of many techniques that Vale is bringing into the mainstream, along with:
Note that shared mutability itself can have run-time cost, but it's opt-in and the user has the flexibility to make that tradeoff where it makes sense.
This is most similar to C++'s unique_ptr<AStruct>. It's like Rust's Box<AStruct>, but freely aliasable.
We can also swap something into its place. In Vale, this is via the set operator.
Also known as a "move destructure".
If the compiler did automatically destroy things, it would be an affine type system instead of linear.
In Vale, we can actually opt into this for specific structs.
A struct will often share the generation of the struct that contains it.
Regions are still in alpha, so forgive any manual lowering and verbosity in the program.