Skip to content

References

Take, pass, and return safe `&T` references; reseat them, restrict them with @weak, and know where raw pointers stop being safe.

Updated View as Markdown

&T is a safe reference to a value of type T. It preserves access to one particular value — or to one interior place inside it — without exposing a machine address and without copying the value.

struct Record { name: string, size: int }

fn describe(value: &Record): string {
    return "${value.name} (${value.size})"
}

fn main(): void {
    r := Record { name: "report.csv", size: 4096 }
    println(describe(r))
}

Note the call: describe(r) passes a Record where a &Record is expected and the compiler inserts the reference take.

Taking a reference

Prefix & takes one explicitly. Use it in an annotation, when storing the reference, or anywhere the intent would otherwise be unclear.

struct Record { name: string }

fn read(value: &Record): string => value.name

fn f(): string {
    r := Record { name: "ada" }
    rr: &Record = &r
    return read(&r) + read(r) + rr.name
}

& requires a place — a variable, a field, or an element. It cannot be applied to a temporary, because there is nothing for the reference to be a reference to:

struct Record { name: string }

fn read(value: &Record): string => value.name

fn f(): string {
    return read(&Record { name: "ada" })
}

Bind the value first, then reference the binding.

&T is the only spelling to use in source. The prelude also exposes a Ref[T] builtin, but it does not unify with &T and is not a nominal alias for it — leave it to the runtime and standard-library internals.

Access

Dereference is implicit. Field reads, field writes, and method calls all go through the reference without any operator.

struct Counter {
    value: int

    fn current(self): int => self.value
    fn bump(mut self): void { self.value += 1 }
}

fn use_ref(): int {
    mut c := Counter { value: 0 }
    r: &Counter = &c
    r.bump()
    return r.current() + c.value
}

There is no source-level * dereference operator:

struct Record { n: int }

fn f(r: &Record): int {
    return *r.n
}

The same applies to fields whose type is itself a collection. Reaching a list through a reference and then iterating it works, because the field access resolves to the list:

struct Order { lines: []int }

fn total(o: &Order): int {
    mut t := 0
    for v in o.lines {
        t += v
    }
    return t
}

A reference to the list itself is a different matter — &[]T is a reference, not an iterable, so for rejects it:

fn total(xs: &[]int): int {
    mut t := 0
    for v in xs {
        t += v
    }
    return t
}

Method calls still work through such a reference, so take []T (or [..]T) rather than &[]T when the callee needs to iterate:

fn count(xs: &[]int): int => xs.len()

fn f(): int {
    xs := [1, 2, 3]
    return count(&xs)
}

Matching through a reference

Pattern matching a referenced enum does not currently see the scrutinee’s variant set as closed, so an exhaustive-looking match needs a wildcard arm:

enum Shape { Circle(float), Square(float) }

fn area(s: &Shape): float {
    match s {
        Circle(r) => return r * r
        Square(w) => return w * w
        _ => return 0.0
    }
}

Taking the value by parameter instead removes the need for the wildcard:

enum Shape { Circle(float), Square(float) }

fn area(s: Shape): float {
    match s {
        Circle(r) => return r * r
        Square(w) => return w * w
    }
}

Mutation

There is no &mut T. Mutation permission comes from the binding, the receiver, and the parameter — not from a second reference type:

struct Record { n: int }

fn f(r: &mut Record): void {
    r.n = 1
}

Write through an ordinary &T when the referenced place is mutable, or use a mut self method:

struct Counter { value: int }

fn bump(c: &Counter): void {
    c.value += 1
}

fn f(): int {
    mut c := Counter { value: 0 }
    bump(&c)
    bump(&c)
    return c.value
}

mut on a binding that holds a reference means the binding can be reseated to point somewhere else. It says nothing about writing through the referent:

struct Record { n: int }

fn f(): int {
    a := Record { n: 1 }
    b := Record { n: 2 }
    mut r: &Record = &a
    r = &b
    return r.n
}

Interior references and escape

Atoll has no user-written lifetime parameters. Escape analysis decides whether a reference can stay frame-local or needs a stable, kept-alive owner, and an interior reference retains enough parent information to reach that owner.

The idiomatic shape is a reference whose owner is visibly one of the inputs:

struct City { name: string }
struct Address { city: City }
struct Customer { address: Address }

fn home_city(c: &Customer): &City {
    return &c.address.city
}

fn f(): string {
    c := Customer { address: Address { city: City { name: "London" } } }
    return home_city(c).name
}

Write signatures so the returned reference is tied to a parameter or to an owned managed value. A reference derived from a purely local temporary has no visible owner for the caller to reason about, and whether it survives is a compiler promotion decision rather than something the signature guarantees — return the value instead when the relationship is not part of the API.

