References

A reference lets code use a value that belongs to someone else, without copying it and without taking it over. &T is a shared reference: it can read the value. &var T is an exclusive reference: it can read and write it. A reference never owns its referent, is never null, and lives no longer than the call or the block that made it.

reference-type = "&" type
               | "&" "var" type
struct Account {
    owner: char8[..];
    balance: int;
}

func Report(account: &Account) {
    PrintLine("{}: {}", account.owner, account.balance);
}

func Deposit(account: &var Account, amount: int) {
    account.balance += amount;
}

func Main() -> int {
    var alice = Account { owner: "alice", balance: 10 };
    Deposit(alice, 5);   // borrowed for writing: alice.balance is now 15
    Report(alice);       // borrowed for reading
    return 0;
}

References are the safe way to share. A raw pointer is an address the program vouches for itself; a reference is one the compiler checks, under the rules on Exclusivity.

Borrowing is implicit

There is no borrow operator. A reference is made — the value is borrowed — when a place is given where a reference is expected:

  • an argument for a parameter of type &T or &var T;
  • a receiver: calling a method declared with self: &T or self: &var T borrows the value it is called on;
  • a binding annotated with a reference type: let shared: &Account = alice;.

The call site looks the same as passing by value; the parameter's type decides. & is never an operator, and writing it before a value is an error that points to @, which takes a raw address:

error: '&' does not take an address; write '@' to take the address of a value

What is borrowed must be a place — a variable, a field, an element, or the referent of another reference. A literal or a freshly built value has nowhere to be borrowed from, so Report(Account { owner: "x", balance: 1 }) and Bump(5) — Bump is defined below — are rejected:

error: argument 1 to 'Report' has type 'Account', but parameter 'account' requires '&Account'
error: argument 1 to 'Bump' has type 'int', but parameter 'count' requires '&var int'

An exclusive borrow also needs a place that may be written. With let savings = Account { … };, Deposit(savings, 10) is rejected:

error: argument 1 to 'Deposit' cannot borrow immutable 'savings' as '&var Account'
  help: declare 'savings' with 'var' to make it mutable

A &var T is accepted wherever a &T is expected: an exclusive reference can always lend reading.

Using a reference

A reference is used as if it were the value. Fields, methods, indexing and .length all reach through it with no * and no arrow:

func First(values: &int[3]) -> int {
    return values[0] + values.length as int;
}

A reference to a scalar — an integer, a float, a bool, a character — supplies the scalar's value wherever a value is expected: in arithmetic and comparisons, in a condition, in a cast, in an assignment or a typed binding, as a by-value argument:

func Twice(value: &int) -> int {
    return value * 2;
}

To copy a scalar out of a reference, give the binding its type: let saved: int = value;. * is a raw-pointer operator and never applies to a reference:

error: operator '*' requires a pointer operand, but found '&var int'
  note: a reference reads and writes its referent without '*'
  help: write 'r' in place of '*r', as in 'r += 1'

Writing through &var

Through a &var T the referent can be written in three ways.

Fields and elements are written as usual: account.balance += 5, values[i] = 0.

A scalar referent is written by assigning to the reference itself. =, <-, the compound assignments and ++/-- all store into the caller's value:

func Bump(count: &var int) {
    count += 1;
}

func Main() -> int {
    var total = 4;
    Bump(total);                 // total is now 5
    let alias: &var int = total;
    alias = 20;
    alias++;                     // total is now 21
    return 0;
}

Any other referent — a struct, a tuple, an array, a value of a type parameter — is replaced whole with = or <-. This is how a method replaces the value it was called on:

extend Account {
    func Reset(self: &var Account) {
        self = Account { owner: self.owner, balance: 0 };
    }
}

The write follows the assignment rules for T exactly as an assignment to a var local would: the new value is produced first, the old one is destroyed, and the new one is installed; a move-only value needs <-. Compound assignment and ++/-- apply to scalar referents only.

Through a shared reference every write is an error:

error: cannot modify data through immutable reference '&Account'

Rebinding a reference

A reference binding declared with let always writes through. One declared with var is different for plain =, which points it at other storage instead:

var first = 1;
var second = 2;
var current: &int = first;
current = second;   // current now refers to second; first is unchanged

Assigning a value of the referent's type to such a binding is therefore an error. With var counter: &var int = total;, counter = 5; fails:

error: cannot assign 'int' to '&var int'
  note: '=' points the 'var' reference 'counter' at other storage
  help: declare 'counter' with 'let' to write through it

Where a reference may appear

A reference is a non-owning alias for the length of a call or a block, so it may appear in exactly three places: a parameter, a receiver, and a local binding. It cannot be stored in a field, returned, or moved out of — a field r: &int in a struct Holder, a function returning &Account, and let mine <- account; on a reference parameter are rejected in turn:

error: field 'r' in struct 'Holder' cannot store reference type '&int'
  note: references are non-owning aliases and cannot escape into aggregate storage
  help: store the owned value or a raw pointer when an address must outlive the borrow
error: function return type cannot store reference type '&Account'
error: cannot move a non-owning reference
  note: references borrow storage but do not own the value they address

A reference cannot destroy its referent either: replacing a value through &var T installs a new one in its place. When an address must be stored or must outlive the call, use a raw pointer.

References and pointers

&T, &var T*T, *var T
Made byan implicit borrow@place
Nullnevernull is a value
Read and writeas the value itself*p; p.field and p[i] reach through
Arithmeticnonep + n, p - n, p[i]
Stored in a field, returnednoyes
Checkedexclusivity, writabilitywritability only

A reference is 8 bytes, the size of the address it holds.

See also