The Calling Convention We Never Defined

· 9 min read

David Barbour (@awelonblue) has been exploring Koru for adaptation to a new assembly language of his, and he posted a thread of observations. They are the kind of observations you only get from someone reading your work through a fully-formed lens of their own — capabilities, session types, content-addressed code — and finding, to their apparent surprise, that it fits.

We’re going to answer his points. But we’re going to do something first that we’d recommend to anyone on the receiving end of a good question: restate it before answering it — both as a courtesy, so he can correct us if we’ve misread, and because half the work in a question this precise is figuring out what is actually being asked.

Koru is made as we go. It looks the way it does because it was discovered — by writing real programs and letting the compiler’s failures dictate the next feature — not specified up front from a theory. That matters here, because his questions land in the gap between the language we meant to build and the one we actually did.

The frame: control, not speed

One thing has to be said up front, because it reframes everything after it. When we floated the idea of an assembly backend, we pitched it the obvious way — fast codegen, bypass LLVM. He corrected us, gently:

I don’t consider assembly to be about performance of codegen or code. It’s more about absolute control, always holding the levers of power.

That is the lens for the whole thread. He is not here for throughput. He is here because he wants a model in which nothing is implicit — where you hold every lever yourself. Keep that in mind; it’s the difference between his questions and the ones a performance engineer would ask.

The question, restated

His first point, in his words:

Context and obligations map well. But the notion of ‘parameters’ does not. Parameters can be understood as a form of context. With an assembly, the context [is] registers and memory.

Here is where the restatement earned its keep, because the first thing we had to admit is that “context” is not our word. It barely appears in Koru as a language concept. He’s importing it — from capability calculi, from the machine, where “context” is simply the typed state available at a program point: registers and memory. In that frame, his observation is this:

Your obligations drop cleanly into that typed state — they’re linear resources, they live in the context, they map. But your parameters look like a second, separate category. At the machine there is no such category: a parameter is just another typed slot in registers or memory. So why do you treat parameters as a notion distinct from context, instead of as a degenerate form of it? Taken to its end: if an event already has access to the live state at its point in the flow, why does it need parameters passed to it at all?

That last sentence is the real one. He’s not quibbling about a keyword. He’s asking whether parameter-passing — a calling convention, a marshalling of values across a boundary — needs to exist in Koru at all.

What we found

Two things, and the second one is the one that matters.

First: a Koru event’s input list is not a calling convention. It’s the boundary of a bounded context. Its job is to take the outside world and name it into the event’s local vocabulary — a renaming at an interface, not a protocol for shoving arguments into registers. We had never written down “an event call is a function call” as a rule of the language. We’d never defined a call boundary at all.

Second: the one time a stack frame ever entered the story, it was an accident of the backend — and we’d written that down. Searching our own docs, there is exactly one place that ties an event to a stack frame, in the proof notes for a recursive value-returning event:

// Lars's hypothesis: an event call introduces a new stack frame, so value
// recursion should fall out for free. It does: the generated handler
// recursively invokes itself ... YES, recursion works.

Read that carefully. The stack frame is a property of the Zig backend emitting each handler as a function — and it was useful precisely because a function that calls itself gives you recursion for free. It was never a semantic commitment. It’s labeled a hypothesis, confirmed by what the emitter happened to do.

And then, later, we went the other way entirely. In The Recursion That Was a Loop we took a flow that re-enters its own event in tail position:

run = if(pc >= n or pc < 0)
| then => done { a, b }
| else |> ... |> run(pc: r.np, a: r.na, ...)
    | done res => done res

…and lowered it to a flat while loop with no frame at all — the invariant state never copied, never pushed. So within the same language we have two backend lowerings of the same frameless semantics: one that emits a self-calling function (and gets a frame, and recursion-for-free), and one that emits a loop (and gets no frame). Neither is the meaning. The meaning underneath both is that Koru never said there was a frame.

The answer

So here is what we can now say to him without flinching:

You’re right that at the machine a parameter is just context, and that passing it is a copy across a boundary. We already eliminate that copy when we can see the loop hiding inside a tail self-continuation. But the deeper thing you’re asking — whether an event needs parameters passed at all — has a better answer than we expected: our event inputs were never a calling convention. They name the outside world into a bounded context. We have never defined a call boundary at the language level. The only time a stack frame appeared, it was a side effect of the Zig backend emitting handlers as functions — handy, because it made value recursion free — and our native backend already lowers tail self-continuations to frameless loops.

Which means an assembly emitter that introduces no call boundary — that lets an event operate on the live context in place, naming what it needs, passing nothing — is not something we’d have to change the language to allow. It’s just a backend that declines to lower the naming into a convention. That’s the control you’re after, and the room for it was already there. We just hadn’t noticed we’d left it.

We did not design Koru to have no calling convention as a principled stand. We built a language about naming and obligations, emitted it to hosts that happen to have functions — and the convention he was worried about was never in the language to begin with. We simply hadn’t had occasion to notice the room we’d left.

One down

His next points go deeper — effects as full event types, effects raising effects, whether “session” is a better word than “event,” content-addressed singletons, and the stack frame as a typed memory obligation. We’ll take them one at a time, the same way: restate until we’re sure what’s being asked, then answer from the language as it actually is.