Interactive walkthrough

Ownership Lite

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)

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))
Move types — str, list[T], map[K, V]
broken.ryo
fn main():
	msg = "hello"
	other = msg
	print(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.

fix_1_borrow.ryo
fn print_twice(text: str):
	print(text)
	print(text)

fn main():
	msg = "hello"
	print_twice(msg)
	print(msg)
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)
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.
fix_3_move.ryo
fn consume(move text: str):
	print(text)

fn main():
	msg = "hello"
	consume(msg)
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)
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))
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))
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?

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

Next steps: the seven formal rules, the why behind the model, and where Ownership Lite draws the line in the language reference, or the ownership chapter of the spec (§5).