Slices

[..]T is a specialised contiguous view built on the same reference machinery: it borrows a range of a list’s elements instead of one value.

fn checksum(data: [..]byte): int {
    mut acc := 0
    for b in data {
        acc = (acc + b.to_int()) % 255
    }
    return acc
}

fn f(buf: []byte): int {
    return match buf.slice(0..4) {
        Some(view) => checksum(view)
        None => 0
    }
}

A slice cannot grow, and growing the original list does not change the view’s length. See Sequences.

Weak fields

@weak marks an optional struct-typed field as non-owning. It is how you break an ownership cycle: a child can point back at its parent without keeping the parent alive.

struct Node {
    name: string
    children: []Node

    @weak
    parent: Node?
}

fn parent_name(n: Node): string {
    return n.parent?.name ?? "root"
}

fn f(): string {
    root := Node { name: "root", children: [], parent: None }
    return parent_name(root)
}

When the referent is destroyed, the runtime clears the registered weak location and the field reads as None. Two restrictions are enforced at check time. The field must be nullable:

struct Node {
    value: int

    @weak
    parent: Node
}

…and it must point at a struct type — scalars, strings, and other inline values cannot be weak targets:

struct Node {
    @weak
    n: int?
}

@weak takes no arguments. Use it only where None after the referent is gone is valid domain behaviour.

Suspension

Safe references survive suspension. The compiler gives them an arena-relative, resumable representation, so holding a &T across an await or any other safepoint is fine:

struct Record { n: int }

fn pause(): void { }

fn f(r: &Record): int {
    before := r.n
    pause()
    return before + r.n
}

Absolute host pointers cannot. The runtime may resume with a relocated arena base, so a ptr[T] that is live across a suspending terminator is rejected by the MIR verifier — later in the pipeline than atoll check, so a unit that passes the checker can still fail to build. This is the central distinction: safe &T references are made resumable; raw machine pointers are not.

Raw pointers

ptr[T] is the unsafe pointer type, used by intrinsics, host adapters, and the low-level prelude implementation.

unsafe fn advance(pointer: ptr[byte], offset: usize): ptr[byte] {
    return pointer + offset
}

unsafe fn distance(a: ptr[byte], b: ptr[byte]): isize {
    return a - b
}

Pointer arithmetic takes pointer-sized isize or usize offsets, and subtracting two pointers to the same pointee yields isize. Loads and stores go through intrinsics inside an unsafe block:

fn first_byte(p: ptr[u8]): u8 {
    return unsafe { intrinsics.load[u8](p) }
}

Raw pointers carry no bounds, lifetime, or suspension guarantee. Application code should reach for values, slices, and safe references instead.

Choosing a form

Need Write Limit
Read a value for the duration of one call plain parameter (Record) callee cannot retain identity
Keep access to a value or an interior field &T / Ref[T] must be taken from a place, and the owner must stay valid
Read a contiguous run of elements [..]T cannot grow; keeps its original range
Point back without owning @weak optional struct field referent may vanish and clear the field
Call an intrinsic or host ABI ptr[T] in unsafe no bounds, lifetime, or suspension safety

Do not add &T to a signature merely to make a large value “cheaper”. The compiler already borrows an ordinary read-only parameter when that preserves semantics:

struct Report { title: string, rows: []int }

fn title(report: Report): string => report.title

fn f(): string {
    r := Report { title: "q3", rows: [1, 2, 3] }
    return title(r) + title(r)
}

Put &T in the public type when shared identity or an interior relationship is genuinely part of the API.

A composed example

struct Address {
    city: string
    postcode: string
}

struct Customer {
    name: string
    address: Address
}

struct Order {
    id: int
    customer: Customer
    lines: []int
}

fn label(a: &Address): string => "${a.city} ${a.postcode}"

fn shipping_address(o: &Order): &Address => &o.customer.address

fn total(o: &Order): int {
    mut t := 0
    for v in o.lines {
        t += v
    }
    return t
}

fn apply_discount(o: &Order, percent: int): void {
    for mut line in o.lines {
        line = line - line * percent / 100
    }
}

fn main(): void {
    mut order := Order {
        id: 1,
        customer: Customer {
            name: "Ada",
            address: Address { city: "London", postcode: "N1 7GU" },
        },
        lines: [1000, 2000, 3000],
    }

    println("order ${order.id} to ${label(shipping_address(order))}")
    println("before ${total(order)}")
    apply_discount(&order, 10)
    println("after ${total(order)}")
}

shipping_address returns an interior reference whose owner is the parameter, label reads through it, and apply_discount writes through a reference to a mutable binding — none of which required a lifetime annotation, a clone, or a dereference operator.

Ownership and copying rules for the values behind these references are in Values.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close