Ownership
AEL, the Agent Engineering Language, catches ownership mistakes before anything runs: using a value after it has moved, changing a value while something else reads it, or reading a value that was never set. Checked code has no null values and no raw pointers.
Copy and move
Every value has one owner: the binding, field or collection that holds it.
- Copied values: integers,
bool, unit and shared references, and arrays, structs, enums,OptionandResultmade only of such values. Passing one on leaves the original usable. - Moved values: bounded text, lists, exclusive references, and anything that holds one of them. Passing one to a function, assigning it, returning it, putting it into a struct or a message, or binding it in a
matchmoves it. The old place is not usable until it receives a new value.
fn archive(note: Str<200>) -> () {
# archive owns note from here on
}
fn example() -> () {
let mut note: Str<200> = "first";
archive(note); # note moves into archive
# archive(note); # would fail the check: note has moved
note = "second"; # note has a value again
archive(note);
}
A struct moves as a whole: you cannot move one field out and keep using the rest.
When a binding receives a new value, the new value is computed first, and only then does the old one go. If computing the new value fails, the binding keeps the old one.
Borrowing
A reference lets code use a value without taking it.
| Reference | Created with | Allows |
|---|---|---|
Shared, &T | &value | Reading. Any number of shared references may exist at once. |
Exclusive, &mut T | &mut value on a let mut binding | Reading and writing. While it exists, it is the only way to reach the value. |
fn read(count: &i64) -> i64 {
return *count;
}
fn reset(count: &mut i64) -> () {
*count = 0;
}
fn example() -> i64 {
let mut count: i64 = 5;
let seen: i64 = read(&count); # shared, for the length of the call
reset(&mut count); # exclusive, for the length of the call
return seen + count;
}
*rreads the value behind a reference. Writing through*rneeds an exclusive reference.- A shared and an exclusive reference to the same value, or two exclusive ones, cannot exist at the same time, including through a function call.
- You cannot move a value while a reference to it exists.
How long a borrow lasts
A borrow held in a binding lasts until the end of the block that declares the binding, not merely until its last use. To end a borrow early, put it in a block of its own:
fn example() -> i64 {
let mut total: i64 = 1;
{
let view: &i64 = &total;
let seen: i64 = *view;
}
total = 2; # allowed: the borrow ended with its block
return total;
}
A borrow passed straight into a call lasts for that call.
Where references never go
A reference cannot:
- be returned from a function;
- be kept in an agent's state that outlives the current call;
- be sent in a message to another agent or service;
- be held across a wait, such as a pending request or a timer.
These rules refuse some programs that would in fact be correct. They are deliberately strict, so that the check never has to guess.
Definite initialization
A binding declared without a value must receive one on every path before it is read.
fn choose(flag: bool) -> i64 {
let x: i64;
if flag {
x = 1;
} else {
x = 2;
}
return x;
}
Leave out the else branch and the check refuses return x;. A value set only inside a while loop does not count after the loop, because the loop may run zero times. A binding without mut can receive its value exactly once.
Ownership and messages
Sending a value to another agent moves it into the receiver's mailbox. If the mailbox is full or closed, the send hands the value back to you, so it is never lost and never owned twice. Concurrency describes mailboxes.