Atoll source is value-oriented. You write construction, binding, and passing;
the compiler picks inline lanes, arena blocks, borrows, and reference counts.
There is no new, no box, no allocator argument, and no retain/release in
source.
struct Point { x: int, y: int }
fn main(): void {
origin := Point { x: 0, y: 0 }
label := "origin"
marks := [origin]
println("${label} ${marks.len()} ${origin.x}")
}The one question that matters for every type is: when I bind or pass a value, does the second name see later writes through the first? The answer splits cleanly along one line — whether the type owns arena storage.
Scalars copy
int, float, bool, char, byte, and the sized integer types are pure
value lanes. A second binding is an independent copy, at every optimization
level.
fn main(): void {
a := 3
mut b := a
b = 10
println("${a} ${b}") // 3 10
}A plain parameter copies as well, so a callee that wants a scratch value binds its own and leaves the caller’s alone:
fn doubled(n: int): int {
mut scratch := n
scratch = scratch * 2
return scratch
}
fn main(): void {
original := 21
result := doubled(original)
println("${original} ${result}") // 21 42
}Marking the parameter mut is a different request. It is not “give me a
mutable copy” — it is “let me write through to the argument”, and the caller
sees the write:
fn doubled(mut n: int): int {
n = n * 2
return n
}
fn main(): void {
original := 21
result := doubled(original)
println("${original} ${result}") // 42 42 — the caller's value changed
}Use mut on a parameter only when writing back is the point. When you just
need somewhere to accumulate, bind a local as in the first version.
Managed values share
Strings, lists, maps, sets, closures, and any aggregate containing them own arena-backed storage. Binding one to a second name does not duplicate that storage — both names refer to the same block, and the compiler adjusts a reference count you never write.
Element writes through either name are therefore visible through both:
fn main(): void {
mut xs := [1, 2, 3]
mut alias := xs
alias.set(0, 99)
println("xs[0]=${xs[0] ?? 0}") // xs[0]=99
}Growth is the exception, and it is worth learning as its own rule. add may
have to move the backing block, and only the list that grew is reseated onto
the new storage. The other name keeps the old block and the old length:
fn main(): void {
mut xs := [1, 2, 3]
mut alias := xs
alias.add(4)
println("xs.len()=${xs.len()} alias.len()=${alias.len()}") // 3 4
}So “these two lists alias” is true for element updates and false across a
grow. Do not build a program on either half of that by accident — if two names
must diverge, say so with clone().
Map and Set share the same way:
fn main(): void {
mut counts := Map.new[string, int]()
counts.put("a", 1)
mut alias := counts
alias.put("b", 2)
println("${counts.len()} ${alias.len()}") // 2 2
}Cloning
clone() is the explicit request for an independent owned copy. It is a
Clone trait method, not a universal operation: the prelude implements it for
int, bool, char, string, and List[T].
fn main(): void {
mut xs := [1, 2, 3]
ys := xs.clone()
xs.set(0, 99)
println("${xs[0] ?? 0} ${ys[0] ?? 0}") // 99 1
}A type you define does not get clone() for free:
struct Point { x: int, y: int }
fn main(): void {
p := Point { x: 1, y: 2 }
q := p.clone()
println("${q.x}")
}Implement Clone when a deep copy is meaningful for the type, and decide
field by field how deep it goes. Here the string is immutable so it can be
shared, while the list is cloned because callers mutate it:
struct Basket {
owner: string
items: []string
}
impl Clone for Basket {
fn clone(self): Basket {
return Basket { owner: self.owner, items: self.items.clone() }
}
}
fn main(): void {
mut a := Basket { owner: "ada", items: ["apple"] }
mut deep := a.clone()
deep.items.set(0, "pear")
println("a=${a.items[0] ?? "?"} deep=${deep.items[0] ?? "?"}") // apple pear
mut shallow := a
shallow.items.set(0, "fig")
println("a=${a.items[0] ?? "?"} shallow=${shallow.items[0] ?? "?"}") // fig fig
}Do not reach for clone() reflexively to quiet the compiler — Atoll has no
borrow checker to appease. Clone when independent mutation is part of the
program’s meaning, and not otherwise.
Aggregates of scalars
A struct, tuple, or enum payload whose fields are all scalars carries no arena storage, so the language rule is the scalar rule: a second binding is an independent copy.
struct Point { x: int, y: int }
fn main(): void {
first := Point { x: 3, y: 4 }
// Reliable at every optimization level — name the fields you keep.
mut second := Point { x: first.x, y: first.y }
second.x = 10
println("${first.x} ${second.x}") // 3 10
}The shape to avoid until the defect is fixed is mut second := first followed
by a write to second. It reads as a copy, type-checks as a copy, and at -O0
behaves as a copy — but at the default -O2 the write lands in first too.
The explicit form is also the one to reach for when only some fields change,
because it reads as “a new Point derived from first” rather than as a
mutation of something that might be shared:
struct Point { x: int, y: int }
fn moved_right(p: Point, by: int): Point {
return Point { x: p.x + by, y: p.y }
}
fn main(): void {
a := Point { x: 3, y: 4 }
b := moved_right(a, 5)
println("${a.x} ${b.x}") // 3 8
}Mutation
mut is a source permission attached to a binding, not a storage class. A
place can be written only through a mutable path.
struct Counter { value: int }
fn main(): void {
mut count := 0
count += 1
mut c := Counter { value: 0 }
c.value = 7
println("${count} ${c.value}")
}Without mut, the write is rejected at check time:
struct Point { x: int, y: int }
fn main(): void {
p := Point { x: 0, y: 0 }
p.x = 1
}mut self on a method and mut on a parameter declare that the callee may
write through its receiver or argument. Neither says anything about how the
value is represented.
struct Counter {
value: int
fn bump(mut self): void { self.value += 1 }
}
fn main(): void {
mut c := Counter { value: 0 }
c.bump()
c.bump()
println("${c.value}") // 2
}Borrowing
An ordinary read-only parameter is not a promise of a copy. When the callee provably does not retain the value, the compiler passes it by borrow — no copy, no reference-count traffic.
struct Report {
title: string
rows: []int
}
fn title(report: Report): string => report.title
fn row_total(report: Report): int {
mut t := 0
for v in report.rows {
t += v
}
return t
}
fn main(): void {
r := Report { title: "q3", rows: [1, 2, 3] }
println("${title(r)} ${row_total(r)} ${title(r)}")
}This is an optimization that preserves the source contract; it never changes
what the program means. Add &T to a signature only when shared identity or
an interior relationship is part of the API — see
References.
Equality
== and != are value equality, dispatched through Equatable. Structs get
a field-wise implementation without you writing one:
struct Point { x: int, y: int }
fn main(): void {
p := Point { x: 1, y: 2 }
q := Point { x: 1, y: 2 }
println("${p == q} ${p != Point { x: 9, y: 2 }}") // true true
}Strings and lists compare by content, not by backing block:
fn main(): void {
a := "a long value"
b := "a long value"
xs := [1, 2, 3]
ys := [1, 2, 3]
println("${a == b} ${xs == ys}") // true true
}Write your own Equatable when equality is narrower than “all fields match” —
for example when one field is a cache:
struct User {
id: int
display_name: string
}
impl Equatable for User {
fn equals(self, other: User): bool => self.id == other.id
}
fn main(): void {
a := User { id: 1, display_name: "Ada" }
b := User { id: 1, display_name: "A. Lovelace" }
println("${a == b}") // true
}A Hashable type must keep hash_code() consistent with equals — two equal
values must hash the same, or map and set lookups will miss.
fn main(): void {
println("${"key".hash_code() == "key".hash_code()}") // true
}Never infer identity from an arena offset or a WebAssembly handle. Those are representation details and can change between compiler versions.
Destruction
When a value’s ownership ends, the compiler releases its managed fields. Reference-counted storage is reclaimed when its last owner leaves. This covers normal exits, branches, early returns, error propagation, and resumable frames; you do not write a free.
A type implementing Drop gets user-defined finalization at that point:
struct Handle { id: int }
impl Drop for Handle {
fn drop(self): void { println("closing ${self.id}") }
}
fn main(): void {
h := Handle { id: 7 }
println("using ${h.id}")
println("done")
}The output is using 7, done, then closing 7. Drop is for resource
semantics — closing a handle, releasing a lease — not for manual memory
management, which the compiler already handles.
A Drop type used in the same function as a list currently fails to lower
(ATOLL5002, a Wasm validation failure), so keep Drop types out of
collection-heavy entry points until that is fixed.
Cycles
Reference counting cannot reclaim a strongly connected cycle on its own, so the
runtime tracks cycle-capable managed types separately from provably acyclic
ones. When a back-pointer is semantically non-owning, say so with @weak and
the cycle never forms:
struct Node {
name: string
children: []Node
@weak
parent: Node?
}
fn main(): void {
root := Node { name: "root", children: [], parent: None }
println("${root.parent?.name ?? "no parent"}")
}@weak fields are covered in References.
Representation is not the contract
Inline lanes, arena blocks, frame slots, closure environments, RC headers, and WebAssembly ABI lanes are compiler and runtime contracts. They belong to the MIR and Runtime sections, not to application type annotations.
What the language guarantees is narrower and more durable: typed values, explicit mutation permissions, value-defined equality, safe lifetimes, and deterministic source-visible cleanup — not one permanent byte layout per type.
Choosing a form
| Requirement | Reach for |
|---|---|
| Independent scalar semantics | an ordinary value |
| A derived aggregate that must not alias | a fresh literal naming the fields you keep |
| Shared growable data | List, Map, Set |
| An intentional independent duplicate | clone(), or a Clone impl you write |
| Stable access to an existing value or interior field | &T |
| Contiguous borrowed elements | [..]T |
| A non-owning relationship that may disappear | a @weak optional struct field |
| A machine address for a proven low-level operation | ptr[T] inside unsafe |
A composed example
An order that shares what is immutable, clones what callers mutate, and derives new aggregates by naming their fields.
struct Line {
sku: string
qty: int
}
struct Order {
id: int
lines: []Line
}
impl Clone for Order {
fn clone(self): Order {
return Order { id: self.id, lines: self.lines.clone() }
}
}
impl Equatable for Order {
fn equals(self, other: Order): bool => self.id == other.id
}
fn total(order: Order): int {
mut t := 0
for line in order.lines {
t += line.qty
}
return t
}
fn scaled(line: Line, factor: int): Line {
return Line { sku: line.sku, qty: line.qty * factor }
}
fn main(): void {
original := Order {
id: 1,
lines: [Line { sku: "a", qty: 2 }, Line { sku: "b", qty: 3 }],
}
// A clone, so the amendment cannot reach back into `original`.
mut amended := original.clone()
amended.lines.add(scaled(Line { sku: "c", qty: 5 }, 2))
println("original ${total(original)} lines ${original.lines.len()}")
println("amended ${total(amended)} lines ${amended.lines.len()}")
println("same order: ${original == amended}")
// An alias, so this one does share.
mut alias := amended
alias.lines.set(0, Line { sku: "a", qty: 100 })
println("amended after alias write ${total(amended)}")
}original and amended share nothing mutable because of the clone();
alias and amended share everything because there is no clone between them;
scaled derives a Line by naming its fields rather than by copy-and-mutate.
No allocation, free, retain, or release appears anywhere in the source.