Language Reference

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.

variables.ryo
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 (i8i64, u8u64, 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.

arithmetic.ryo
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.
logic.ryo
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 +.

strings.ryo
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).

slices.ryo
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:

utf8_boundary.ryo
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.

bytes.ryo
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.

structs.ryo
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_move.ryo
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.

control_flow.ryo
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.

functions.ryo
# 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
precedence.ryo
# 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.

borrow.ryo
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.

inout.ryo
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).

move.ryo
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.