S01E06 — 01-06-2026.
zo 0.4.0 — The Turfu is Coming.
A long month of work, rewarded by a good compiler. This milestone had no single goal. The aim was to stabilize the compiler’s architecture as far as possible.
Plenty of small, irritating cases were slowing down the writing of zo programs. We had to hunt them down and fix them, to reach the point where you can write programs close to a real developer’s needs. We haven’t fixed every case, but many now carry a dedicated test, to guard against regressions.
I’m aware I don’t yet have a rigorous method for covering every case. The right answer will come. For now, this is how I like to learn: write the programs, hit the walls, fix what breaks.
website
zo has a home: https://zo.compilords.house. All the documentation now lives there: the initiation, the spec, and these news posts. We also publish llms.txt and llms-full.txt, so the models people now ask about everything can answer about zo from a single, authoritative source.
providers
zo can talk to the outside world. A binding generator turns a C library into a zo provider, a typed surface you load and call. The feature is still experimental and will keep improving, but it already sits near the core of what zo can reach.
json
Parse, read, and build JSON. The Json type carries the whole surface: parse, kind, get, get_at, as_bool, as_int, as_f64, as_str, len, keys_len, key_at, to_str, array, object, push, set, write, and free.
load core::json::*;
fun main() {
imu data: Json = Json::parse("[10, 20, 30]");
imu first: int = data.get_at(0).as_int();
showln("first: {first}"); -- first: 10
data.free();
}
raylib
Games already run. Under zo-tests you’ll find Conway’s Game of Life, asteroids, arkanoid, and smaller examples. Opening a window is this short:
load core::c::*;
load provider::raylib;
fun main() {
raylib::init_window(800, 600, CStr::new("zo + raylib"));
raylib::set_target_fps(60);
loop {
if raylib::window_should_close() { break }
raylib::begin_drawing();
raylib::clear_background(0xFF000000);
raylib::draw_text(CStr::new("hello, zo!"), 12, 12, 32, 0xFFFFFFFF);
raylib::end_drawing();
}
raylib::close_window();
}
sqlite
A start, not yet complete. We plan full support for the milestone that begins now.
core & preload
std is gone, renamed core. We refined the preload, the set of packages zo makes available without an explicit load, and core grew a run of new packages.
random — a small, seedable generator.
load core::random::*;
fun main() {
mut rng: Rng = Rng::new(42);
imu roll: int = rng.range(1, 100);
showln("{roll}");
}
regex — compile a pattern, then match, find, replace, or split.
load core::regex::Regex;
fun main() {
imu vowels: Regex = Regex::new("[aeiou]", "");
showln(vowels.replace_all("hello", "*")); -- h*ll*
vowels.free();
}
time — a monotonic clock, durations, and sleep.
load core::time::(Instant, Duration);
fun main() {
imu start: Instant = Instant::now();
sleep(Duration::from_millis(10));
imu ms: int = start.elapsed().as_millis();
showln("{ms}"); -- >= 10
}
hash — SHA-1 and SHA-256 digests.
load core::hash;
fun main() {
showln(hash::sha256("abc"));
-- ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
}
encoding — base64, encode and decode.
load core::encoding::base64;
fun main() {
showln(base64::encode("zo")); -- em8=
showln(base64::decode("em8=")); -- zo
}
net — TCP sockets and an HTTP client.
load core::http;
fun main() {
match http::get("http://127.0.0.1:8080/") {
Result::Pass(response) => showln(response.body),
Result::Fail(_) => showln("request failed"),
}
}
And more, each a small package in core: c (CStr and CBytes for the FFI boundary), env (read and set environment variables), log (debug/info/warn/error), os, path, pool, info, and misato for 3D.
concurrency
zo runs concurrent work under a nursery, a scope that owns the tasks you spawn and does not return until they all finish. No leaked threads, no orphaned work.
nursery {
spawn server(fd);
spawn client(port);
}
supervise scopes children the same way, with one difference: a child panic cascades up instead of stopping at the scope. Reach for it when one failure should bring the whole subtree down.
A task is green by default. Thousands ride one OS thread through a cooperative scheduler. spawn thread gives a task its own OS thread, for CPU-bound work or a blocking call that would otherwise stall the others.
spawn worker(arg); -- green task
spawn thread os_worker(); -- dedicated OS thread
Tasks talk over typed channels, a (Tx<T>, Rx<T>) pair from channel(n). select waits on several at once, taking the first that fires.
nursery {
imu (tx1, rx1) := channel(1);
imu (tx2, rx2) := channel(1);
spawn producer_a(tx1);
spawn producer_b(tx2);
select {
rx1 => fn(value: int) => showln("chan1: {value}"),
rx2 => fn(value: int) => showln("chan2: {value}"),
}
}
module system
Modules nest now, and visibility is real. A pack can hold another pack, and a name stays private unless you mark it pub.
pack inner {
pack inner2 {
pub fun hello() {
showln("hello, modular world");
}
}
}
fun main() {
inner::inner2::hello();
}
testing
zo carries a testing framework. It isn’t finished, but the foundation is in place through the test modifier. A test fun runs under zo test and asserts with check.
fun add(left: int, right: int) -> int {
left + right
}
test fun adds_small_numbers() {
check@eq(add(2, 3), 5);
}
A
test funruns only underzo test. A normal build strips it, so tests never ship in your executable.
strings and interpolation
Indispensable, and now here. You can build and manipulate strings, and interpolate a value straight into a literal with {...}.
imu total: int = 42;
showln("total: {total}"); -- total: 42
For now, only a name goes inside the braces:
{total}, not{total + 1}. Interpolating an expression is planned for the next milestone.
error messages
Still a work in progress, but the hierarchy of our diagnostics is sharper, built to be useful while you debug. Diagnostics come in two modes: human and AI. Human is the default. Pass --format json to your zo command for the machine-readable form.
A zo error reads as an argument, not a bare complaint: a claim (the rule you broke), grounds (a caret on the offending source), and a resolution (the fix).
[E0309] Error • Cannot mutate immutable variable
╭─[ immutable.zo:3:3 ]
│
3 │ count = 1;
│ ──┬──
│ ╰──── cannot assign to immutable variable
│
│ Help • Use 'mut' to declare a mutable variable
───╯
--format json emits the same structure machine-readably, so a tool or an agent applies the fix without parsing prose. code and id name the error, severity ranks it, message is the claim, and fixes carries each edit as a kind / text / description triple.
{ "id": "immutable-variable", "code": "E0309", "severity": "error",
"message": "Cannot mutate immutable variable",
"fixes": [ { "kind": "insert", "text": "mut ",
"description": "Declare the variable as mutable with `mut`" } ] }
agents
We added an AGENTS.md and pointed CLAUDE.md at it, one source of truth that any agent model can rely on. It holds the project’s rules, principles, and code style. It isn’t only for the machines, either: contributors and volunteers read the same rules, so the whole team stays aligned. It still needs work. Most of the time the models lean on it less than they should. Taming that is its own milestone, and we’ll plan it soon.