&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.