I’ve written a handful of languages in my life. None of them was mine.
There was Jack from nand2tetris, assembled from logic gates on up, all the way to a compiler that is an exercise in humility on its own. There was Lox from Crafting Interpreters, both passes: the tree walker and the bytecode machine. There were a few runs at brainfuck, because brainfuck is the shortest road to understanding what an execution loop really is. There was a small C-flavored language and a second one closer to JavaScript, which I wrote mostly to find out how much parser I could hold in my head at once. There was Logo, because a turtle on screen is still the most honest way to show someone that code does something. Some of it in C, some in Rust.
Each of them taught me something, and each left the same nagging sense of unfinished business: these were languages to work through, not to use. I’d finish the tutorial, get a working interpreter, close the editor, and never reach for any of it again, because why would I. They were mockups. Pretty, instructive mockups. And to this day I’m not sure how much they really taught me and how much was plain transcription - sometimes outright copying of someone else’s code that I didn’t fully understand.
This time I want something different. I want to design a language I’d actually reach for myself, and to build it publicly enough that every decision can be traced. This piece is about the why and about what it will look like. The full specification I keep separately in docs/spec.md. I want to show what led me to it and what it lets you write.
For now I’m calling it RuCoil.
What I was missing
When I look at all these toy languages, the same thing is always missing - and it isn’t the syntax. What is there to invent in syntax anyway? And besides, the more familiar the syntax, the more comfortable the language is to work with. What’s missing is something else entirely: a runtime that is interesting in its own right.
Because to my mind the most interesting things in languages don’t happen in the grammar. They happen in what the machine can do with a suspended program. Concurrency without threads. Coroutines that read like ordinary code. A call stack that is ordinary data - so you can dump it to disk, move it to another machine, and resume it. Hardly any tutorial even shows what interesting thing you could write in a language like that.
On top of that came a second thing, more down to earth. I want to write code that waits
for something: for the network, for a timer, for another task - and I don’t want that
waiting to define the rest of the program for me. I know the “function coloring” problem
from JavaScript and Rust: you have async, you have await, and suddenly half of your API
has to know which side of that boundary it stands on. I want a language where any function
can suspend, and await is just one of the places where that happens - not a separate
species of function.
And a third thing: I want the same program to run natively and in the browser. Not two implementations - one virtual-machine core and two hosts. Natively, because the developer loop is fast that way and the tooling is normal. In WebAssembly, because a browser playground you can send someone as a link is the best advertisement a language can make for itself.
Those are three threads. RuCoil is an attempt to coil them into one - which is where the second half of the name comes from.
The thesis in one sentence
A familiar surface, an unusual interior. Syntax from the Rust family, because I happen to
like that language: loops, braces, fn, match, everything you already know and don’t have to learn
again. All the originality sits in the runtime - in suspension, in capabilities, in
migrating a live program. Nobody should have to learn a new way of writing a loop. Everybody
should get to discover that the loop they just wrote can be frozen halfway through and
resumed on another computer.
Execution looks like this: source → RuCoil bytecode → a stack machine written in Rust. That machine is first an ordinary native program - the whole main series is built and debugged natively. Compiling that same machine to a WebAssembly component, all the way to a browser playground, is a reward for later, not the foundation.
The rest of this piece is examples. Because what tells you the most about a language is how code that gets something done looks in it.
“Hello” without the ubiquitous print
Let’s start with the smallest program, because even it gives away one of the main decisions.
fn main(sys) {
sys.console.print("Hello from inside the sandbox")
}
Notice that print doesn’t come from nowhere. There is no global print. There is
sys.console.print, and sys is an argument the host handed to main. This is the rule of
no ambient authority: nothing in RuCoil touches the outside world unless it holds the right
capability in hand. Authority enters the program through a single door - main(sys) - and
from there it travels hand to hand.
It sounds like ceremony, until you see what it buys:
fn main(sys) {
let one_host = sys.net.scope("example.com:443") // a narrowed capability
handle(one_host) // handle() can't reach anything else
}
and run it:
rucoil my-net-program --allow net=*.example.com:443
The handle function, to which I handed only one_host, simply cannot open a file or
connect to another server - because it was given nothing it could do that with. Least
privilege here isn’t a rule someone has to enforce. It falls out of how the values flow.
Errors that are values
Almost all error handling in RuCoil lives in ordinary control flow, because errors are
ordinary values of type result. Plus one operator that does all the everyday work - the
postfix ?:
fn load(sys, path) {
let text = sys.store.get(path)? // err -> return err from load; ok(t) -> t
let cfg = json.parse(text)?
ok(cfg)
}
? on ok(v) gives v; on err(e) it does return err(e). No exceptions flying up the
stack, no try/catch around every call. You read it top to bottom like an ordinary
sequence, and ? is that small mark saying “and if this one fails, we’re out”. This is the
layer where almost all coping with errors should live.
…and errors that are catastrophes
But not everything is an “expected condition the caller ought to handle”. Division by zero,
integer overflow, reaching past the end of an array, a match with no matching arm - these
are not values to pass along. These are traps: the machine unwinds the current task’s stack
and, by default, aborts it, printing a stack trace.
I keep these two layers apart on purpose. You don’t clutter ordinary code with failure
handling, and you don’t bury a catastrophe in a result nobody will read anyway. The bridge
between the layers is a single construct - catch - and its companion defer:
fn main(sys) {
let outcome = catch { // turn any trap into a value
let conn = sys.net.connect("example.com:79")
defer { conn.close() } // runs on success, trap, and cancellation
conn.read_line().to_int() // .to_int may trap: TypeError
}
match outcome {
ok(n) => sys.console.print("got ${n}"),
err(e) => sys.console.eprint(format_error(e)), // message + kind + stack trace
}
}
catch is the only way to turn a trap into a value. defer registers cleanup that runs
when the block ends in any way at all - normally, through return, through a trap, through
cancellation. It’s thanks to defer that code holding capabilities stays correct even as
the stack unwinds beneath it. Exactly where that line runs - why overflow is a trap while a
missing key in a map is an ordinary value - I spell out in the specification; here all that
matters is that I drew the boundary explicitly, rather than leaving it to intuition.
Concurrency without threads
This is where it gets interesting. All concurrency in RuCoil is cooperative and single-threaded - the virtual machine schedules it. There’s no preemption, no data races, because there’s no second thread. And yet you write code that does several things at once.
The key is the nursery: it scopes concurrent tasks and doesn’t finish until each of them
has finished. No orphaned tasks.
fn main(sys) {
let outcome = catch {
nursery |n| {
n.spawn(|| fetch(sys.net, "a"))
n.spawn(|| fetch(sys.net, "b")) // if this traps, the sibling is cancelled,
} // its defers run, and the trap surfaces here
}
match outcome {
ok(_) => sys.console.print("both done"),
err(e) => sys.console.eprint("something failed: ${format_error(e)}"),
}
}
The two tasks start side by side. If one fails, the nursery cancels the other, waits for it
to unwind (running its defer), and throws the first failure out of the block as a trap. An
unhandled failure never disappears quietly. And await, if it were needed here, is not a
separate species of function. Any function can suspend, at any suspension point: await, a
blocking channel operation, sleep, yield. Between those points the code runs
uninterrupted. And that’s the end of function coloring.
Because all of it - await, channels, sleep, generators - is one and the same mechanism of
suspension and resumption, just wearing a different hat each time.
Generators that read like a story
The same suspension mechanism gives you generators. gen fn returns a lazy iterator that
runs to the next yield and stops:
gen fn fibs() {
var (a, b) = (0, 1)
loop { yield a; (a, b) = (b, a + b) }
}
let ten = fibs().take(10).collect()
An infinite Fibonacci sequence written as a loop with a single yield. You take as much of
it as you want. Under for, generators, ranges, and channels lies one iterator protocol -
they all answer .next() with some(x) or none - so the lazy combinators work on each of
them the same way:
let total = (0..1000).filter(|n| n % 3 == 0).take(10).sum()
A game driven by coroutines
The prettiest example of what a “stack suspended inside its own value” gives you is a game. Take a falling bonus in an Arkanoid-style game. Instead of breaking its behavior into a state machine polled every frame, you write its whole life linearly, as a generator:
gen fn bonus(x, y) { // the whole life of a falling bonus, written linearly
var yy = y
while yy < 240 { yield (x, yy); yy += 2 }
}
fn main(sys) {
let screen = sys.gfx.open(320, 240)
defer { screen.close() }
var entities = [bonus(160, 0)]
loop {
match screen.poll_input() {
some(ev) => if ev.kind == "quit" { break },
none => nil,
}
entities = step_and_reap(entities) // .next() on each; drop the finished ones
screen.present(render(entities))
sys.clock.sleep(16) // suspension point: the frame budget
}
}
Each entity carries its own suspended stack, so its behavior reads like a simple story: “fall
until you reach the bottom of the screen”. The same machinery as with concurrency and
migration, only this time wearing the hat of a game. sys.gfx is deliberately a floor, not
an engine: one frame buffer, one event queue. Sprites and blitting you write in RuCoil
itself, as a library - which is a lesson in its own right.
The climax: freeze it, save it, resume it elsewhere
And here we reach the thing this is all being built for. Since the virtual machine has its own stack, and its heap is an arena of ordinary values addressed by handles, a suspended task can be captured, serialized, and resumed somewhere else.
fn main(sys) {
let task = spawn_detached(|| slow_counter(sys))
sys.clock.sleep(2500)
match task.snapshot() { // result<Snapshot, Error>
ok(snap) => sys.store.put("serialized_snapshot_001", snap.serialize()),
err(e) => return err(e),
}
// ...elsewhere, a machine with the same module image...
match Task.restore(sys, sys.store.get("serialized_snapshot_001")) { // resumes from where it parked
ok(t) => t.resume(),
err(e) => sys.console.eprint("resume failed: ${format_error(e)}"),
}
ok(nil)
}
A program counting away in the background is frozen halfway, saved to the store, and resumed, possibly even on another machine, exactly from where it stopped. Capabilities and resources don’t travel in that serialization: each of them becomes a hole that, on resume, is filled by the sys of a new host. Authority is granted on the spot, never smuggled inside the bytes. And this is exactly the thing none of my toy languages could do, and none of them could have - because none of them owned its stack.
What this actually buys
When I set these examples side by side, it’s clear this isn’t a collection of separate tricks. It’s one design decision - the machine owns its stack and its heap seen from a few different sides. Concurrency without threads comes from suspension, not from parallelism. Generators are the same suspension, only exposed as an iterator. Stack traces are cheap, because the stack is ordinary data you can walk. And migration of a live program works because handles-as-numbers survive the trip to disk, which native pointers would not.
One idea, seen four times. This is what I was looking for in all those mockups and found in none of them. These are also heavily simplified versions of features you’d find in the big languages - but the point is to build a small language within some sensible stretch of time.
What’s deliberately missing
To be fair about it - because I trust texts about languages least when they pretend to have no
trade-offs. RuCoil in this version is dynamically typed: nothing forces the caller to check
the returned result. catch is coarse-grained: you catch all failures at once and only
then branch on e.kind, instead of typed exceptions. Refcounting doesn’t reclaim cycles.
There are no shared-memory threads either, and if parallelism ever arrives, it will be as many
machine instances joined by channels, with migration as the primitive for moving work around.
Where this is going
This is a piece about intent, not about a finished thing. I’m no expert on programming languages. I don’t know whether everything I’ve set out to do will come together the way it’s planned, and I’m not sure I could even tell you whether the whole plan is feasible at all. What I do know is that I want to build it, and I’m getting down to it right now.
But before I wrote the first line of any of this, I wanted to know what for. I want a language I’d reach for myself. A language where a loop looks familiar and yet can be frozen halfway through and resumed somewhere else. A language that, for the first time, would be mine.
This is RuCoil.