Ryo Reference
Ryo /ˈraɪoʊ/ is a statically-typed, compiled language that combines Python-style syntax with the memory safety of Rust and the concurrency model of Go. This reference documents the language as implemented in the current pre-alpha. Every example below compiles and runs today. Features not yet available are gathered in What's Next and never presented as working.
Variables & Mutability #
Bindings are introduced without a keyword — just a name and a value. They are immutable by default. The compiler infers the type from the initializer; you may also write an explicit annotation.
fn main():
name = "Alice" # inferred as str, immutable
count: int = 10 # explicit type annotation
mut total = 0 # mutable variable
total = total + count
# name = "Bob" # error: cannot assign to immutable binding
name = expr— immutable, type inferred.-
name: Type = expr— immutable, explicit type. -
mut name = expr— mutable; may be reassigned.
No let keyword
Ryo drops the declaration keyword entirely. The presence of
mut is the only thing that makes a binding
writable, mirroring Python's readability while keeping types
static.
Primitive Types #
Ryo has a small set of primitive types. Today int is
always 64-bit signed and float is always 64-bit
IEEE 754. Float literals take the usual decimal and
scientific forms (3.14, 1.23e-10,
1e5) and may use underscore separators
(1_000.5).
| Type | Description |
|---|---|
int |
64-bit signed integer (i64). |
float |
64-bit IEEE 754 floating point
(float64).
|
bool |
Boolean — exactly true or false.
No implicit conversion to or from int.
|
str |
Owned, heap-allocated UTF-8 string. Grows and shrinks when
bound to a mut variable.
|
bytes |
Owned, heap-allocated byte buffer. Grows and shrinks when
bound to a mut variable; slicing yields a
zero-copy bytesview.
|
void |
Unit type. The implicit return of functions that produce no value. |
Width types are planned
Explicit sizes (i8–i64,
u8–u64, usize,
float32) and char are specified but
not yet implemented. Writing i32 today is a parse
error — use int.
Integers & Arithmetic #
Integer literals may use underscores for readability
(1_000_000) and the standard 0x,
0o, 0b prefixes. Division between two
integers truncates toward zero, like Python's //.
fn main():
print(int_to_str(7 / 2)) # 3 — integer division truncates
print(int_to_str(7 % 2)) # 1 — remainder
print(int_to_str(0xFF)) # 255 — hex literal
mut counter = 0
counter += 10 # compound assignment
counter -= 3
print(int_to_str(counter)) # 7
print() renders numbers bare
print() accepts int,
float, and bool directly —
print(42) writes 42. For custom
formatting, convert first: int_to_str(n),
float_to_str(x), bool_to_str(b).
Booleans & Logic #
The bool type has two values, true and
false, produced by comparison operators. Logical
operators and, or, and
not are words, not symbols, and
and/or short-circuit.
| Operator | Name | Description |
|---|---|---|
and |
AND | True if both operands are true (short-circuits). |
or |
OR | True if either operand is true (short-circuits). |
not |
NOT | Negates a boolean value. |
fn is_valid(x: int) -> bool:
return x > 0 and x < 100
fn check_access(is_admin: bool, is_owner: bool) -> bool:
return is_admin or is_owner
Strings #
The str type is an owned, heap-allocated UTF-8
string. A binding is read-only unless declared mut.
Concatenate with +.
fn main():
mut greeting = "Hello"
greeting = greeting + ", World!"
print(greeting) # Hello, World!
print("tab\there\n") # escape sequences
print("quote: \"hi\"\n")
Escape Sequences
| Escape | Meaning |
|---|---|
\n |
Newline |
\r |
Carriage return |
\t |
Horizontal tab |
\\ |
Backslash |
\" |
Double quote |
\0 |
Null byte |
Slices
s[start:end] yields a strview — a
read-only, zero-copy view into the string's buffer.
Shorthands: s[start:], s[:end],
s[:]. Bounds are byte offsets; they panic at
creation when out of range, reversed
(start > end), or not on a UTF-8 boundary.
Views are non-escaping: they can't be returned or stored
past the current function, and the owner is frozen while
a view is live. Views also pass to ordinary
str parameters — a call-scoped re-borrow, no
copy.
When viewed data must outlive the call — returned, stored,
or moved — str(view) materializes an owned
str copy (allocates + copies). It is never
implicit: x: str = view stays a type error,
while x: str = str(view) is the explicit fix.
Materializing where the free re-borrow already suffices —
or copying data that never escapes and whose source is
never mutated — draws warning W0003
(RedundantMaterialize).
fn main():
s: str = "hello world"
print(s[0:5]) # hello
print(s[6:]) # world
print(s[:5]) # hello
word = s[0:5] # word: strview — a view, no copy
print(word)
Bounds are byte offsets, not character indices — slicing inside a multi-byte character panics at creation:
fn main():
s: str = "héllo"
print(s[0:1]) # h — byte 0 is a boundary
print(s[1:2]) # panics: byte 2 falls inside 'é' (bytes 1–2)
Scanning for ASCII delimiters (spaces, commas, newlines)
byte-wise is always safe — UTF-8 never reuses ASCII bytes
inside a multi-byte character. Character-level access comes
from explicit decoding APIs (.chars(), planned),
never from indexing.
f-strings are planned
Interpolated strings like f"{name}" are not yet
implemented. Build strings today with + and
int_to_str().
Bytes
bytes is the binary sibling of
str: an owned, heap-allocated, contiguous byte
buffer. Literals use the b"..." prefix and accept
the full \xNN escape range. Slicing yields a
bytesview — the same zero-copy, non-escaping
projection as strview, but with no UTF-8 boundary
rule, so scalar indexing (b[i]) is allowed.
Bridging to text is explicit: raw.to_str()
validates UTF-8; text.to_bytes() makes an owned
copy; text.as_bytes() is the zero-copy mirror —
a bytesview over the string's UTF-8 bytes, with
the usual view rules (non-escaping, freezes the owner while
live). Choosing between them: reach for
as_bytes() when you only read the bytes within
the current scope (scanning, parsing, hashing); reach for
to_bytes() when the bytes must outlive the call
or become an owned, mutable buffer that diverges from the
string. A to_bytes() copy that is never
mutated and never escapes draws warning
W0004 (RedundantToBytes) — the
compiler's nudge toward as_bytes().
Concatenate with +, append a byte with
bytes_push(&b, x); printing renders the escaped
b"..." form.
fn main():
raw = b"\x01\x02\x03"
header = raw[0:2] # bytesview — a view, no copy
print(int_to_str(header[0])) # 1
mut buf = b"\x00"
bytes_push(&buf, 255)
buf = buf + raw
print(buf) # b"\0\xff\x01\x02\x03"
Two interim simplifications
b[i] currently yields int
(0–255) — it becomes u8 when explicit
sizes land — and to_str() panics on invalid
UTF-8 until error unions land (final signature:
Utf8Error!str).
Structs #
Structs are user-defined composite types with named fields.
Declare one with struct and an indented field
list; construct values with brace syntax —
Point{x=1.0, y=2.0} — naming every field (no
defaults). Field access is by name: p.x. By
convention, struct names use PascalCase.
struct Point:
x: float
y: float
struct Rectangle:
width: float
height: float
fn area(rect: Rectangle) -> float:
return rect.width * rect.height
fn main():
p = Point{x=1.0, y=2.0}
print(float_to_str(p.x)) # 1.0
r = Rectangle{width=10.0, height=5.0}
print(float_to_str(area(r))) # 50.0
mut q = Point{x=0.0, y=0.0}
q.x = 7.0 # field assignment needs `mut`
print(float_to_str(q.x)) # 7.0
Field Access & Mutation
| Syntax | Meaning |
|---|---|
p.x |
Read field x (nests: l.end.x) |
p.x = v |
Assign a field — the binding must be
mut; compound forms like
p.x += 1.0 work too
|
Point{x=1.0, y=2.0} |
Brace construction — every field, by name, any order |
f(&p.x) |
A field is a valid inout borrow target —
the callee mutates it in place
|
Copy vs. Move
A struct whose fields are all Copy types (numbers, booleans)
is itself Copy — assignment duplicates it, and both sides
stay usable. A struct that owns a non-Copy field (like
str) moves on assignment: the
old binding is dead afterward, and destruction recurses into
the fields, freeing each owned buffer exactly once. Partial
moves are rejected — you can't move a single field out of a
struct.
struct Person:
name: str
age: int
fn main():
p = Person{name="alice", age=30}
q = p # moves — p is no longer usable
print(q.name) # alice
print(int_to_str(q.age)) # 30
Printing & Equality
Every struct has an always-on Debug
representation mirroring the literal syntax:
print(p) writes Point{x=1.0, y=2.0},
nested structs render recursively, and primitives print bare
(print(42), print(3.14),
print(true)). Printing borrows the value — it
stays fully usable afterwards.
Memberwise == / != is opt-in via
#[derive(Eq)] on the struct definition; every
field must be Eq-capable, and float fields use IEEE
== (a struct holding NaN never equals itself).
#[repr(C)] pins field layout to declaration
order with standard C padding — the hook for future FFI.
#[derive(Eq)]
#[repr(C)]
struct Point:
x: float
y: float
fn main():
p = Point{x=1.0, y=2.0}
print(p) # Point{x=1.0, y=2.0}
print(p == Point{x=1.0, y=2.0}) # true
print(p != Point{x=3.0, y=4.0}) # true
No methods yet
Structs are plain data today — methods land with
Milestone 17. Behavior lives in free functions like
area(rect) above.
Enums #
Enums — sum types, tagged unions — are user-defined types
that hold exactly one of several named
variants, each of which may carry data.
Declare one with enum and an indented variant
list. A variant takes one of three shapes: unit
(Red), tuple (Circle(float)), or
named fields
(Rectangle(width: float, height: float)).
Why "algebraic"?
An algebraic data type is composed from other types two
ways. A sum is one of several
alternatives — the variants. A product
is this and that — fields held together, which
is all a struct is. An enum is a sum whose variants
carry products, so Shape reads
Circle(float) | Rectangle(float, float) | Dot.
Because the compiler knows every alternative up front, it
lays every value out as a tag plus payload — and can
require that a match handles every
alternative.
enum Color:
Red
Green
Blue
enum Shape:
Circle(float)
Rectangle(width: float, height: float)
Dot
fn main():
c = Color.Green
print(c) # Color.Green
print(Shape.Circle(5.0)) # Shape.Circle(5.0)
s = Shape.Rectangle{width=1.0, height=2.0}
print(s) # Shape.Rectangle{width=1.0, height=2.0}
print(Shape.Dot) # Shape.Dot
Forms
| Syntax | Meaning |
|---|---|
Red |
Unit variant — no payload |
Circle(float) |
Tuple variant — positional payload |
Rectangle(width: float, ...) |
Named-field variant — payload by name |
Color.Red |
Construct a unit variant |
Shape.Circle(5.0) |
Construct a tuple variant — parens, always
EnumName.Variant
|
Shape.Rectangle{width=1.0, height=2.0} |
Construct a named-field variant — braces name every field (the Brace Law: parens in declarations, braces in construction) |
Printing & Equality
Every enum has an always-on Debug
representation mirroring construction syntax:
print(c) writes Color.Green,
tuple payloads render in parens (Result.Success(5)),
named payloads in braces (Shape.Rectangle{width=1.0,
height=2.0}). Printing borrows the value.
Variant-aware == / != is opt-in
via #[derive(Eq)] on the enum definition
(attributes attach to the definition, not to individual
variants). Values compare equal when the variant matches
and every payload field is equal — str
payloads by content, floats by IEEE ==.
Without the derive, == is a compile-time
error.
An enum value owns the data in its active variant, so it
follows the struct ownership rules: an enum holding a
non-Copy payload (like str) moves
on assignment, and the payload is destroyed with the value.
#[derive(Eq)]
enum Result:
Success(int)
Error(message: str)
fn main():
r = Result.Success(5)
print(r == Result.Success(5)) # true
print(r != Result.Error{message="boom"}) # true
Reading a payload back
A variant's payload can be written at construction and
read back through equality and Debug — but the
idiomatic way to branch on a variant and unpack its
data is exhaustive match; see
Pattern Matching.
Anonymous Structs & Tuples #
Ryo has exactly one ad-hoc grouping type — the
anonymous struct. Tuples are positional
sugar over it: (17, "alice") is the same value
as {0=17, 1="alice"}, one structural type, not
two. Reach for an anonymous shape when data is local and
transient (multi-return, unpacking); once a shape crosses a
boundary or carries domain meaning, give it a name.
fn divmod(a: int, b: int) -> {q: int, r: int}:
return {q=a / b, r=a % b}
struct Point:
x: int
y: int
fn main():
# one shape crosses the boundary; two ways to take it apart
(q, r) = divmod(17, 5) # positional: binds in field order
print(q) # 3
{quot, _} = divmod(17, 5) # named: quot = q, r is dropped
print(quot) # 3
pair = (17, "alice") # sugar for {0=17, 1="alice"}
print(pair.0) # 17
print(pair) # (17, "alice")
point = Point{x=q, y=r} # graduation: explicit construction
assert((1, 2) == (1, 2), "structural equality, no opt-in")
print(divmod(17, 5)) # {q=3, r=2}
Forms
| Syntax | Meaning |
|---|---|
{x=1, y=2} |
Anonymous struct literal — fields are names |
(17, "alice"), (7,) |
Tuple sugar for fields named "0" /
"1"; a one-element tuple keeps the
trailing comma
|
{q: int, r: int},
(int, str)
|
Type literals — return types, parameters, annotations |
pair.0 |
Positional access; chained access takes parens —
(pair.0).1
|
pair.0 = 42 |
Assign a positional field — the binding must be
mut; compound forms like
pair.0 += 1 work too
|
f(&pair.0) |
A positional field is a valid inout borrow
target — the callee mutates it in place
|
(q, r) = expr |
Destructuring — paren-less
q, r = expr also works; every field
must be bound or _-ed
|
{quot, _}, {x = ex} |
Named patterns: bare names bind their own field (or
the next uncovered one); renames name the field; a
_ field is destroyed, not bound
|
{x, y} = point |
Named structs destructure by field name — positional patterns on them are rejected |
Printing & Equality
Debug rendering is always on and mirrors the literal
syntax: all-positional shapes print in paren sugar —
(3, 1), (3,) — while any named
field switches to braces — {x=1, y=2} — with
str fields quoted. == /
!= compare memberwise with no opt-in whenever
every field type is Eq-capable; float fields use IEEE
==, so a shape containing NaN never equals
itself.
Diagnostics
| Code | Meaning |
|---|---|
E0109 |
{} is reserved for the future empty map
literal — there is no empty anonymous struct
|
E0110 |
pair.0.1 reads as pair . 0.1
— chain positional access with parens:
(pair.0).1
|
E0111 |
() is not a value — void
is the unit type; write none or
(x,)
|
E0112 |
(a) = x needs the trailing comma —
(a,) — or write plain
a = x
|
E0113 |
Pattern/shape count mismatch — "expected 2 fields, found 3 bindings — no field left to bind 'c'"; the unbound side is named |
E0114 |
Pattern names a field the struct doesn't have, or
leaves one unbound — bind it or write
_
|
E0115 |
Named structs destructure by field name — use the
brace pattern ({x, y}), not
(x, y)
|
E0116 |
== on a shape with a non-Eq-capable
field — the error names the field
|
Pattern Matching #
match is an expression that tests a value (the
scrutinee) against a list of patterns and yields
the first arm whose pattern matches. Arms are tried in
written order, the first match wins, and the compiler
checks both coverage and arm types at compile time — an
unhandled case is an error, not a runtime surprise.
enum Shape:
Circle(radius: float)
Rectangle(width: float, height: float)
Dot
fn area(s: Shape) -> float:
return match s:
Shape.Circle(r): 3.14159 * r * r
Shape.Rectangle(w, h): w * h
Shape.Dot: 0.0
fn describe(n: int) -> str:
return match n:
0: "zero"
42: "the answer"
_: "just a number"
fn main():
print(area(Shape.Circle{radius=1.0})) # 3.14159
print(area(Shape.Rectangle{width=3.0, height=4.0})) # 12.0
print(describe(42)) # the answer
Patterns
| Syntax | Meaning |
|---|---|
Shape.Dot |
Unit-variant pattern — matches that variant, binds nothing |
Option.Some(v) |
Payload bind — the variant's fields bind positionally; a non-Copy payload moves out of the scrutinee |
Red, Circle(r) |
Unqualified variant pattern — the same arms with the enum name omitted; the variant resolves against the scrutinee's enum |
42, true, "hi" |
Literal patterns over int /
bool / str — exact match
|
{kind="circle", r} |
Anonymous-struct pattern — literal fields guard the
match, bare names bind their fields; a
_: arm is still required
|
_ |
Wildcard — matches anything; the catch-all |
Exhaustiveness
Every possible value of the scrutinee must be covered.
Enums need an arm per variant, bool needs both
literals, and int / str /
anonymous-struct scrutinees always need a
_: arm. Incomplete coverage is
E0129 — the error names the missing
variants. Because arms run in written order, an arm that
can never execute (anything after _:, or a
duplicate of an earlier literal) is
W0005, not a silent dead branch.
Ownership
A payload bind of a non-Copy field moves the payload out of
the scrutinee — on that arm's path the value is consumed
whole, shell included, and unbound fields that own memory
are dropped when the arm starts. Matching a borrowed
scrutinee (match &opt:) still allows Copy
binds, but a by-move bind of a non-Copy field is
E0133: match an owned value instead, or
bind _ and read the field another way.
Diagnostics
| Code | Meaning |
|---|---|
E0129 |
Non-exhaustive match — names the missing variants
(declaration order); add the missing arms or a
_: arm
|
W0005 |
Unreachable arm — anything after _:, or
a duplicate of an earlier literal/variant arm
|
E0131 |
Pattern kind doesn't match the scrutinee type — an
int literal against a
str, a variant pattern against a
non-enum, and so on
|
E0132 |
Variant pattern names a variant the enum doesn't declare — with a did-you-mean over the enum's variants |
E0133 |
By-move bind of a non-Copy field out of a borrowed scrutinee |
E0134 |
Arm value types disagree — every value arm must yield the same type |
E0135 |
A value-producing match has an arm whose statement
body falls through without a value — end the arm
with a value or return
|
E0136 |
Unmatchable scrutinee type — match works on enums,
int, bool,
str, and anonymous structs; floats,
named structs, views, bytes, and
void are rejected
|
E0137 |
View-typed match result — a match whose value arms all yield one view type is rejected until view-result merging lands |
E0138 |
Match value arm yields a value borrowed from the
caller (a borrowed parameter or a projection of
one) — the result cannot take ownership of
caller-owned storage; restructure with a
mut binding
|
Control Flow #
Ryo uses Python-style colons and indentation. There are
if/elif/else,
while, and for loops, plus
break and continue.
fn classify(n: int) -> str:
if n > 0:
return "positive"
elif n == 0:
return "zero"
else:
return "negative"
fn main():
mut i = 3
while i > 0:
print("countdown\n")
i -= 1
# range(start, end) is half-open: 0, 1, 2, 3, 4
for j in range(0, 5):
print(int_to_str(j))
The for loop iterates a half-open integer
range produced by range(start, end): it yields
start through end - 1.
Functions #
Functions are declared with fn. Parameters carry
explicit type annotations; the return type follows the parameter
list after ->. Omit the return type for a function
that returns nothing (void). By convention, functions
and variables use snake_case.
# Two int inputs, returns int.
fn add(x: int, y: int) -> int:
return x + y
# No return value — return type omitted.
fn greet(name: str):
print("Hi, " + name)
fn main():
result = add(3, 4)
greet("Ryo")
No void keyword in signatures
Don't write -> void. Just leave the
return-type slot empty — the function returns the unit value
implicitly.
Operators #
Operator precedence follows the usual mathematical rules. Parentheses group sub-expressions.
| Category | Operators |
|---|---|
| Arithmetic |
+ - * / %
|
| Compound assign |
+= -= *= /= %=
|
| Comparison |
< > <= >= == !=
|
| Logical | and or not |
| Grouping | ( ) |
| Ownership |
& (mutable borrow) · inout ·
move
|
# Multiplication and division bind tighter than + and -.
result = 1 + 7 * (3 - 4) / 2 # = 1 + (7 * -1 / 2) = 1 + (-3) = -2
Built-in Functions #
These are available in every file without an import — the Prelude.
| Signature | Description |
|---|---|
print(value) |
Write a value to standard output. No trailing newline.
Accepts str (strview
re-borrows), bytes (bytesview
re-borrows), int, float,
bool, and structs (their Debug
representation).
|
panic(msg: str) |
Abort the program with msg. Never returns.
|
assert(cond: bool, msg: str) |
Abort with msg if cond is
false. msg must be a string literal —
dynamic or view messages are rejected (the P6'
re-borrow does not apply here).
|
int_to_str(n: int) -> str |
Convert an integer to its decimal string form. |
float_to_str(x: float) -> str |
Convert a float to its shortest string form;
non-finite inputs produce "nan",
"inf", or "-inf".
|
bool_to_str(b: bool) -> str |
Convert a boolean to "true" or "false". |
bytes_push(inout b: bytes, x: int) |
Append byte x (0–255, panics out of
range) to b in place.
|
range(start: int, end: int) |
Half-open integer range for for loops. |
print() prints strings, bytes, and
Debug-capable values
int, float, and
bool render bare (42,
3.14, true); structs render
their Debug form (Point{x=1.0, y=2.0}).
Printing borrows the value.
Ownership Lite #
Ryo is memory-safe without a garbage collector and without lifetime annotations. Every binding owns its value; how a function receives an argument is declared once, on the parameter, and the compiler enforces the rules. The design is inspired by Rust's borrow checker and Mojo's eager destruction.
New to ownership?
Walk through a real ownership error and the four ways to fix it in the interactive Ownership Lite tutorial.
There are three parameter modes, chosen by a keyword between the colon and the type:
| Mode | Keyword | Callee access | Caller afterwards |
|---|---|---|---|
| Immutable borrow | (default) | read-only | unchanged, still usable |
| Mutable borrow | inout |
read-write | unchanged, still usable |
| Move (take ownership) | move |
read-write, then drops | dead — read is a compile error |
One rule ties them together: call sites are
sigil-free for every mode except the mutable
borrow, which takes a & at the call site to make
mutation explicit. The signature does the rest.
Borrow Modes in Practice #
Immutable borrow (default)
A plain parameter is borrowed read-only. The caller keeps the value and can use it again afterwards.
fn read_text(text: str):
# text is implicitly borrowed — read-only
print(text)
fn main():
mut message = "Hello"
read_text(message) # implicit borrow
print(message) # message still usable
Mutable borrow (inout + &)
An inout parameter may be mutated by the callee. The
caller passes it with an explicit & so every
mutation is visible at the call site.
fn mutate_text(inout text: str):
text = text + ", mutated"
fn main():
mut message = "Hello"
mutate_text(&message) # explicit & at the call site
print(message) # Hello, mutated
Move (move)
A move parameter takes ownership. After the call, the
caller's binding is dead — reading it is a compile error. Inside
the callee the value is dropped at its last use, not at return
(unless it moves it onward).
fn take_text(move text: str):
modified = text + ", owned"
# text's last use — it is dropped here, before the print
print(modified)
fn main():
message = "Hello"
take_text(message) # ownership moves into take_text
# print(message) # compile error: `message` was moved
Safety without lifetimes
No use-after-free, no double-free, and no leaked values — and you never annotate a lifetime. The compiler tracks liveness implicitly and inserts destruction at last use.
The Seven Rules #
Every ownership diagnostic is an instance of seven formal rules from the spec (§5.3):
| 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 | Per value: one inout or many reads, never both — E0032 |
Why No GC, Ever #
All of the ownership 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. And because borrows are scoped to a single call, most of the fights people have with Rust's borrow checker simply never happen.
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.)
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.
Enforced today
Free functions can't return views of owned values
(Rule 5), so the parser-combinator
shape —
fn parse(input: strview) -> (strview, strview) —
can't exist: parsing a huge file zero-copy works
inside one call, but the results can't come back to
you as borrows.
Enforced today A borrow can't be stored: an iterator/filter chain saved in a struct and consumed later holds a borrow across arbitrary time, and Ryo's views are tied to their owner's scope — pipelines are callback-shaped instead.
Deliberate non-goal
Mutable lending across tasks — rayon-style
par_iter writing into disjoint chunks — stays
out: proving disjointness across tasks is full borrow
checking.
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.
What's Next #
Ryo is pre-alpha. The features below are specified and on the roadmap but not yet implemented — they do not compile today. This list tracks the largest items.
| Feature | Status |
|---|---|
Errors as values (!T) &
catch
|
Milestone 13 |
Optionals (?T) & orelse
|
Milestone 16 |
| Methods on structs | Milestone 17 |
Collections (list, map) |
Milestone 22 |
| Concurrency (tasks & channels) | Planned |
For the full picture, see the Language Specification and the Implementation Roadmap.