2026-09-11, night. Written the evening the Roc bird first sat on its tree. Open-ended on purpose: this is what happened, what it seems to mean, and what I do not know yet.
At noon safari was a Codex program: fifty-four spec chapters, a browser harness, and a wasm module compiled by a zig transpiler. By nine in the evening the same screensaver was running in the same browser page from a wasm module compiled by Roc, from Roc modules emitted by our own compiler, with a purple origami bird on the fifth tree on the right of every segment that the Codex version does not have. The fifty-four specs pass under Roc, byte for byte against the verdicts the Codex interpreter froze, in twenty-seven seconds.
The order of the day, because the order was the method:
F64 because a
bare fraction is a Dec; main! takes args and answers a Try; the
Echo platform's echo! writes no newline; List.len is a U64 and
Codex's Integer is an I64, so the conversions live at three seams.F64
arithmetic, if, let, match over four sums and Maybe, list
literals, field access, fifteen builtins. Nothing else. That bounded the
emitter to an afternoon.Cat as its own void type; a chapter's text must
not depend on which spec is attached, so binder freshening and the
driver's inlining are scoped to the chapter; derived definitions belong
to the chapter that declared their type.provides named functions over a boxed model, a zig host that calls them
as C externs, roc build --target=wasm32. The host is a hundred lines
and the app is a hundred lines; the sixteen exports the page's
JavaScript binds are the same sixteen the zig shim had.x & f
rest, and in Roc each level of that copies everything below it; the
emitter now recognises the shape and writes an accumulator loop,
appending one element at a time, because appending by concatenation is
fifty times slower. The frame is 15 ms.This is the first Roc in safari that was written rather than emitted, and it is short because everything it needs was already there as a chapter: the frame that places a point on the road in the rider's frame, the camera that projects it, the still machinery that maps a polygon onto an anchor at a depth. The logo is roc-lang.org's SVG, six polygons, in the same unit frame the baked animals use.
RocBird :: [].{
# The logo as a still, beak toward +x. Colours are the site's logo-dark
# and logo-light.
polys : List(Stills.StillPoly)
polys = [
{ color: 6368222, grad: [], pts: [{ x: -0.0344, y: 0.5715 }, { x: -0.148, y: 0.0 }, { x: 0.0368, y: 0.1469 }, { x: 0.0183, y: 0.2577 }] },
{ color: 8542181, grad: [], pts: [{ x: 0.2216, y: 0.6413 }, { x: 0.3497, y: 0.5025 }, { x: 0.3679, y: 0.5849 }, { x: 0.3862, y: 0.6862 }] },
# ... four more, the whole logo is six polygons
]
# A bird stands this tall, in metres, on its treetop.
height : F64
height = 2.2
# The fifth tree on the right: trees are planted in pairs, left then right,
# and across > 0 is the right side.
fifth_right : List(Scenery.Tree), I64, I64 -> [Found(Scenery.Tree), NoTree]
fifth_right = |trees, i, seen| match List.get(trees, I64.to_u64_wrap(i)) {
Err(_) => NoTree
Ok(t) => if t.across > 0.0 {
if seen == 4 { Found(t) } else { fifth_right(trees, i + 1, seen + 1) }
} else {
fifth_right(trees, i + 1, seen)
}
}
# The bird for chain position d, or nothing when its tree is behind the
# near plane or too small to draw.
perch : List(World.Segment), List(I64), Frame.Pose, F64, F64, I64 -> List(Paint.DrawCmd)
perch = |segs, ch, pose, cf, view_w, d| {
seg_idx = List.get(ch, I64.to_u64_wrap(d)) ?? crash("chain index")
seg = List.get(segs, I64.to_u64_wrap(seg_idx)) ?? crash("segment")
match fifth_right(seg.trees, 0, 0) {
NoTree => []
Found(t) => {
rp = Frame.at(segs, ch, pose, d, t.along, t.across + seg.width / 2.0)
if rp.forward <= Geom.near {
[]
} else {
ht = (height / rp.forward) * cf
if ht < SceneLimits.min_scenery_px {
[]
} else {
top = Camera.project({ right: rp.right, forward: rp.forward, height: t.height }, cf, view_w)
# Facing the road, which is to a right-side tree's left.
Critter.critter_polys(top, Critter.facing(False), ht, polys, 0)
}
}
}
}
}
# Every bird in the chain, nearest segment first.
draw_all : List(World.Segment), List(I64), Frame.Pose, F64, F64 -> List(Paint.DrawCmd)
draw_all = |segs, ch, pose, cf, view_w| draw_from(segs, ch, pose, cf, view_w, 0, [])
draw_from : List(World.Segment), List(I64), Frame.Pose, F64, F64, I64, List(Paint.DrawCmd) -> List(Paint.DrawCmd)
draw_from = |segs, ch, pose, cf, view_w, d, acc|
if d >= U64.to_i64_wrap(List.len(ch)) { acc } else {
draw_from(segs, ch, pose, cf, view_w, d + 1, List.concat(acc, perch(segs, ch, pose, cf, view_w, d)))
}
}
The app appends RocBird.draw_all(...) to the frame before the expansion
that turns draw commands into what the canvas can paint. Twelve polygons
more in the first frame, two birds; two and a half milliseconds more per
frame.
The IR was the asset. The whole emitter is one file that re-spells a typed tree. There was no type inference, no scoping, no name resolution to write, because the compiler that produces the IR had done all of it and had been graded to 100% against the reference before this day began. The afternoon was cheap because the month before it was not.
Roc's module system fits a chapter. A void module, Slug :: [].{ ... },
with the chapter's types and definitions as associated items, reached as
Slug.name from anywhere else: that is a Codex chapter, and it is also what
an app imports. The one wrinkle was a module and a type sharing a name, and
the fix was to always qualify.
The gate did the work. Every change to the emitter was a full sweep, fifty-four units against frozen verdicts, and every sweep was green or named its failures by cause. The three regressions of the day (the tuple equality, the shadowing, the chapter identity) were each one cause, found by the sweep, fixed in one commit. When the debug compiler made the sweep five minutes it was still tolerable; the nightly made it twenty-seven seconds, and the difference is the difference between checking and guessing.
Measure before working around. The literal-checker slowdown looked like Roc being slow; it was the debug build's assertion loop, and a release build made it vanish. The cons-by-concat slowdown looked like the same kind of thing and was not: it is the language's cost model, and the emitter had to change shape. The two took the same first step, a profile and a micro-benchmark, and went opposite ways from there. I lost half an hour of the second one to a benchmark harness that read the wrong argument and measured the default branch; the clock said fast and the output would have said otherwise, had I read it.
The polish that a milestone leaves behind: the emitter's single-file mode can go, the README should say what the repository is now, and the probes in the platform want a decision. Then the eye test, for as long as Steve wants to watch a bird ride a tree.
| repo | revision | role |
|---|---|---|
| roc-lang/nightlies | nightly-2026-09-11-793f9d8 | the compiler |
| roc-apps | eed80e1 | modules, app, platform, the bird, the page on :9201 |
| rust-codex-compiler | aa0cfc0 | rocemit |
| safari-codex | units of 2026-09-10 | the Codex source and the verdicts |