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 (through Milestone 8.4). 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.
| 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 //.
Prefixed literals are planned
Underscore separators and the 0x /
0o / 0b prefixes are specified but
not yet implemented. Writing 1_000 or
0xFF today is a lex error — use plain decimal.
fn main():
print(int_to_str(7 / 2)) # 3 — integer division truncates
print(int_to_str(7 % 2)) # 1 — remainder
mut counter = 0
counter += 10 # compound assignment
counter -= 3
print(int_to_str(counter)) # 7
print() takes only str
To output a number, convert it first with
int_to_str(n). There is no implicit formatting
yet.
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. 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
No methods yet
Structs are plain data today — methods land with
Milestone 17. Behavior lives in free functions like
area(rect) above. Printing a struct
directly (print(p)) and == on
structs arrive with Milestone 9.1.
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 |
& (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(s: str) |
Write a string to standard output. No trailing newline. |
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. |
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 str and
bytes
Passing an int, float, or
bool is a compile error. Convert numbers with
int_to_str() first; a
float-to-string helper is not yet available.
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.
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. The
value is destroyed when the callee returns (unless it moves it
onward).
fn take_text(move text: str):
modified = text + ", owned"
print(modified)
# text is dropped when take_text returns
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.
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 |
|---|---|
| Methods on structs | Milestone 17 |
| Tuples | Milestone 10 |
| Enums (algebraic data types) | Milestone 11 |
Pattern matching (match) |
Milestone 12 |
Errors as values (!T) &
catch
|
Milestone 13 |
Optionals (?T) & orelse
|
Milestone 16 |
Collections (list, map) |
Milestone 22 |
| Concurrency (tasks & channels) | Planned |
For the full picture, see the Language Specification and the Implementation Roadmap.