Four ways to fix any ownership error — and the one rule behind
them.
Ryo gives you Rust-level memory safety — no dangling pointers, no
use-after-free, no garbage collector — without lifetime
annotations. The whole model fits in one sentence:
Functions borrow by default; assignment and return move by
default.
Because borrows live only for the duration of a single call, the
compiler always knows when they end — so there are no lifetimes to
write. But you will still meet the ownership rules, and when you do,
there are four ways to fix it. Every example below is verified
against the actual compiler.
1 A program that doesn't compile
This looks innocent. We create a string, hand it to another
variable, and print it:
broken.ryo
fn main():
msg = "hello"
other = msg
print(msg)
ryo run broken.ryo
[E0020] Error: use of moved value `msg`
╭─[ broken.ryo:4:8 ]
│
4 │ print(msg)
│ ─┬─
│ ╰── use of moved value
│
3 │ other = msg
│ ─┬─
│ ╰── value moved here
│
Note: help: consider using the value before the move, or pass by default (borrow) instead of `move`
What just happened?str is an ownership type: it manages a heap
buffer. Assignment of an ownership type moves it —
other = msg transfers ownership of the string to
other, and msg is left empty. Printing it
afterwards would be a use-after-free, so the compiler stops you at
compile time.
Which types move and which don't? Copy is a property of the type:
int, float, bool — and small
structs containing only Copy types — are Copy types.
Assignment just copies the bits, and both bindings stay valid.
Everything else moves:
Copy types — int, float, bool
copies.ryo
fn main():
mut n = 42
m = n
print(int_to_str(n))
print(int_to_str(m))
output
→42→42
Ryo's print emits no trailing newline — each
→ marks one print call.
Move types — str, list[T], map[K, V]
broken.ryo
fn main():
msg = "hello"
other = msg
print(msg)
compiler
[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?
Four fixes, four cards — each shows the corrected program and why it
works.
Why it works
Function parameters borrow by default — no
annotation, no &, 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.
fix_2_inout.ryo
fn add_suffix(inout text: str):
text = text + "!"
fn main():
mut msg = "hello"
add_suffix(&msg)
print(msg)
output
hello!
Why it works
When the callee needs to modify your value, borrow it
mutably with 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.
Why it works
Sometimes ownership really should change hands — the callee
stores the value, sends it elsewhere, or consumes it to build
something new. Mark the parameter 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.
fix_4_restructure.ryo
fn main():
msg = "hello"
print(msg)
other = msg
print(other)
output
→hello→hello
Why it works
Ownership errors are about order, so reordering is a
legitimate fix: use the value before you move it. Print while you
still own the value, then move it. The same trick scales up: when
one call borrows part of a value and another part needs mutating,
hoist the read into its own statement first — borrows end at the
end of each call, so splitting statements splits the borrows.
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:
alias_broken.ryo
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))
ryo run alias_broken.ryo
[E0032] Error: cannot borrow `p` as mutable more than once in the same call
╭─[ alias_broken.ryo:13:2 ]
│
13 │ clamp2(&p.x, &p.y, 10)
│ ─────────┬─────┬──────
│ ╰────────────── first mutable borrow here
│ ╰──────── second mutable borrow here
│
Note: help: a value can have one mutable borrow OR many immutable borrows in a call, never both (Rule 7)
What just happened?
Borrows in Ryo are whole-value and call-scoped:
&p.x borrows all of p for the duration
of the call, so &p.y in the same call collides with
it. Rust's borrow checker splits field borrows and would allow this;
Ryo deliberately keeps the rule coarse — the compiler stays simple,
and the fix is always mechanical.
alias_fixed.ryo
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))
output
→5→9
Why it works
A borrow dies the moment its call returns — so splitting one call
into two splits the borrows. The same trick handles the read/write
overlap: when a call needs to read a value it also borrows
mutably, hoist the read into its own statement first. One mutable
borrow or many immutable borrows per call — never both —
and the borrow never outlives the call.
4 The decision rule
Everything above collapses into one question:
Does ownership of the value need to leave the caller?
Ownership leaving the caller?Do thisCaller afterwards
Parameters borrow by default — no annotation, no &,
no lifetime. The borrow dies when the call returns and the binding
is fully yours again. If in doubt, do nothing.
Mutation is a borrow, not a transfer: under the hood this passes a
pointer, so no copy is made — and the mut binding
keeps mutability visible at every call site.
Ownership errors are about order: use the value first, move it
second. Borrows end at each call, so splitting statements splits
the borrows.
The call site stays plain; the transfer is declared in the
signature and the compiler enforces that the old binding is never
touched again. Say it in the signature, never at the call.
Click a row for the why; the fix → link jumps to its fix.
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