Initiation
An initiation to zo.
For Andrea Le Saint And those who refuse the uniformity of software.
« Why accept slow compilers? Just make them faster. » — Jonathan Blow
COMPiLORDS©
install
A short setup before we open the first page. Two commands, two minutes.
« Simplicity is a prerequisite for reliability. » — Edsger W. Dijkstra
get the binary
curl --proto '=https' --tlsv1.2 -sSf https://zo.compilords.house/install.sh | sh
The script downloads and extracts the zo compiler into bin/zo and adds it to your PATH so zo is reachable from any shell.
verify
Confirm zo is reachable from your shell.
zo --version
Succesfully it will display zo x.x.x. The number depends on the latest release.
trouble?
Drop into the discord or open a GitHub issue — fastest path to a fix.
You’re ready. Turn the page.
prologue
[tsh-tsh]
This initiation is for the zo programming language.
What are we waiting for to improve our developer experience? Why does having to wait several seconds, or even minutes, to get feedback on the correctness of our program bother absolutely no one?
The quality of our current tools and infrastructure borders on mediocrity. Fortunately, some developers, resistant to this pervasive nonsense, perpetually continue to refine our ecosystem. To accept this mediocrity is to submit to the dictates of executive committees who force their products on us through waves of intensive marketing.
zo is not a revolutionary programming language: its concepts come from languages that have proven themselves. zo does not solve any new problem that another language hasn’t already solved. It is a different language, but one that aims to be familiar, so that people from diverse backgrounds can explore low-level programming through a high-level language.
Whether you are back-end, front-end, a hacker, curious, a nerd, a scientist, a Ph.D., or a creative, zo is just another field of possibilities among many. It’s up to you to see if you want to discover more.
Our convictions rest on simplicity, software harmony, and the developer experience. For this, with the compilords, we focused on the minuscule details that matter, but that everyone pretends not to see. The main idea remains to open new dimensions in which our thoughts transform into a series of zeros and ones, without ever sacrificing the joy.
JOiN THE DEVOLUTiON.
language design
keywords
zo has 53 keywords in total:
- Namespacing
- 4 letters
- pack
- declares the current package
- load
- imports items into scope
- Type Definitions
- misc letters
- abstract
- declares a behavior contract
- struct
- declares a record with named fields
- apply
- attaches behavior to a type
- enum
- declares a tagged union
- type
- declares a type alias
- Member Definitions
- misc letters
- fun
- declares a function
- ffi
- declares a foreign function binding
- val
- declares a compile-time constant
- imu
- declares an immutable binding
- mut
- declares a mutable binding
- fn
- declares a closure
- Control Flow
- misc letters
- continue
- skips to the next iteration
- return
- returns a value from a function
- match
- pattern matches across arms
- while
- loops while a condition holds
- break
- exits the current loop
- else
- alternate branch of an if
- when
- ternary expression
- loop
- infinite loop
- for
- iterates over a range or collection
- if
- conditional branch
- Concurrency
- misc letters
- supervise
- declares a supervised task scope
- nursery
- declares a structured task scope
- select
- multiplexes channels or tasks
- thread
- marks an OS thread spawn (parser-synthetic)
- spawn
- launches a concurrent task
- await
- suspends until a task completes
- Infixes
- misc letters
- and
- ...
- as
- casts a value to a type
- is
- type test (reserved)
- Modifiers
- misc letters
- group
- ...
- wasm
- marks an item for WebAssembly
- pub
- marks an item as public
- Qualifiers
- 4 letters
- Self
- refers to the current type
- self
- refers to the current instance
- Values
- misc letters
- false
- boolean false literal
- true
- boolean true literal
- Integers
- misc letters
- uint
- the default unsigned integer (32-bit)
- int
- the default signed integer (32-bit)
- s16
- 16-bit signed integer
- s32
- 32-bit signed integer
- s64
- 64-bit signed integer
- u16
- 16-bit unsigned integer
- u32
- 32-bit unsigned integer
- u64
- 64-bit unsigned integer
- s8
- 8-bit signed integer
- u8
- 8-bit unsigned integer
- Floats
- misc letters
- float
- the default floating-point (64-bit)
- f32
- 32-bit floating-point
- f64
- 64-bit floating-point
- Primitives
- misc letters
- bytes
- byte buffer
- bool
- boolean type
- char
- Unicode character
- str
- UTF-8 string
- </>
- template fragment type
- Fn
- function type
operators
unary
| Precedence | Operator |
|---|---|
| 0 |
+
-
! |
binary
| Precedence | Operator |
|---|---|
| 1 |
||
|
| 2 |
&&
..
..=
|
| 3 |
==
!=
|
| 4 |
<
<=
>
>=
|
| 5 |
|
|
| 6 |
^
|
| 7 |
&
|
| 8 |
<<
>>
|
| 9 |
+
++
-
|
| 10 |
*
/
%
|
| 12 |
.
|
assignments
| Operator |
|---|
=
+=
-=
*=
/=
%=
&=
|=
^=
<<=
>>=
:=
::=
|
others
| Operator | Name |
|---|---|
->
|
arrow (return type) |
=>
|
fat arrow (match arm) |
=:>
|
template fat arrow (template-body closure) |
|>
|
pipe arrow (reserved) |
::
|
path separator |
?
|
question |
@
|
at |
delimiters
| Open | Close | Name |
|---|---|---|
(
|
)
|
parentheses |
{
|
}
|
braces |
[
|
]
|
brackets |
<>
|
</>
|
template fragment |
punctuations
| Symbol | Name |
|---|---|
,
|
comma (list separator) |
;
|
semicolon (statement terminator) |
:
|
colon (type ascription) |
_
|
underscore (wildcard pattern) |
#
|
hash (reserved) |
$
|
dollar (reserved) |
%%
|
attribute marker |
...
|
ellipsis (spread / variadic) |
introduction
This guide is your initiation to the zo programming language ecosystem. Read sequentially on your first pass, then skip around once you grasp the architectural patterns.
how to use this guide
Every lesson delivers a high-fidelity snapshot of a functional zo program. Pay close attention to the comments: documents comments (-!) leverage raw Markdown formatting to establish systemic guidelines, while line (--) and block (-* *-) markers isolate execution mechanics.
-! Sup? I'm a doc comment, I support markdown
-! format. What's good?
-- I'm a line comment, I don't care about markdown.
-*
From my side I'm a block comment, I'm happy to help
for details that matter.
*-
Every executable compilation unit inside zo must expose an explicit, non-colored entry block called main:
-- Wassup?! I'm `main`, a function.
-- Use me as a entry point with `fun` keyword.
fun main() {
-- This program does nothing... yet.
}
-! ## the capstone.
-!
-! - every programs must declare a `main` block
-! to serve as the runtime launchpad. hello
Let’s spin up your first interactive instance. We trigger text printing operations via an internal optimized wrapper.
-! Yo! Let's start with a simple program.
-! In this lesson, we learn how to print a message.
fun main() {
-- We call our buddy `showln`.
-- It tells the compiler to display the value
-- passed as argument with a newline at the end.
showln("hello, hacker");
}
-! ## the capstone.
-!
-! - `showln` is an internal compiler builtin. No
-! import or namespace matching required. literals
numbers
Programming boils down to memory allocation layout and data mutations. Data arrives in primary primitives called literals. You do not need to memorize these constraints instantly, but you must respect their sizes.
All contextual code snippets assume code is running inside an active
fun main()execution block.
integers
-! Let me introduce the gang members.
-!
-! ## the integer family.
-!
-! signed: s8, s16, s32 (int), s64
-! unsigned: u8, u16, u32 (uint), u64
-- Ya, I'm the `int` chief — a signed 32-bit integer
-- by default for any bare number you write.
-- I scale up to `s64` if you need more room.
42
-- I support large values natively — `600851475143`
-- allocates down without complex object types.
600851475143
floats
-! And that's the rival clan.
-!
-! ## the float family.
-!
-! f32: 32-bit
-! f64: 64-bit (float)
-- Heyo, I'm `float` — a 64-bit double. Just add a
-- `.` and you get me.
14.0
3.14159
-- I support scientific notation natively. No
-- overhead, just quick compilation values.
1.0e10
2.5e-3
bases
-! Mask-on integer prefixes change internal notation
-! views.
0b11110000 -- Binary notation evaluates to 240
0o77 -- Octal notation evaluates to 63
0xff -- Hexadecimal notation evaluates to 255
parse modifiers
-! A `#` prefix sets the display base. The digits stay
-! decimal; only how the value prints changes.
b#30 -- value 30, shown in binary
o#75 -- value 75, shown in octal
x#76 -- value 76, shown in hexadecimal
booleans
-- Wordup, we're `bool` — only `true` and `false`.
-- No "truthy" or "falsy" mind games here."
true
false
strings
-- Look out! I'm `str` — a string literal. I live in
-- the binary's read-only data section hood, so I
-- cost nothing at runtime. Skuuuuuu!"
"JOiN THE DEVOLUTiON."
chars
-- Call me `char` — a single Unicode scalar wrapped
-- in single quotes.
'z'
bytes
-- Call me `bytes` — a multi-byte buffer wrapped in
-- backticks. Same layout as `str`, but without the
-- UTF-8 safety validation promise. Every raw byte
-- is preserved.
`hello`
`¥orld` variables
Data bindings demand structured tracking. zo enforces variable clarity using three dedicated allocation keywords: val for global or local compile-time constants, imu for unalterable execution parameters, and mut for active stack transformations.
constants
-- Hi, I'm `val` — your compile-time constant.
val VERSION: str = "1.0.0";
val MAX_HEALTH: int = 100;
locals
-- Hey, I'm `imu` — your immutable local. Set me
-- once, read me many.
imu name: str = "johndoe";
-- And me, I'm `mut` — your mutable local. I can be
-- reassigned.
mut health: int = 22;
health = 50; -- Legal modification mutation.
shadowing
-- Variable shadowing isolates structural scopes.
-- Each statement allocates a fresh slot, shadowing
-- the predecessor safely.
imu x: int = 40;
imu x: int = x + 2; -- x now safely evaluates to 42. interpolation
Drop variables into any string with {variable_name}. The compiler resolves each hole at compile time — no runtime parsing, no format functions.
imu name: str = "johndoe";
imu hp: int = 100;
imu attack: int = 15;
showln("hero: {name}, hp: {hp}");
Interpolation works in every string context — assignments, arguments, return values.
imu power: int = hp + attack;
imu status: str = "power level: {power}";
showln(status);
All scalar types resolve automatically: str, int, float, bool, char.
imu pi: float = 3.14;
imu active: bool = true;
imu label: str = "pi={pi}, ok={active}";
showln(label);
how it works
Interpolated strings compile into a single allocation that concatenates all segments. Each non-str variable converts to its string representation first, then everything merges in one pass.
showln("hp: {hp}");
-- Desugars to:
-- show("hp: ");
-- showln(hp);
imu msg: str = "hp: {hp}";
-- Compiles to:
-- to_str(hp) → "100"
-- multi_concat("hp: ", "100") → "hp: 100"
Direct output through showln skips the heap entirely — each segment writes straight to the file descriptor Assigned strings allocate once regardless of how many {} holes they contain.
-! ## the capstone.
-!
-! - `{variable}` inside any `"string"` resolves the variable.
-! - `\{` produces a literal brace, not interpolation.
-! - direct output (`showln`) allocates nothing.
-! - assigned strings allocate once, not per segment.
-! - all scalar types (`str`, `int`, `float`, `bool`, `char`) supported. operators
Operators are how you transform values. zo keeps them small and predictable — the same five arithmetic operators you’ve used everywhere, plus reassignment and a handful of shorthands.
arithmetic
imu power: int = hp + attack; -- + - * / %
mut current_hp: int = 100;
current_hp -= 25; -- += -= *= /= strings
All strings adhere to valid UTF-8 formats, supporting raw character arrays, unicode hex variants, layout escapes, and structural symbols seamlessly.
-- Custom strings are completely immutable. The
-- internal data bytes never change.
"\e[32mHello!\v\e[34mWorld\e[0m\n"
"\x48\x65\x6c\x6c\x6f" -- hex char
"\u{e9}\u{e8}\u{ea}" -- latin char
"\u{1F600} \u{1F680} \u{1F4A9}" -- decoded emoji
"🙈🙉🙊" -- raw unicode literals
concatenation
The ++ operator acts as your layout welding torch. Merging literals fuses values immediately inside the binary compilation phase, yielding zero performance penalties at execution time.
imu greeting: str = "hello";
imu name: str = "johndoe";
imu full: str = greeting ++ ", " ++ name ++ "!";
-- Direct character index extraction evaluates
-- efficiently.
showln(greeting[0]); -- Evaluates to 'h'
-! ## the capstone.
-!
-! - `str` is immutable.
-! - `++` concatenates.
-! - `s[i]` for string indexing. tuples
A tuple organizes items into a fixed-length, ordered sequence. Unlike arrays, a single tuple can house distinct data types within the same memory footprint.
imu point: (int, str, int) = (100, "john", 3);
-- Extract parameters via positional indices.
showln(point.0);
-- Deconstruct the memory layer instantly via
-- structured binding patterns.
imu (x, y, z): (int, int, int) = point; -- Structured destructuring binding
-- Declare structural type aliases for fast shape
-- replication.
type Point = (int, int); -- Type shaping alias arrays
Arrays host homogeneous components where every single block matches a identical data type. They come in static and dynamic variants.
static array
The notation format [N]T fuses the length constraint straight into the data classification layer. The memory block is determined at compile-time and guarantees safe bounds-checking verification parameters during constant array access tasks.
imu nums: [3]int = [10, 20, 30];
imu zeros: [5]int = [0...]; -- [0, 0, 0, 0, 0]
imu grid: [2][3]int = [[1, 2, 3], [4, 5, 6]];
-- Unpack items instantly using assignment sequences.
imu [a, b, c]: [int, int, int] = nums;
dynamic array
The designation []T offloads length calculations to execution headers rather than structural types. Coupling
arrays with a mut binding permits array extensions.
mut arr: []int = [];
arr.push(10);
imu last: int = arr.pop(); -- Safely extracts 10
-- The `[value...count]` expression triggers explicit
-- array expansion routines.
imu sevens: []int = [7...4]; -- [7, 7, 7, 7] blocks
…
functions
-- I'm a function accepting two arguments of type
-- int and returning a value of type int.
fun sum(a: int, b: int) -> int {
a + b -- implicit return
}
-- Then you can call me, like this:
sum(39, 3); closures
-- closure:block.
imu square: Fn(int) -> int = fn(x: int) -> int {
return x * x;
};
-- closure:line.
imu square: Fn(int) -> int = fn(x: int) -> int => x * x;
-- Then you can call me, like this:
square(7); structures
Custom structures package operations into meaningful domain boundaries. The struct keyword models complex custom fields, while the apply keyword assigns functional behavior logic to those types.
struct
A field can declare a default value with =. Fields without one are set when you construct the value.
struct Point {
x: int,
y: int,
}
struct Rect {
x: int = 10,
y: int = 20,
w: int = 100,
h: int = 200,
}
struct Counter {
x: int = 0,
}
methods
Use the apply statement block to bind custom functions to a target structure definition.
apply Counter {
-- Static instantiation function block.
fun new() -> Self {
Self { x = 0 }
}
-- Mutable state tracking modifier function.
fun incr(mut self) {
self.x += 1;
}
} enums
Enums manage disjoint data states safely, supporting direct discriminants along with tuple-wrapped inline data tracking blocks.
-- ...
enum Foo {
Bar,
Oof(str), -- Structural data tuple binding
Rab = 42, -- Assigned explicit discriminant value
}
-- Instance consumption example.
imu foo: Foo = Foo::Bar;
imu foo: Foo = Foo::Oof("What's crackin'?"); abstracts
Abstracts establish zo’s architecture for ad-hoc polymorphism. You define a structural contract once, then
declare custom implementation tracks across varying types via explicit implementation mappings: apply Abstract for TargetType.
abstract Display {
fun display(self) -> str;
}
struct Point {
x: int,
y: int,
}
apply Display for Point {
fun display(self) -> str {
return self.x.to_str() ++ ", " ++ self.y.to_str();
}
}
using abstracts as parameters.
Functions that accept elements under abstract contract bounds choose between three compilation techniques balancing raw execution speed against binary sizing limits.
form 1 — implicit-mono.
fun render(item: Display) -> str {
item.display()
}
The compiler manages the heavy lifting under the hood: it automatically generates a hidden type variable, extracts parameters straight from the call site, and generates a dedicated, static monomorphized copy of the function block per unique type. Zero runtime cost, zero vtable tracking lookups. Violating limits triggers error diagnostics immediately.
form 2 — explicit-mono.
fun render<$T: Display>(item: $T) -> str {
item.display()
}
This follows the exact same performance-optimal static compilation track as Form 1. Use this explicit syntax strategy whenever you must reuse the type constraint identifier across signature bounds—such as enforcing matched input types or coordinating return paths.
form 3 — dynamic dispatch.
fun render(item: any Display) -> str {
item.display()
}
Prepend the type parameter with any to box the instances safely behind a uniform vtable layout pointer. The compiler generates exactly one execution block in the final binary, executing code paths via runtime lookup addresses. This trade-off incurs slight call overhead but permits heterogeneous data grouping inside shared array vectors.
mut widgets: []any Drawable = [];
widgets.push(Button { label = "ok" });
widgets.push(Slider { value = 42 });
for w := widgets {
showln(w.draw());
}
| Engineering Requirement | Optimal Architectural Choice |
|---|---|
| Maximum dispatch velocity; isolated single types per call site. | item: Abstract (Form 1) |
| Shared type bounds enforcement across arguments or return paths. | <$T: Abstract>(item: $T) (Form 2) |
| Mixed collection handling; minimal compiled binary space footprint. | item: any Abstract (Form 3) |
type alias
-- ...
type Bar = int;
-- ...
imu z: Bar = 42;
group type Idx = int
and Pair = (int, int)
; generics
Generics are zo’s form of parametric polymorphism — one body of code that works across many types via type parameters
(<$T>). The other form, ad-hoc polymorphism, is covered by abstracts.
type inference
- hindley milner.
control flow
if else
-- same type inside
if 1 == 2 {
false
} else if 2 == 3 {
false;
} else if 3 == 4 {
false
} else {
true
}
ternary
-- only as expression
when true ? 1 : 2;
pattern matching
-- ...
match 5 {
10 => check(false),
_ => check(true), -- wildcard
}
-- ...
match "z" ++ "o" {
"ivs" => showln(false),
"zo" => showln(true),
_ => showln("default"),
}
jumps (terminators)
for i := 1..10 {
if i == 3 { continue; }
if i == 7 { break; }
show(i);
}
-- 12456 loops
while loop
-- while:block
mut z: int = 0;
while z < 1_000_000_000 {
z += 1;
}
-- while:line
mut z: int = 0;
while z < 1_000_000_000 => z += 1;
for loop
-- for:block.
for x := 0..3 {
showln("{x}");
}
-- for:block:mut.
for mut x := 0..3 {
x += 1
}
-- for:line.
for x := 0..3 => showln("{x}");
-- for:line:mut
-- mutable iterator, body reassigns it.
for mut n := 0..3 => n += 1;
infinite loop
mut x: int = 0;
loop {
if x == 1_000_000 {
showln(x);
break;
}
x += 1;
} concurrency
The zo runtime ignores standard state-machine async/await transforms entirely, eliminating function coloring bugs across your system. Execution runs inside native runtime-managed green threads tracking execution scope blocks called nurseries. Blocking a task triggers immediate context frame swaps inside the scheduler.
nursery
A nursery container sets strict lexical boundaries for concurrent task Lifecycles. The execution block cannot exit until every spawned green thread unwinds completely.
fun worker(id: int, tx: Tx<int>) {
showln("worker: {id}");
tx.send(id * 10);
}
fun main() {
imu (tx, rx) := channel();
-- The nursery handles concurrent task tracking
-- smechanics cleanly.
nursery {
spawn worker(1, tx);
spawn worker(2, tx);
} -- exical boundary block: execution holds here
-- until both tasks complete.
imu res1 := rx.recv();
imu res2 := rx.recv();
showln("collected results: {res1}, {res2}");
}
- True Stackful Green Threads: Every concurrent execution task allocates a lightweight runtime execution stack. Deep nested calls compile natively without restructuring code into complex async state loops.
- Unified Runtime Scheduler: The internal task coordinator monitors state mutations directly. Invoking
rx.recv()on an empty channel yields execution, swapping out active thread contexts immediately. - Structural Cancellation: Nurseries form distinct isolation islands. Triggering task cancellations propagates downstream through children stacks because the underlying runtime owns the stack handles.
supervise
select
Coordinate communication states across multiple channel references using the non-blocking select block format:
select {
rx1 => fn(value: int) => showln("chan1: {value}"),
rx2 => fn(value: int) => showln("chan2: {value}"),
}
thread
await
tests
zsx
The compiler builds an inline UI interface engine called zsx (zo Syntax Extension). It compiles interface markup nodes into static target rendering definitions at build-time, completely freeing the stack from heavy application bundles or runtime framework assets.
fun user_account(username: str, score: int) {
imu view: </> ::= <div class="card">
<h2>User: {username}</h2>
<p>Current Score: {score}</p>
</div>;
}
@events
@click, @input, event handlers.
components
…
style
Target style behaviors directly inside your source file using scoped lexical style assignment scopes ($: {}). The declarations remain completely isolated to the compilation package module boundary, guaranteeing style isolation.
-- Isolated module styles stay bound to local markup
-- components.
$: {
card {
background: #161b22;
border-radius: 4px;
padding: 16px;
}
button {
background-color: #ff79c6;
color: #0d1117;
font-weight: bold;
}
}
-- Use the public prefix modifier to pass declaration
-- metrics globally.
pub $: {} directives
#render — …
#html — …
foreign function interfaces
zo calls C-ABI libraries directly. You declare the foreign function once, tell the linker where its symbol lives, and call it like any zo function — no wrapper layer, no runtime cost.
declaring a foreign function
A pub ffi declaration names a function that lives in a C library. It has no body; the symbol resolves at link time.
-- A declaration ends in `;` — no body.
pub ffi sqrt(x: f64) -> f64;
-- A void function omits the return.
pub ffi close_window();
zo drives the call straight from this signature: it places arguments in registers per the platform ABI, narrows or widens scalars, and passes structs by value. Adding a function costs one line.
linking a library
A #link block tells the linker which dylib owns the symbols in the file. One block covers every pub ffi in the same pack.
#link {
macos: "@executable_path/libzo_provider_sqlite.dylib",
linux: "@executable_path/libzo_provider_sqlite.so",
}
pub ffi zo_sqlite_open(path: CStr) -> int;
pub ffi zo_sqlite_close(handle: int);
C strings cross the boundary as CStr (from core::c), never str — a zo str carries a length header that a C function would misread.
calling a library
Foreign libraries ship as opt-in providers. Load one and call it:
load core::c::*;
load provider::sqlite::*;
fun main() {
imu db: int = zo_sqlite_open(CStr::new("scores.db"));
zo_sqlite_exec(db, CStr::new("CREATE TABLE s (score int)"));
zo_sqlite_close(db);
}
the binding generator
Hand-writing a pub ffi line per function — and keeping each one in sync with the library — is the tedious part. zo-binder writes those declarations for you, from one of two sources.
from a rust library
Wrap any Rust crate in a small shim that exports plain C functions:
#[unsafe(no_mangle)]
pub extern "C" fn undo_stack_new() -> i64 { /* ... */ }
Then generate the bindings:
just bind undoredo
zo-binder reads the shim’s signatures and writes provider/undoredo/undoredo.zo — the #link block plus one pub ffi per function, ready to load.
from a c header
For a C library, feed zo-binder the header’s machine-readable API. raylib emits one through its rlparser tool:
zo-binder --json raylib_api.json --lib raylib \
--macos /opt/homebrew/lib/libraylib.dylib \
--linux /usr/lib/x86_64-linux-gnu/libraylib.so
zo-binder maps each C type to its zo equivalent, generates the structs, renames InitWindow to init_window, and skips what it cannot map — callbacks, variadics — reporting each one.
The generated file is committed and reviewed like any other source. Nothing runs during zo run: you regenerate bindings deliberately, the way you would run a formatter.
core
options
imu some: Option = Option::Some("...");
imu none: Option = Option::None;
results
imu pass: Result = Result::Pass("value");
imu fail: Result = Result::Fail("error");
errors
the ? operator
- short-circuit Result inside a Result-returning function
- desugaring:
expr?≡match expr { Pass(v) => v, Fail(e) => return Fail(e) }
error propagation
- chaining:
read_file(p)?.parse()?.validate()? - composing helpers that bubble up domain errors
errors vs panics
- Result for expected failure modes (file not found, parse error)
- panics for bugs (invariant broken, indexing past length)
- never use Result to signal logic errors; never panic on user input
ranges
for i := 0..5 { -- iteration
showln(i); -- 0 1 2 3 4
}
imu slice: []int = xs[2..5]; -- slicing
collection types
arrays
imu scores: []int = [1, 2, 3, 4, 5];
imu empty: []int = [];
scores.sum(); -- 15
empty.sum(); -- 0
scores.contains(3); -- true
empty.contains(5); -- false
scores.find(3); -- 2
empty.find(99); -- -1
scores.min_of() -- 1
scores.max_of() -- 5
empty.min_of(); -- 0
vectors
mut numbers: Vec<int> = Vec::new();
numbers.len(); -- 0
numbers.is_empty(); -- true
numbers.push(10);
numbers.push(20);
numbers.push(30);
numbers.get(0); -- Option::Some(10)
numbers.get(99); -- Option::None
numbers.set(1, 42); -- set in-bounds.
!numbers.set(7, 0); -- set out-of-bounds returns false.
numbers.pop(); -- Option::Some(30)
numbers.remove(1); -- Option::Some(42)
sets
mut ids: HashSet<int> = HashSet::new();
ids.is_empty(); -- true
ids.insert(10); -- true (new key)
ids.insert(20);
ids.insert(10); -- false (already present)
ids.contains(10); -- true
ids.contains(99); -- false
ids.remove(20); -- true
ids.len(); -- 1
maps
mut counts: HashMap<str, int> = HashMap::new();
counts.is_empty(); -- true
counts.insert("a", 1);
counts.insert("b", 2);
counts.get("a"); -- Option::Some(1)
counts.get("z"); -- Option::None
counts.contains_key("b"); -- true
counts.remove("a"); -- Option::Some(1)
counts.len(); -- 1
file system
imu path: str = "/path/to/file";
match write_file(path, "hi") {
Result::Pass(_) => {},
Result::Fail(_) => showln("write-err"),
}
match read_file(path) {
Result::Pass(text) => showln(text),
Result::Fail(_) => showln("read-err"),
}
exists(path); -- true
remove_file(path); -- true
imu names: []str = read_dir("/some/dir");
directories
load core::io;
io::is_dir("/some/dir"); -- true
io::copy("a.txt", "b.txt"); -- Result::Pass(bytes copied)
io::remove_dir("/empty/dir"); -- Result::Pass(0)
io::remove_dir_all("/whole/tree"); -- recursive teardown
Each returns Result::Fail(errno) on failure.
terminal
load core::io;
-- fd 0 stdin, 1 stdout, 2 stderr.
when io::isatty(1) ? showln("interactive") : showln("piped");
environment
load core::env;
env::current_dir(); -- "/work/dir"
env::set_current_dir("/tmp"); -- true
env::temp_dir(); -- the OS temp directory
env::get("HOME"); -- "/Users/me" ("" on miss)
env::set("KEY", "value"); -- true
env::remove("KEY"); -- true
imu all: []str = env::vars(); -- ["KEY=VALUE", ...]
command-line
load core::cli;
load core::io;
imu app: Cli = Cli::new("greet", "say hello")
.flag("v", "verbose", "print more")
.option("n", "name", "who to greet");
imu parsed: Parsed = app.parse(io::args());
parsed.has("verbose"); -- true when -v / --verbose given
parsed.value("name"); -- Option::Some("zo")
parsed.positionals(); -- bare arguments, in order
--name=zo,--name zo, and-n zoall parse the same- unknown dashed tokens fall through to positionals
app.help()renders usage text from the registered specs
module system
pack
pack say {
fun hello() {
showln("hello, modular world");
}
}
load
load core::math::pow_i;
load core::math::(pow_i, abs); error messages
When a program does not compile, zo tells you what went wrong, where, and how to fix it. Pick the shape of that report with --format: a colored snippet for you, or structured data for a tool.
fun main() {
imu s: str = "hello" ++ 42;
}
By default the compiler renders a human snippet to stderr — the offending line, a caret under each span, and the conflicting types in color.
zo build greeting.zo
[E0304] Error • Type mismatch
╭─[ greeting.zo:2:25 ]
│
2 │ imu s: str = "hello" ++ 42;
│ ───┬─── ─┬
│ ╰─────────── conflicts with this type `str`
│ ╰── incompatible type `int` here
warnings
Not every diagnostic stops the build. Warnings point at code that compiles but breaks a convention — an unused variable, unreachable code, or a name that does not follow zo’s naming rules:
struct,enum,type, and generic names are PascalCase.valconstants are SCREAMING_SNAKE_CASE.- everything else —
imu/mutbindings,funnames and arguments, struct fields,abstractfunctions — is snake_case.
Each naming warning carries the convention-correct rename as its help, so the fix is always one copy-paste away:
[E0355] Warning • Name is not snake_case
╭─[ counter.zo:2:7 ]
│
2 │ imu MyCount := 1;
│ ───┬───
│ ╰───── expected a snake_case name
│
│ Help • rename it to `my_count`
A leading underscore opts a binding out (_unused), and digits never need a separator (r0, grid2, MAX2 are all fine). The program builds and runs regardless — warnings inform, errors stop.
machine formats
An agent reads text differently than you do — it never skims and it is never overwhelmed by length. So zo offers two machine formats that carry the full diagnostic, not a terse summary. Both stream to stdout, leaving stderr for you.
--format=json emits one JSON object per diagnostic, one per line (NDJSON), flushed as each error is found.
zo build greeting.zo --format=json
{"$schema":1,"id":"type-mismatch","code":"E0304","severity":"error","phase":"analyzer","message":"Type mismatch","fixes":[],"notes":["The types of both operands must be compatible"],"snippet":{"before":["fun main() {"],"lines":[" imu s: str = \"hello\" ++ 42;"],"after":["}"]},"span":{"file":"greeting.zo","byte_start":35,"byte_end":37,"line_start":2,"line_end":2,"col_start":25,"col_end":27},"secondary":{"file":"greeting.zo","byte_start":24,"byte_end":31,"line_start":2,"line_end":2,"col_start":14,"col_end":21},"primary_type":"int","secondary_type":"str"}
--format=xml emits one well-formed <diagnostics> document. The tag boundaries read as explicit structure — clean to drop straight into a prompt.
zo build greeting.zo --format=xml
<diagnostics schema="1">
<diagnostic id="type-mismatch" code="E0304" severity="error" phase="analyzer">
<message>Type mismatch</message>
<fixes/>
<notes>
<note>The types of both operands must be compatible</note>
</notes>
<snippet>
<before>
<line>fun main() {</line>
</before>
<lines>
<line> imu s: str = "hello" ++ 42;</line>
</lines>
<after>
<line>}</line>
</after>
</snippet>
<span file="greeting.zo" byte_start="35" byte_end="37" line_start="2" line_end="2" col_start="25" col_end="27"/>
<secondary file="greeting.zo" byte_start="24" byte_end="31" line_start="2" line_end="2" col_start="14" col_end="21"/>
<primary_type>int</primary_type>
<secondary_type>str</secondary_type>
</diagnostic>
</diagnostics>
The two machine formats are isomorphic: the same fields under the same names. A JSON key maps 1:1 onto the XML element or attribute of the same name, so a tool that reads one reads the other.
the schema
Every diagnostic carries a stable identity and the data needed to act on it without re-parsing your source.
id— a frozen, kebab-case name (type-mismatch). Match on this, not the prose.code— the display alias (E0304), derived fromid.severity—errororwarning.phase— where it surfaced:tokenizer,parser,analyzer,codegen,runtime.message— the one-line headline.span— the primary location:file, byte offsets, and 1-indexedline/col(columns count characters, soéadvances one).secondary— the conflicting location, when a diagnostic carries two spans.fixes— machine-applicable edits, always an array. Each fix names akind(insert/replace/delete), the replacementtext, adescription, and the exact span to edit. A tool auto-applying picks the first.notes— attached context, always an array.snippet— the source lines around the span (before/lines/after). Tune the radius with--snippet-context N;0turns it off.
fixes and notes are always present — empty rather than absent — so a consumer never needs a presence check. The same source over the same input renders byte-identical output, so a tool can diff two builds.
-! ## the capstone.
-!
-! - default `--format=human` paints a colored snippet to stderr.
-! - `--format=json` streams one NDJSON object per diagnostic to stdout.
-! - `--format=xml` emits one well-formed document to stdout.
-! - both machine formats share one frozen, isomorphic schema.
-! - match on the stable `id`, never on the prose `message`.
-! - `fixes` carry exact edits; `--snippet-context N` sets the source radius. epilogue
Your initiation is complete. Now that you have mastered the fundamentals of the zo language — it is up to you to get creative and show the world your talent. We have put together a collection of programs for you to browse, sample, replicate, and modify however you like.
Click the following link to access them: @how-to
TRiLU!
For humans: faq.
For Ai agents: llms.txt (curated index) and llms-full.txt (full docs).
Privacy: No cookies, no ads, no tracking. It's like you were never here.