Spec §6 — The memory model
Values are copied (§6.1); sharing is asked for explicitly. Every explicit
sharing mechanism (views, projections, Shared, Arena/Handle, and the
resource class, §6.8) is a claim on an owner, and all of them obey the
one law stated in §6.0. The numbered sections that follow are that law’s
projections: the four rules (design:
the design notes), the escape hatches, and resources
(design: claims and epochs, destruction). Rules
1–3 and rule 4’s static half are normative and enforced; rule 4’s dynamic
remainder is future work, marked below.
6.0 The law — owners, epochs, and claims
One relation underlies every sharing mechanism in this chapter; the sections after it are its projections.
An owner is any value with independent existence: a binding, a container, an arena slot, a counted cell, a resource. Every owner has an epoch: an abstract counter that advances on a fixed set of events, determined by the owner’s shape:
| owner shape | epoch events |
|---|---|
| scalar cell (a boxed local) | rebinding, death |
| aggregate (a struct root) | + reassigning an aggregate field out from under an interior view |
container (List, arena slots) | + geometry: resize, insert, remove, reallocation |
counted cell (Shared; the future counted tier, §6.8) | + the strong count reaching zero |
| resource (§6.8) | + the move (the source binding’s epoch ends), + drop (the final event) |
A claim is any alias to a place inside an owner: a &/&mut view
(§6.3), an Arena Handle, a Weak, a loan of a resource. In the
abstract a claim is the triple (owner identity, path, epoch at capture).
The law: a claim is valid while its owner’s epoch is unchanged. Nothing
else is forbidden: aliasing is permitted, and writing through an alias is
permitted (§6.4 declines exclusivity); only using a claim whose owner’s
epoch has advanced is illegal. A suspension point (await) is the
degenerate event: while a function is suspended, other turns run, so every
owner’s epoch must be assumed to advance. await bumps the world (§6.6).
A claim’s validity is established at exactly one of two times, giving two enforcement regimes:
- Static discharge: views (§6.3). The compiler proves that no event
occurs between a view’s capture and its last use. The proof needs a
surveyable interval, which is why views are second-class: lexical
liveness (declaration to block end) is the interval the compiler can
audit. Because the proof is total, the access is infallible and free:
a view compiles to a bare
(base, key)and checks nothing at runtime. - Dynamic carry: handles. When a claim outlives every surveyable
interval (stored in a field, kept across
await, held between turns), it carries its epoch as data (aHandle’s generation) and every access re-establishes validity by comparison, answeringOption<&T>. The check runs at use time, so access is failable, and the failability is in the type. A handle is a view that survived by carrying its proof obligation with it.
The same relation, read at compile time or at use time:
| concern | static (proof) | dynamic (check) |
|---|---|---|
| interior access | &/&mut views; borrows provenance (§6.5) | Arena.get(handle): Option<&T> |
| invalidation | rule 4 (§6.4): reassignment, mutating call | generation mismatch → None |
| suspension | no view across await (§6.6) | handles cross freely; the next access re-validates |
| death | resources: use-after-move; drops placed statically (§6.8) | a stale handle / Weak → None |
| exclusivity | declined: aliased views and content writes are legal | traps only on invalidating overlap (§6.7) |
Reading down the columns: the left is what the compiler proves when
liveness is lexical; the right is the identical relation carried as data
when the claim must outlive the compiler’s sight. Sections 6.1–6.8 place
each mechanism in a cell: rules 1–2 (§6.1–§6.2) govern owners that carry
no claims; rule 3 (§6.3) is the static column’s interval requirement; rule
4 (§6.4) is the static invalidation cell; borrows (§6.5) makes a claim’s
origin explicit; the escape hatches (§6.7) are the dynamic column; and
resources (§6.8) are the static death cell, where use-after-move is the
compile-time twin of a stale handle’s None. §6.9 is the one section
outside the table: a closure capture aliases a binding rather than a
place inside an owner, so it is not a claim, and its rule is which of
this chapter’s values may take part.
6.1 Rule 1 — values are copied; copies are semantic
Every binding, assignment, argument pass, field initialization, and
return copies the value. After mut b = a, mutating b never
affects a, for primitives, structs, enums, tuples, lists, and every
other value type alike:
struct Point { x: i32, y: i32 }
fun main() {
mut a = Point { x = 4, y = 6 };
mut b = a;
b.x = 10;
print(a.x); // 4 — b is a semantic copy
}
“Field initialization” is the whole of a construction: a place read into a
list element, a tuple element, a struct field, or a variant payload is stored
in a slot of a new aggregate that outlives the literal, so it copies there too.
The same holds for a place handed to a parameter declared own — a signature’s
way of saying the callee keeps it, which is how List::push is written — and
for a place a body returns that it does not own, which is every place
reached through a loaned parameter: bare, &, and &mut alike (§6.8’s R3
calls all three loans). What decides is the return, not the place: a
signature that hands back a value hands back a value, so fun make(&self): (i32, i32) { self.pair } copies, while a signature that hands back a view
(fun slot(&mut self): &mut (i32, i32) borrows self) is rule 3’s sanctioned
projection and keeps naming the receiver’s storage. Together these are what
make “a call owns its result” true, which is the premise every elision below
rests on:
The return reads through a view to reach the place, because a returned
expression can name storage without being a place: fun grab(&self): Inner { &self.inner } hands back a reference, and fun get(h: &Holder): (i32, i32) { peek(h) } hands back what the borrows callee projected — which is h’s
storage, named in the call. Both copy, exactly as the bare self.inner /
h.pair spellings do; only the return type tells them apart from the
projections one line away. And a scalar’s copy is its read: a scalar view
is a (base, key) pair at runtime, so fun same(v: &mut i32): i32 { v } hands
back the i32 it promises rather than the pair. A resource crosses no such
seam at all — it cannot copy (§6.8’s R1), so handing one back out of a loan is
a move out of a loan and is refused, with or without the &.
fun main() {
mut xs = [1, 2];
mut rows = [xs]; // a copy: the element is not xs's storage
rows[0].push(9);
print(xs.len()); // 2
}
A consequence worth naming: the list a pure List method returns never shares
element storage with its receiver. Writing through an element of xs.map(f),
xs.filter(p), xs.sort_by(c), or xs.reverse() cannot show up in xs.
A pattern capture is a binding, so it copies too — and the copy is taken
when the pattern matches. A capture therefore holds what its subject held at
that moment, and no later write to the subject can reach back into it. This
holds however the write is spelled, and the spellings differ underneath.
Assigning a whole binding rebinds it: a fresh value is installed and
everything that already read the old one keeps the old one. Every other form
mutates the storage in place — assigning through a view, writing a
component of a place (t.1 = 9, h.pair.1 = 9, xs[0] = 9), and calling
a &mut self method that does either. None of it is visible through a capture:
enum Feed { Ready(List<str>, i32), Done }
impl Feed {
fun step(&mut self): str {
if self is Feed::Ready(let items, let at) {
self = Feed::Ready(items, at + 1);
ret items[at]; // the index the match found, not at + 1
}
"-"
}
}
fun main() {
mut feed = Feed::Ready(["a", "b", "c"], 0);
print(feed.step()); // a
print(feed.step()); // b
mut t = (7, 3);
if t is (let a, let b) {
t.1 = 99;
print(b); // 3 — the component write cannot reach the capture
}
}
The rule is about the storage the subject names, not about how the subject
is spelled, so it holds for all three spellings that name storage someone else
owns: a place (h.pair), a dereference (*view), and a borrows call
(h.slot(), which projects the receiver’s storage rather than producing any of
its own). The last one asks its questions of the arguments the call projects —
the receiver is right there at the call site — so a write reaching the receiver
reaches the capture’s subject by the same reasoning:
struct Holder { cells: (List<i32>, i32) }
impl Holder {
fun slot(&mut self): &mut (List<i32>, i32) borrows self { &mut self.cells }
}
fun main() {
mut h = Holder { cells = ([1, 2], 3) };
if h.slot() is (let xs, let n) {
h.cells.0.push(9);
print(xs.len()); // 2 — the capture copied what the match found
}
}
A call returning an owned value is not in this family: its result is storage of its own, with no second owner, so its captures bind without copying.
Two payload shapes are the exceptions to “a capture copies”. A capture of a
resource payload takes no copy (R1, §6.8): it is a loan of the payload, so
it names the value the match found rather than a second owner of it — the
timing above is what makes “the value the match found” well defined. A capture
that is itself a view (Some(let v) over an Option<&mut T>) takes none
either, for the reason a view exists: it aliases on purpose, and copying it
would mean writes through it stopped reaching the value it borrows.
All of this is the pattern capture. A closure capture is a different mechanism and copies nothing at all: §6.9.
6.2 Rule 2 — elision is an optimization, never observable
An implementation may skip a copy (reuse the storage) when no conforming program can tell, e.g. when the source is never used again. Elision must not change any program’s output; a live view of the source counts as a use. (This rule licenses the JS backend to alias under the hood; it grants programs nothing.)
6.3 Rule 3 — references are second-class views
&place / &mut place create a view: an alias of a place, readonly
or writable. Views are values of view type (&T, &mut T) but are
deliberately second-class; a view may not outlive the thing it views:
- A view may be a parameter (the caller’s place is lent for the
call), a short-lived local, or a return projecting a parameter
(§6.5). One vocabulary, position-dependent defaults:
&mut selfon a method,bump(&mut c)at a call site,x: &mut Tin a signature all carry the same convention. - A view may not be stored in a struct field, a collection element,
or a
Signal/Sharedpayload; may not be returned except through aborrowsprojection; may not be captured by a closure (§6.9); may not cross anawait(§6.6).
Mutating through a view writes the viewed place; reading its value
requires an explicit *. A view in value position (passed where a
value is expected, used as an operator’s operand, or bound to a value
type) is a compile error, never a silent coercion to the pointee (so the
(base, key) representation of a scalar view can’t leak); write *v to
copy the value out. Iteration by view (for e in &mut list) binds each
element as a view: assignment and field writes go through; *e reads the
element. The parameter conventions:
| Convention | Written | Data | Resource |
|---|---|---|---|
| bare | x: T | by value (a copy, rule 1) | a loan — no copy, no move (R3) |
| own | own x: T | by value, explicitly (documentation of intent) | a move (R3) |
| ref | &x / x: &T | readonly view | readonly view (a loan) |
| ref mut | &mut x / x: &mut T | writable view | writable view (a loan) |
The two columns are one rule read at two types, not two rules. A bare
parameter is a loan; for data a loan is indistinguishable from a copy —
nothing observes the difference, because the callee’s copy is private and a
resource’s is forbidden — so the data column states the copy the
implementation performs and the resource column states the ownership the
convention carries. own is the same story from the other side: it is the
convention that says “the callee takes ownership”, which for data is
performed by copying and for a resource is the move. R3 (below) is the
normative statement for a resource; this table is its by-convention index.
The consequence that catches people, spelled out because it is what the
“a copy” reading hides: a body may not move a bare parameter out — no
returning it, no own-passing it on, no consuming match of it — when the
parameter is a resource. A loan changes no ownership, so ownership cannot
leave through one.
Orthogonal to the conventions, mut x: T marks the binder mutable:
the callee may rebind and field-write its by-value copy (rule 1 copies
an aggregate at body entry), with nothing visible to the caller. To
mutate the caller’s value, use &mut. mut combines with neither
own nor a view — an own parameter is already the callee’s own storage
and is writable without it, so mut own x is a parse error.
6.4 Rule 4 — no invalidating mutation under a live view
While a view of a place is live, mutations that would invalidate the
view (replacing the aggregate that contains the place, removing the
element it points into, resizing past it) are forbidden. The compiler
enforces the statically-decidable half in full: reassigning the viewed
root (or an enclosing place), and any call that may advance the root’s
geometry: the callee’s inferred bumps verdict per parameter, so a
content-stable &mut callee (one that only writes fields or elements
through the parameter) passes freely. Views anchor at their origin roots
wherever they arise: a direct &place, a view-returning call (the
callee’s borrows positions mapped through the arguments), or a
wrapped-view match capture. A projection returned through a call is
policed exactly like the &place it came from.
Implementation note: the dynamic remainder (aliasing reached through
calls, container-internal invalidation) is tracked future work. When it
lands it enforces the same event set as the static rule above (§6.0’s
design invariant): a trap fires on a reassignment, a geometry change, or a
death under a live claim, never on a mere overlap of content writes, which
the static rule deliberately permits (aliased views, and writing through
them, are legal). Shared.read()/write() are the cell-level form of this
dynamic check (§6.7).
6.5 Projections: borrows
A function may return a view into one of its parameters: the one
sanctioned escape from rule 3’s return ban. The projected parameter is
named by a borrows clause, which is inferred when the body makes it
evident (a method returning a view of self needs no clause):
fun write(self): &mut T borrows self; // Shared::write — explicit
fun get(&mut self, i: i32): &mut T // inferred: borrows self
What is inferred is a projection, so the return type decides, exactly as
it does for the copy in §6.1: fun copy(&self): Holder { self } hands back a
value — the return copies it out of the loan — and projects nothing, so its
result is an ordinary owned binding (mut c = h.copy() is legal). Change the
return type to &mut Holder and the same body is a projection.
That agreement is exact for a forwarded parameter and conservative for the
other leaf shapes: a by-value return whose tail is &self.inner, a borrows
call, or a wrapped Some(&mut self.x) still projects at the call site, even
though §6.1’s copy has already taken the value out of the loan. The result is
therefore a value that is treated as a view — mut is refused on it, and rule
4 counts it live — which restricts more programs than it must and mis-copies
none.
At the call site the returned view obeys the same second-class rules, with the borrow anchored to the projected argument: the argument’s place is treated as viewed while the result is live (rule 4 applies to it). Returning a view of a local is always an error (it would dangle).
The clause is a set: a function whose branches return a view of
different parameters projects them all (borrows a, b), and the call
result anchors at every corresponding argument. Hover shows the inferred
clause on any projection, alongside its sibling effect bumps: the
per-&mut-parameter geometry verdict §6.4’s call rule fires on. A
&mut parameter the body only writes fields or elements through is
content-stable (absent from the clause; the owner’s epoch holds); one
the body may resize, insert into, remove from, or whole-reassign through
bumps. Both effects are inferred, never written (the explicit
borrows clause remains legal); an unknowable callee (a bodiless trait
signature, an untabled host function) is treated as bumping and, when
view-typed, as projecting every argument: the conservative direction.
Option<&T> is permitted as a return type for “a view, maybe” (map
lookups); the Some payload obeys the same anchoring. An Option<&mut T>
may also be built inline as a transient and matched in the same
expression: match Some(&mut a) { Some(let v) => … }, including the
conditional form match if c { Some(&mut x) } else { None } { … } and
forwarding a bare view parameter (match Some(p) { … } for p: &mut T).
Because the transient never outlives the match that consumes it, its
payload may view a local (unlike a returned projection). Binding the
same constructor to a let stores the view and is rejected.
6.6 Views and suspension
A view may not be live across an await. Between suspension and
resumption other code runs and may invalidate any place; rather than
extend rule 4 across turns, the language forbids the shape. Re-derive
the view after the suspension:
let row = &mut rows[index];
send(row.id); // suspends
row.text = "sent"; // ✗ error: view live across await
send(rows[index].id); // ✓ re-derive
rows[index].text = "sent";
This applies to every suspension point: calls to async functions
(implicitly awaited, §7), explicit await, and calls through async
closure values.
6.7 Library escape hatches (informative)
Shared<T> (one shared cell; read() copies, write() yields a
statement-scoped view of the contents) and Arena<T>/Handle<T> (stable,
generation-checked identities: handles are plain values, storable where
views are not) are std types built on these rules, not extensions of them.
They are §6.0’s dynamic regime: a Handle’s generation is its owner’s
epoch carried as data, and Arena.get answers Option<&T>. When the
counted tier (§6.8) gives Shared a runtime check, that check enforces the
same event set as rule 4: a reassignment, geometry change, or death of the
cell under another live view traps, while overlapping writes through the
cell do not (the reconciled trap law of §6.0; a write view is not
exclusive). See cells.
6.8 Resources and destruction
(Design: destruction.) Rule 1 copies values, and a
droppable value cannot survive copying: a copied file handle double-closes,
a copied refcount miscounts. Destruction is therefore not bolted onto the
data world: the world is partitioned. Data is everything above: copied
on binding, elided at last use, reclaimed by the host. A resource is a
small, explicitly-rooted class with affine discipline (one owner at a
time, no copies) whose owner’s scope end runs its destructor. This section
is the static death cell of §6.0: a resource’s move ends the source
binding’s epoch, and drop is its final event.
The class is two tiers. Tier 1, specified here and enforced on JS, is
unique resources: one owner, move-only. Tier 2 (Shared as a counted
resource, Weak, counted closure environments) is specified against the
future native arc and is out of scope for this section beyond the forward
references above.
The resource class
resourceis a declaration modifier, written inexternal’s position:resource struct S,resource external struct D,resource enum E.- Containment infers. An aggregate (struct, enum, tuple, or fixed
array
[R; n]) with a resource field, payload, element, or member type is a resource, recursively (theWire/Hashableall-fields machinery with the polarity flipped: any resource member marks the whole). Declaringresourceon such a type is allowed and checked; omitting it never hides resource-ness. - The modifier is required at leaves. An
external structis opaque, so a host-object resource (Database) must declare itself one. - Per-instantiation for generics.
Option<Database>is a resource instantiation;Option<i32>stays data. Resource-ness of a generic type is decided at each instantiation, like the platform and asyncness bits. Dropmay be implemented only for a resource type (see below); an impl on a data type is an error steering to addresource.
The affine rules
Move transfers ownership and leaves the source binding dead; loan is the
existing second-class view (self / & / &mut conventions, §6.3), which
changes no ownership and is policed by rule 4.
A note on the numbering, because two of them collide. R1–R12 below are the
affine rules — they are about ownership, and every one of them is about a
resource. The design record carries a second, unrelated R1–R8 in
transparent references, the view
surface §6.3 states here; the two namespaces are disjoint and R7 means “no
conditional moves” in one and “no rebinding a view” in the other. Where a
discussion spans both, write mR7 for this section’s and tR7 for the
view surface’s. Unqualified Rn on this page always means the affine rule.
-
R1: binding moves.
let b = a;transfers ownership; any later use ofais a compile error naming the move site. No copies ever fire for a resource. -
R2: overwrite drops. Assigning onto a place that still holds a resource drops the old value first, then moves the new one in. A write through a
&mutview is such an assignment: it mutates the pointee in place — that is how the write reaches the caller at all — so it destroys what it replaces even though the loan owns nothing. The two spellings are indistinguishable by design, because they write the same storage under different names:import std::drop::Drop; resource struct Guard { label: str } impl Guard with Drop { fun drop(&mut self) { print(self.label); } } enum Holder { Full(Guard), Empty } impl Holder { fun clear(&mut self) { self = Holder::Empty; } // drops the Guard } fun main() { mut viewed = Holder::Full(Guard { label = "viewed" }); viewed.clear(); // prints "viewed" mut owned = Holder::Full(Guard { label = "owned" }); owned = Holder::Empty; // and so does this }The loan asks no liveness question and needs none: it cannot move the pointee out (R5, R6), and a binding its owner already moved out of is dead, so lending it is use-after-move (R1). The drop runs the same per-type glue in the same order the owned spelling does.
The new value is computed before the old one is destroyed. “Drops the old value first” orders the destructor against the write, not against the right-hand side’s own evaluation: the assignment evaluates its new value, then destroys what the place holds, then stores. So a right-hand side that panics leaves the place holding its original value — still live, still owned, and dropped exactly once at the scope end — rather than a value the overwrite had already destroyed. Where the right-hand side has effects of its own, they are observable before the outgoing value’s
dropbody runs.The rule is read over the place, all the way down: writing over a resource-typed component — a field, a tuple element, a fixed-array element — destroys the outgoing value too, whether the aggregate is owned or reached through a view.
import std::drop::Drop; resource struct Guard { label: str } impl Guard with Drop { fun drop(&mut self) { print(self.label); } } struct Slot { held: Guard } fun refill(slot: &mut Slot) { slot.held = Guard { label = "replaced" }; // prints "replaced" } fun main() { mut slot = Slot { held = Guard { label = "held" } }; slot.held = Guard { label = "replaced" }; // prints "held" }A component asks no liveness question either, and for R5’s reason: a resource field is loan-only and cannot be moved out of a live aggregate, so a component place always holds a live value. Only the component’s own type decides — writing an
i32field of a struct that happens to hold a resource elsewhere destroys nothing. -
R3: parameters.
self/ barex/&x/&mut xare loans, unchanged (§6.3’s table is the by-convention index of this line);own xis a move, and for a resource only a move: anownargument that is not the binding’s last use is an error (where a dataownwould silently copy). The rule reads in both directions: because a loan changes no ownership, a body may not move a loaned resource parameter out — no returning it, noown-passing it on, no consumingmatchof it. Doing so would hand the caller a second owner while the caller’s binding stays live and still drops at its scope end, destroying one value twice.ownis the only convention a body may consume, and a consuming method therefore declaresown self:o.unwrap()is a move ofo, so a later use ofois use-after-move andois not torn down at scope end. A bareselfreceiver stays a loan (db.exec(..)never consumesdb). -
R4: returns move out, including through
if/matchtails (a diverging leg is exempt). A tail move-out is still a move on that path only — R7 reads the arms against each other, so producing a different binding from each arm is a conditional move, not two independent returns. -
R5: fields. A struct literal moves resources in. A resource field is read only by loan (
self.db.exec(..),&mut self.db); moving it out of a live aggregate is rejected; v1 has no partial moves. The sanctioned partial move isOption(below). Writing over a resource field is permitted, and is R2’s overwrite: the outgoing value is destroyed first. -
R6: match consumes. Matching a resource by value consumes the subject; pattern captures move the payloads into the arm, and each capture owns what it took — it is destroyed at the end of the arm that bound it, in reverse capture order, unless it is moved onward first. The same holds for a
letpattern (let (handle, count) = pair), whose captures drop at the declaring scope’s end. Matching a loan (match &self.state, and thex is Some(let v)test) inspects without consuming: the subject keeps ownership and destroys the payload itself, so its captures own nothing — and, owning nothing, they may not be consumed. This is R3’s “a loan changes no ownership” read in the capture position:own-passing a loaned capture, returning it, or matching it by value would hand a second owner the payload the subject still destroys at its own scope end. The fix is to consume the subject (match xwithout the&), not to redeclare the capture — a capture carries no convention to change. -
R7: no conditional moves. A binding must be moved on every path through a scope or on none; moving it on one path only is an error. This keeps end-of-scope ownership static: there are no runtime drop flags in v1. Branch tails are paths too. R4 makes each arm’s tail a move-out, and the arms are alternatives, not a rejoin — so
if flag { x } else { x }is moved on every path and fine, whileif flag { first } else { second }moves each binding on one path and abandons it on the other, and is the same error. The one exemption is a binding that, on the path in question, provably carries no resource payload: after a failedx is Some(_)testxcan only beNone, which has nothing to destroy, so leaving it un-moved there leaks nothing. The exemption is granted only on proof — an enum whose every variant carries a payload gets none. -
R8: no moves in repeatable interiors. Moving a binding declared outside a loop from inside its body is an error (the move would repeat).
-
R9: closures and spawns cannot capture resources. Capturing one would give the closure a second owner. Injected
context-clause bodies receive resource parameters as loans (parameters are per-call, not captures), sonursery(|n| ..)-shaped APIs are unaffected. A closure referencing a module-level resource is likewise exempt: a module global is loan-only and lives for the process (see Module-level resources never drop, below), so the closure can never own it and no second owner is created: the reference is a per-call loan, exactly like a parameter. Captures of a local or a parameter stay rejected. -
R10: no resource elements in the native containers.
List/Map/Setand every external generic (Shared,Task,Promise,Context) reject resource type arguments in v1: their internals are host code the move checker cannot see.Optionis the sanctioned container (it is a Vilan enum, checkable under R11). The rule is read per instantiation, not per written head: a resource that reaches a container through a generic aggregate’s member is rejected the same way, soSignalCell<Database>— whose storage is aShared<T>— is refused exactly asShared<Database>is. Holding the resource in a struct field of your own is the sanctioned alternative, and stays legal.The set is closed over the standard library’s own internals, not just the surface it publishes: the raw
NativeMapthatMapandSetare built on rejects a resource the same way, so the rule holds however the raw layer is reached. Because aMap<K, Database>offends at both heads — its own and theNativeMapinside it — and one mistake is one diagnostic, the refusal is reported at the head you wrote, and never a second time at the storage behind it. That collapse is scoped to the one mistake: it silences only the containers built out of the very type parameters the reported refusal already carried. A container the body builds out of a different parameter is an independent offense nobody has been told about, and is still reported.It is read per instantiation whatever the type’s provenance: an inferred container is a container. Deleting the annotation changes nothing, and neither does never writing one —
mut arr: List<Guard> = [Guard { .. }]; // rejected mut arr = [Guard { .. }]; // rejected, identically let cell = Shared::new(Guard { .. }); // and so is this fun stash<T>(own value: T) { let items = [value]; } // rejected at T := Guard— because containment is a question about a type, not about a spelling. The nesting is read the same way: a container inside a container, a tuple, or a fixed array is found wherever it sits. A fixed array of resources (
[Guard; 2]) is not a container in this sense: it is a value aggregate, a resource by containment, and it drops its elements in reverse order. -
R11: generics must be move-clean per instantiation. Instantiating a type parameter with a resource type re-checks the instantiated body under the affine rules (T := the resource): every T-typed value is used at most once as a move, with no captures and no copies.
Option::unwrap(own self): Tpasses; a body that reads its parameter twice fails at the instantiation site, not inside std. For anown Tparameter the rule tightens to exactly once (a generic body is emitted once and so cannot run an instantiation-conditional destructor; zero moves would leak). The tightening is not about parameters but about destruction: since a generic body can destroy nothing, noT-typed value in it may still own at the end of its scope — not a parameter, not aletlocal, and not a pattern capture that took a consumed subject’s payload (R6). Each is the same leak and is rejected at the instantiation site. The consequence worth stating: a combinator that hands a payload to a closure and then discards it — the closure only loans it — cannot be resource-clean, whatever its receiver convention.Read the same sentence at R2’s seam and it reaches writes, not only scope ends. Overwriting a
T-typed place destroys the outgoing value, and a generic body cannot run that destructor either — so a body that writes through a&mut T, or over aT-typed component, is rejected at a resource instantiation:fun set<T>(slot: &mut T, own value: T) { slot = value; } // rejected at T := a resource fun clear<T>(slot: &mut Option<T>) { slot = None; } // and so is thisA loan owns nothing, but it is not excused either: the value the write replaces belongs to the caller, and somebody must destroy it. The concrete spelling of the same body is fine —
fun set(slot: &mut Guard, own value: Guard)knows the type and emits the drop — which is the steer: move it out on every path, or take a concrete type. -
R12: no coercion to
any. A resource passed whereanyis expected is an error (print(db)included):anyis a data sink, and the discipline must not launder away. Debug-print the fields instead.
Destruction — the Drop trait
trait Drop {
fun drop(&mut self);
}
The body cleans up through the &mut self loan; the compiler destroys the
fields afterward, in reverse field order. &mut self is the exact and
only accepted shape: a by-value self could move the value out and keep it
alive (resurrection), a &self receiver cannot run the mutating teardown,
an extra parameter cannot be supplied by an inserted call, and a
value-returning body is rejected. Two further restrictions are enforced:
dropis synchronous. Anasyncor awaitingdropbody is rejected: teardown runs synchronously in v1. (Cancel owned tasks through anOwnedNursery, whose owndropcancels them.)dropis context-free. Adropbody that requires an ambient context (for example, one that writes aSignal, which threads the turn as a hidden context argument) is rejected: a destructor’s call sites are scope exits, which thread no context.
A resource without a Drop impl is legal: containment alone still enforces
moves and destroys the resource’s fields. Drop is distinct from the
cooperative Disposable protocol, which is the data-world teardown hook
(subscriptions, owners) and is capture-based (exactly why it is not a
resource mechanism).
Drop timing and order
(amended 2026-08-28 — disposal moved from scope end to last use;
proposal/lifetimes.md §6. The previous law read “at the owner’s scope
end, still-owned resource locals drop in reverse declaration order”.)
A still-owned resource is destroyed after its last use — the last
statement of its declaring scope that reads it — and simultaneous
discharges run in reverse declaration order, so two resources last read
in the same statement are destroyed second-declared-first, as they always
were. A pattern capture (R6) counts as a local of the arm that bound it,
declared before that arm’s own statements. A value’s own drop body runs
before its fields, and the fields drop in reverse field order; an
enum’s payload drops with the value. Every exit runs drops:
fall-through, ret, jump break, jump continue (out of the scopes they
leave), and panic unwinding, because the region between an acquisition and
its last use lowers to a try/finally and every exit flows through the
finally. Concrete own resource parameters drop at their last use like
locals (a generic own T is required to move out instead, per R11) — and
the same split holds for pattern captures: a concrete one drops at its
last use, while inside a generic body no T-typed capture may be left
owning at all.
Three clauses make “its last use” total:
- Nothing reads it ⇒ it drops at its declaration. A handle the program
never names again is released immediately, not at a scope end that a
mainwhich never returns would never reach. Binding a value is how the language says keep this; reading it is how it says still. - A loan extends its owner. A view keeps the storage it names alive to
the view’s OWN last use, so an owner is never destroyed under a live
projection.
let v = &mut holder; use(v)holdsholdertouse(v), even thoughholder’s own last read was the&mut. - A last use inside a branch or a loop is the branch’s or the loop’s. The drop lands at the join, which every path reaches, so the arm that read the resource and the arm that did not both release exactly once — and no runtime flag decides which (R7’s doctrine, intact).
Where the compiler cannot stand behind an answer — a binding read from more than one region, a loan it cannot follow to its end — the resource keeps the scope-end teardown, which is the previous law. The fallback is never a guess.
One rule pushes a drop later than the last use. The region between an
acquisition and its teardown is a block, so it cannot close while a name
declared inside it is still read afterwards — the drop waits for that read
instead of putting the name out of scope. In let r = …; let size = r.size(); print("between"); print(size), r’s last read is r.size() but
its teardown runs after print(size), because size lives in the region
r opened. This holds at every depth: inside an if arm, a match
leg, a loop body or a nested block, exactly as at a body’s top level.
- A loan takes no teardown. Only an owner destroys. A view binding of a
resource (
let v = &mut holder) names storage another binding still owns, so its scope end drops nothing — its referent is destroyed once, where the owner’s scope ends. This is the same sentence R2’s view-write half reads in the other direction: a loan owns nothing, so it neither destroys at its own scope end nor is excused from destroying what it overwrites. - Module-level resources never drop. A top-level
letresource has process lifetime (the serve-forever server’sDatabase). It is consequently loan-only: moving it into a local, anownargument, ordrop(x)would hand a process-lifetime resource to a droppable owner and is rejected; method calls and&/&mutpassing are accepted. - Panic during unwind. A
dropthat panics while a panic is already unwinding replaces the in-flight error (JSfinallysemantics; a native backend would abort). - Across
await. Owning a resource across a suspension is legal: frames own their locals, and §6.6’s no-view-across-awaitrestriction is about loans, not ownership. Under cancellation a bridged operation rejects, the frame unwinds, and drops run. - Exit after finally. When a value-returning
mainowns a resource, its process exit is sequenced after the teardownfinallyruns, so drops are never skipped by process termination.
Temporaries
(added 2026-08-28 — proposal/temporary-drop.md, backlog C11. A resource
born and consumed inside one expression was previously neither destroyed nor
rejected.)
A resource-typed value that is neither bound nor moved is an owning temporary: it is owned by the statement in which it is constructed, and destroyed at that statement’s end, before any enclosing scope’s drops and in reverse construction order among the temporaries of that statement. This is the same law read at its narrowest — a temporary’s last use is its statement.
print(File::open(path).stat().size); // the handle closes here
let held = File::open(path); // this one lives to ITS last use
print(held.stat().size);
A resource that is bound (let f = …) or moved (into an own parameter,
into drop, into ret, into an aggregate) is not a temporary and is
unaffected. try/finally is emitted per resource-owning scope or
statement; only scopes and statements that own resources pay.
R7, extended to temporaries. A resource temporary must be constructed on
every path through its drop region or on none. A resource constructor on a
conditionally-evaluated operand — the right of && or || — is rejected:
there is no statement that can destroy it, and the alternative would be v1’s
first runtime drop flag. This refuses a spelling, never a program; the fix
is a let, and the diagnostic names it. A branch arm needs no such refusal:
an arm is a scope with statements of its own, so a temporary there has a
statement to belong to.
Externs and retention
(added 2026-08-28 — proposal/lifetimes.md §6.4.)
An [extern] receives loans under the same conventions any function does
(R3), and those loans are call-bounded: the compiler assumes the host
reads what it is handed only until the call returns, which is what lets the
caller’s binding be destroyed at its last use. A host that keeps what it
is handed — an event listener, a stashed value, a request body — breaks that
assumption, so the declaration must say so:
[extern(method, "addEventListener", retains)]
external fun on(self, event: str, handler: || void): void;
retains is a trailing flag on the [extern(..)] attribute, and it
composes with every binding form. An argument to a retaining extern keeps
its liveness for the whole of its binding’s scope — the conservative
envelope. Module-level bindings are exempt by construction: they never drop
at all.
An argument with no binding is exempt too, and for the same reason read the other way: a resource temporary handed to a retaining extern is not its statement’s to destroy, because there is no scope that could hold it open past the call. Such a temporary leaks rather than being freed under a host that is still reading it — the direction these rules always fail toward.
Option.take and Option.replace
Moving out of a place must leave a valid value behind. One intrinsic pair on
Option<T> provides the sanctioned partial move:
impl Option<type T> {
fun take(&mut self): Option<T>; // Some(v) -> None here, Some(v) out
fun replace(&mut self, own value: T): Option<T>; // new value in, old contents out
}
replace takes its new value own because the slot keeps it: only own
transfers (R3), so a bare declaration would have left the caller owning a
value the slot also owned — readable after the move, and destroyed twice.
self.slot.take() is how a resource field leaves a live aggregate (R5), and
match opt.take() { Some(let c) => drop(c), None => {} } is the
conditional-teardown idiom R7 pushes toward. Both are ordinary std surface,
useful for data too.
Early teardown — drop
fun drop<T>(own value: T) {}
Moving a value into drop destroys it at that (immediate) scope end instead
of waiting for the owner’s scope to close. The call is rewritten at each
site by the concrete argument type: for a resource it lowers to that type’s
destructor; for plain data it is a no-op that consumes the argument for its
effects. There is no public close() to keep in sync with a destructor;
drop(x) is the early form. Inside a generic body a drop(x) on a value of
a still-abstract type T has no concrete destructor (a generic body is
emitted once, erased), so R11 rejects it under a resource instantiation
(“whose erased body has no concrete destructor; destroy at a concrete type, or move the value out to the caller”); a data
instantiation keeps the legitimate no-op consume.
drop(x) moves the teardown earlier; it does not remove the scope’s safety
net. The owner’s scope end still covers the window between the acquisition
and the drop(x), so a panic that never reaches the explicit call releases
the resource on the way out exactly as an implicit scope-end drop would — and
the two never both fire, so a fall-through path destroys exactly once.
OwnedNursery
OwnedNursery (std::task) is the resource-owner for object-lifetime
background work that no function-scoped nursery can hold. It wraps a
Nursery (which stays data, the ambient handle; Context<R> is
R10-rejected, so ownership lives only in the wrapper). enter(body) runs
body with the owner’s nursery established as ambient (every task spawned
in the body’s dynamic extent registers with the owner) but, unlike
nursery, does not join: it returns the body’s value as soon as the body
settles, leaving the spawned tasks running under the owner. Its Drop
cancels the owned nursery, so dropping the owner (at scope end or via
drop(owner)) aborts in-flight bridged IO.
Its nursery runs in detached mode: because nothing ever joins the owned children, a child failure that is not a cancellation echo takes the free-task reporting path (console, with the spawn origin) rather than being stored for a join, and a child does not cancel its siblings: ownership is lifetime, not fate-sharing. Cancellation echoes stay silent.
Services and resources
A [service] struct that owns a resource field is itself a resource by
containment, and its generated dispatcher builds per-[rpc] handler
closures that capture self, which R9 forbids. R9’s module-level exemption
does not apply here: self is a local store value, not a module global, so
the capture is a genuine second owner. This collision is by design: the
sanctioned shape is the module-level idiom: hoist the resource (a
Database) to a module-level let, and let the store hold only its reactive
state (plain data) and reach the resource by loan. The module-level resource
is process-lifetime and never drops, and reachability keeps its initializer
out of the client bundle.
Honesty limits
The following are recorded limits the model does not promise, not bugs:
- A never-settling await leaks its frame’s drops. An unbridged await that never resolves (nothing cancels it) leaves its frame, and the resources the frame owns, undropped. This is structured concurrency’s concern, not the memory model’s.
- R11/R12 diagnostic residues (completeness, not soundness). The
per-instantiation move scan descends into direct lexical closures only,
so a double move of
Tinside a nested closure is not separately flagged (its capture is still caught); dispatched (trait-typed-receiver) callees are not re-discovered by R11’s scan or R12’sany-coercion check, matching the standing convention that convention checks skip dispatched callees; R11’s per-instantiation re-check is the net under that residue. A cross-file instantiation may anchor its primary span imprecisely (the body note carries the correct source). A module-initializer global → global move is not scanned (benign, since module globals never drop).
6.9 Closures capture bindings
(Design: the lifetime model §4.)
A closure captures bindings, not values. A captured binding is the same binding: a write on either side is visible on the other, and a later rebinding is visible through the capture. This is the one alias in the chapter that is not a claim (§6.0) and needs no hatch: the environment holds the binding itself, not a path into an owner, so there is no epoch to outlive.
Read against §6.1 the contrast is exact, and it is the whole rule. A pattern capture is a binding, so it copies, and it copies at the moment the pattern matches. A closure capture is not a new binding at all, so it copies nothing and has no moment to copy at:
fun main() {
mut label = "before";
let show = || label; // captures the binding, not "before"
label = "after"; // a rebinding installs a fresh value
print(show()); // after — the capture is the same binding
}
§6.1’s other three spellings behave the same way through a capture, where
a pattern capture sees none of them: an assignment through a view, a
component write (box.n = 9), and a &mut self call all reach the
captured binding.
The one copy a closure takes is §6.1’s return copy, and a closure
takes it per call: a place the closure returns, rooted at a binding it
did not declare, is storage the closure’s own frame does not own, so the
return copies it out. || items hands back an independent list on every
call, and pushing to one is invisible to items and to the next call’s.
Three kinds of value do not take part, each for a reason already stated elsewhere:
- A resource may not be captured (§6.8’s R9): the capture would be a second owner. A closure that references a module-level resource loans it per call instead — a loan is not a capture.
- An ambient context value is snapshotted when the closure is created (§8.4) — the one true capture-time copy in the language, so a deferred body reads the context it was written in rather than the one it happens to run in.
- A view may not be captured (§6.3). A view is second-class and a
closure is not: the closure may outlive the frame its place lives in,
and lexical liveness — the surveyable interval §6.0’s static regime
proves against — says nothing about when a closure body runs. The two
ways to write the same code are to read the value out (
*v) before the closure, or to take the view as a closure parameter, which is a per-call loan exactly like a function’s. The enclosing function’s own&/&mutparameter may be named inside a closure — it views the caller’s place, which outlives the call — but that closure is then second-class itself and may not escape, by §6.3’s list read of the closure.
A closure’s environment keeps the bindings it captured alive for as long
as the closure itself lives, which is why the three exclusions are the
shape they are: what an environment cannot carry is a claim — a path
into an owner it does not hold. Two closures that must share mutable
state neither of them declared hold a Shared cell (§6.7), which aliases
by design and survives every copy the model takes; capture is not a
substitute for it, and not a hatch.
Honesty limit. The view exclusion is enforced at the capture for view
bindings and by the escape rule for a captured view parameter, and
the escape rule does not follow a closure through an ordinary call: a
closure over a &mut parameter, handed to a function that stores it,
leaves the frame whose place the view names. Nothing in std does this,
and the shape needs the closure-escape analysis §6.4’s dynamic remainder
is future work for.