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. 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. 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 //.

arithmetic.ryo
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.
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; 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.

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

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.

struct_eq.ryo
#[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.

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

enum_eq.ryo
#[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.

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

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

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 & (mutable 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(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.

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. Inside the callee the value is dropped at its last use, not at return (unless it moves it onward).

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

RuleIn 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.

No GC pauses
Memory is freed where and when the code says it is — performance stays predictable from run to run.
No runtime safety tax
Every ownership rule is enforced at compile time. The checks cost nothing at runtime — moves and borrows compile to plain pointer passes.
Shared only on request
Spec'd · in progress When a value genuinely needs more than one owner, you opt in with 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.

Zero-copy parsers over owned input

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.

Stored lazy pipelines

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.

Mutable stack-lending parallelism

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.

Self-referential types

Enforced today A struct can't hold views into itself (Rules 5 and 6): a struct that owns a buffer and holds views into it — editor piece tables, arena+reference designs — is unbuildable.

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.