1 A program that doesn't compile
This looks innocent. We create a string, hand it to another variable, and print it:
fn main():
msg = "hello"
other = msg
print(msg)
Which types move and which don't? Only types that manage a resource move. Everything else is a Copy type — assignment just copies the bits, and both bindings stay valid:
fn main():
mut n = 42
m = n
print(int_to_str(n))
print(int_to_str(m))
4242
fn main():
msg = "hello"
other = msg
print(msg)
[E0020] use of moved value `msg`
2 Pick a fix
The right fix depends on one question: does ownership of the value need to leave the caller? Pick a card — each shows the corrected program and why it works.
fn print_twice(text: str):
print(text)
print(text)
fn main():
msg = "hello"
print_twice(msg)
print(msg)
hellohellohello
&, no lifetime. msg is
lent to print_twice for the duration of the call and
is fully yours again when it returns. This is the fix most of the
time: in Rust you'd reach for &str here, and the
borrow checker would fight you about it later. In Ryo the borrow
is scoped to the call, so the fight never starts.
fn add_suffix(inout text: str):
text = text + "!"
fn main():
mut msg = "hello"
add_suffix(&msg)
print(msg)
hello!
inout in the signature and
& at the call site. The binding must be declared
mut — mutability is always visible. Ownership never
moves: msg is still yours afterwards, now with a
! attached. Under the hood this passes a pointer —
no copy of the string is made.
fn consume(move text: str):
print(text)
fn main():
msg = "hello"
consume(msg)
hello
move in the
signature and the transfer is explicit and deliberate: the caller
can see it, and the compiler enforces that the old binding is
never touched again. If you add print(msg) after
this call, you get the same E0020 — on purpose.
3 A second error: two borrows collide
Use-after-move is one error family. The other is aliasing — two borrows of the same value fighting inside a single call. Say we have a point and want to clamp both coordinates in one go:
struct Point:
x: int
y: int
fn clamp2(inout a: int, inout b: int, hi: int):
if a > hi:
a = hi
if b > hi:
b = hi
fn main():
mut p = Point{x=5, y=9}
clamp2(&p.x, &p.y, 10)
print(int_to_str(p.x))
struct Point:
x: int
y: int
fn clamp(inout v: int, hi: int):
if v > hi:
v = hi
fn main():
mut p = Point{x=5, y=9}
clamp(&p.x, 10)
clamp(&p.y, 10)
print(int_to_str(p.x))
print(int_to_str(p.y))
59
4 Or just restructure
The compiler's own note points at a fourth option, and it's often the best one: use the value before you move it.
fn main():
msg = "hello"
print(msg)
other = msg
print(other)
hellohello
5 Why bother? No GC, ever
All of this ceremony buys one thing: deterministic destruction. Every value's resources — heap buffers, sockets, file handles — are released at a point the compiler can see: at last use, or at the end of the scope. There is no garbage collector to pause your program, no finalizer running late, no refcount traffic on the hot path.
shared[T] — refcounting is a tool you reach for, not
a tax you always pay. (Written into the spec; the refcount
runtime is still being built.)
6 Where Ownership Lite draws the line
Honesty time: the same rules that delete the lifetime vocabulary also forbid one fundamental capability — a borrow that outlives the call it was created in. It can't be returned, stored, or sent anywhere. Four shapes of programs are unbuildable in the safe model because of exactly that one sentence — and for each, here's what's enforced in the compiler today versus what's already written into the spec as the planned way out:
Enforced today the compiler rejects this code right now. Spec'd · in progress designed and specified, but not yet implemented — the blocker is named on each card.
The parser-combinator shape — fn parse(input: strview) -> (strview, strview) —
can't exist: free functions can't return views of owned values
(Rule 5). Parsing a huge file zero-copy works inside
one call; the results can't come back to you as borrows.
Retaining views (spec §5.7): a view sliced from
a shared[T] store keeps the buffer alive, so a
parser library will hand borrowed results back — and
structs will be able to hold them. Blocked on the
shared[T] refcount runtime. Cost: a frozen input
and one atomic retain per view.
An iterator/filter chain saved in a struct and consumed later holds a borrow across arbitrary time. Ryo's views are tied to their owner's scope and can't be stored — pipelines are callback-shaped instead.
Rewrite spec'd · ships with iterators
No language escape is planned — sugar can't extend a borrow's
life, and the general fix is the lifetime machinery Ownership
Lite exists to avoid. The mechanical rewrite is spec'd: chain
adaptors in one statement and collect(), or store
a closure over owned data and run it on consumption.
Streaming stages over a channel arrive with the concurrency
runtime.
Read-only stack lending is in the spec: inside a
task.scope, child tasks may capture by immutable
borrow — the scope joins every child before your frame ends, so
the borrow provably never outlives it. That's Rust's
thread::scope shape for readers. Blocked on the
concurrency runtime.
Mutable lending (rayon-style par_iter
writing into disjoint chunks) stays out — proving disjointness
across tasks is full borrow checking. The single blessed
exception is std.slice.split_mut: disjoint chunks
by construction, stdlib-internal unsafe as the
proof, each task.scope child capturing one chunk.
A struct that owns a buffer and holds views into itself (editor
piece tables, arena+reference designs). Rules 5 and 6 together
forbid it — a retaining view's owner is always an external
shared[T] cell, never the struct itself.
The escape hatch is manifest-gated unsafe blocks
(spec §17) — the same tool Rust requires (Pin +
unsafe) for most of these types. Lands with the
FFI/unsafe work. The common case has a safe rewrite: IDs or
handles into a shared[T]-owned buffer.
Not on this list: disjoint field borrows, borrowed graph edges,
interior mutability. Those change how you write the
program — split the call, use IDs, opt into shared[T] —
never whether you can. Every application has a mechanical
rewrite; there is always an escape hatch for the rest.
The one real casualty — and its rescue plan
Applications can always be restructured, so Ryo can build any of
them. The hard case is the zero-copy parsing library
ecosystem — libraries where the borrowed result
is the product. That shape is impossible over owned
input today — but it isn't abandoned: retaining views (spec
§5.7) bring it back over shared[T] input, at the
price of a frozen buffer and one retain per view. Until the
refcount runtime lands, that ecosystem waits — by design, not
by oversight.
7 The decision rule
Everything above collapses into one question:
default
inout
inout in the signature, &x at the
call site, and the binding declared mut — mutation
is always visible.
reorder
move
move in the signature — store it, send it, consume
it. The call site stays plain; the transfer is explicit and the
compiler enforces it.
No lifetimes appear anywhere in this page — not because we're
hiding them, but because they can't occur: borrows never escape a
call, so there's nothing to annotate. When you need shared
ownership that outlives a call, that's what shared[T]
(reference counting) is for
Spec'd · in progress
Next steps: the language reference, or the ownership chapter of the spec (§5).
The seven rules
Everything on this page is an instance of seven formal rules from the spec (§5.3). Rule 7 is the one the aliasing error cites:
| Rule | In one line | |
|---|---|---|
| 1 | Assignment and return default to move | Owning types invalidate the source binding — E0020 |
| 2 | Parameters default to immutable borrow | No annotation anywhere; the caller's binding stays valid |
| 3 | Mutable borrows are always explicit | inout in the signature, & at the call site, mut binding |
| 4 | move parameters override the default |
The transfer is declared in the signature; the caller's binding dies |
| 5 | Functions cannot return borrows | Returns are always owned — retaining views over shared[T] excepted (spec'd) |
| 6 | Structs cannot contain references | Fields are owned, shared[T], retaining views, or IDs — never &T |
| 7 | One writer or many readers — borrows scoped to calls | One inout or many borrows per call, never both — E0032 |