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.