The Birth of Event Continuations: The Night of August 27, 2025
The Birth of Event Continuations
A zero-cost abstraction that disappears.
What Are Event Continuations?
When you invoke an event in Koru, you explicitly handle its branches and chain into the next step:
~validate(value: 42)
| valid v |> double(value: v.result)
| ok doubled |> success(result: doubled.result)
| invalid e |> failure(msg: e.msg) The syntax is elegant. But the magic isn’t in the syntax.
The magic is what it compiles to.
The Disappearing Continuation
Here’s a complete Koru program with event definitions and a flow:
// Event definitions with their possible branches
~event validate { value: i32 }
| valid { result: i32 }
| invalid { msg: []const u8 }
~event double { value: i32 }
| ok { result: i32 }
~event success { result: i32 }
| done {}
// Implementations
~proc validate {
if (value > 0) {
return .{ .valid = .{ .result = value } };
}
return .{ .invalid = .{ .msg = "Value must be positive" } };
}
~proc double {
return .{ .ok = .{ .result = value * 2 } };
}
~proc success {
std.debug.print("Success: {}\n", .{result});
return .{ .done = .{} };
}
// The flow - event continuations in action
~validate(value: 42)
| valid v |> double(value: v.result)
| ok doubled |> success(result: doubled.result)
| done |> _
| invalid e |> failure(msg: e.msg)
| done |> _ What does this compile to? Zig code with tagged unions and switch statements:
// Events become typed structs
pub const validate_event = struct {
pub const Input = struct { value: i32 };
pub const Output = union(enum) {
valid: struct { result: i32 },
invalid: struct { msg: []const u8 },
};
pub fn handler(input: Input) Output {
if (input.value > 0) {
return .{ .valid = .{ .result = input.value } };
}
return .{ .invalid = .{ .msg = "Value must be positive" } };
}
};
// The flow becomes a function with exhaustive switches
pub fn flow0() void {
const result_0 = validate_event.handler(.{ .value = 42 });
switch (result_0) {
.valid => |v| {
const result_1 = double_event.handler(.{ .value = v.result });
const doubled = result_1.ok;
const result_2 = success_event.handler(.{ .result = doubled.result });
_ = result_2;
},
.invalid => |e| {
const result_1 = failure_event.handler(.{ .msg = e.msg });
_ = result_1;
},
}
} Look at what happened:
- Events → Tagged Unions: Each
| branchbecomes a variant in aunion(enum) - Branches → Exhaustive Switches: The continuation tree becomes switch arms
- Flows → Direct Function Calls: No runtime dispatch, no queues, no event loop
The event continuation has completely disappeared. What remains is plain, efficient, type-safe Zig code.
This is the core insight: Event continuations are a zero-cost abstraction. They give you a beautiful way to express branching control flow, and then they compile away entirely. No runtime overhead. No dynamic dispatch. Just function calls and switches.
The Zig compiler can then optimize this further - inlining handlers, eliminating dead branches, proving exhaustiveness at compile time.
The Monadic Structure
If you squint, you’ll recognize what this is: a free monad that compiles away.
Each event invocation is a monadic bind. The branch determines which continuation runs. The payload carries context forward. The |> arrow is >>= in disguise:
~validate(value) -- invoke computation
| valid v |> -- bind result, pattern match on sum type
double(v.result) -- chain into next computation This is Result<T, E> or Either with explicit continuation syntax. But unlike Haskell’s monads or Rust’s ? operator, the abstraction doesn’t just feel zero-cost - it literally disappears. No trait objects. No vtables. No monomorphization overhead. The compiler sees through the entire continuation tree and emits direct control flow.
The algebraic structure:
- Branches form a sum type:
| valid | invalid→union(enum) { valid, invalid } - Payloads form product types:
{ result: i32 }→struct { result: i32 } - Continuations form a tree: Nested
|>→ Nested switches
You get compositional semantics with zero runtime representation. The monad is a compile-time fiction that guides code generation.
This is the event continuation as computational monad in the Leibniz sense: the indivisible unit from which programs are composed. Not a library abstraction you pay for - a language primitive that exists only to instruct the compiler.
Branches Without Judgment
There’s something else hiding in the syntax. Compare:
Rust’s Result:
match file.read() {
Ok(data) => process(data), // The "happy" path
Err(e) => handle_error(e), // The "sad" path
} Exceptions:
try {
process(file.read()); // The "normal" path
} catch (Exception e) { // The "exceptional" path
handleError(e);
} Koru:
~file:read(path)
| success data |> process(data)
| not_found |> create_default(path)
| permission_denied |> request_elevation() See the difference?
Result<T, E> encodes a value judgment: Ok is good, Err is bad. One path is “happy,” one is “sad.” Even Either<L, R> has the convention that “Right is right.”
Exceptions are worse: there’s the “normal” flow and the “exceptional” flow. The exception path is literally designed to be ignored - you can always add another catch clause, or catch Exception and swallow everything.
Koru’s branches are morally neutral.
not_found isn’t an error - it’s an outcome. Maybe you create the file. permission_denied isn’t a failure - it’s information. Maybe you request elevation and retry. The branch names come from your domain, not from a generic success/failure binary.
And because they compile to exhaustive switches, you can’t ignore any of them. There’s no catch (Exception e) escape hatch. Every branch is a first-class citizen that demands explicit handling.
This matters because “happy path” thinking is a cognitive trap. Real systems have multiple valid outcomes, not just “worked” and “didn’t work.” Event continuations force you to model that reality.
But Where Did This Come From?
We did digital archaeology on our own codebase. We went looking for the exact moment event continuations were born. And we found it - not just one moment, but an entire sleepless night where two parallel concepts emerged.
The Night of August 27, 2025
1:18 AM - The Vision
In beist-core, a commit landed:
6f45470 docs: add VISION.md outlining BEIST's evolution into an autonomous development platform Deep in that document, a seemingly small detail:
“Production: Zig with ring buffers”
The idea that the event queue itself could be swapped out. Development in TypeScript, production in Zig. And the transport mechanism? Ring buffers.
2:42 AM - The Chains
84 minutes later, another commit:
bec355b docs: add S-expression workflow language spec and examples Inside was ROADMAP.md. And in that file, this:
BEIST> chain (if (tests:pass)
(deploy:prod)
(rollback)) BEIST> chain (parallel
(gpu:compile)
(cpu:compile)
(tests:run))
(join zig:link) There it is. Chains. Branches. Parallel execution. S-expression syntax describing event flow.
Two concepts, born 84 minutes apart:
- Ring buffers - the transport mechanism
- Continuation chains - the control flow
The Parallel Evolution
Once we had those timestamps, we could trace how both concepts evolved in parallel:
August 31, 2025 - The Ring Buffer Manifests
Four days later, beist-os appeared. In its ARCHITECTURE.md:
pub const EventQueue = struct {
buffer: []?Event, // Pre-allocated ring buffer
head: Atomic(usize), // Consumer position
tail: Atomic(usize), // Producer position
capacity: usize, // Power of 2 for fast modulo
}; Performance: 60M+ events/second. Lock-free. The vision from 1:18 AM made real.
September 2, 2025 - Continuations Become Data
The chain concept evolved. A new commit in beist-core:
71777a3 feat: add support for event continuations with nested data and chaining Events started carrying their own “what happens next”:
beist emit build:start --data '{
"then": "deploy:start"
}'
beist emit setup:complete --data '{
"then": ["compile:core", "compile:ui", "test:all"]
}' The then field. Events knowing their own destiny. Continuations as data.
September 10-14, 2025 - The Marriage
The two concepts merged in beist-rings. Events would travel through ring buffers, carrying their continuation trees with them:
const FileReadEvent = struct {
pub fn success(self: FileReadEvent, data: []const u8) void { ... }
pub fn file_not_found(self: FileReadEvent, path: []const u8) void { ... }
pub fn permission_denied(self: FileReadEvent, user: []const u8) void { ... }
};
const workflow = parseSexpr("
(file:read
(success file:process (ok file:save))
(file_not_found file:create (ok file:read))
(permission_denied security:alert))
"); Event types with compile-time defined continuation methods. S-expressions describing routing. Bloom filters for O(1) routing decisions. The architecture was complete.
September 16, 2025 - Koru
Two days later, a new repository. The S-expression syntax became something cleaner:
~file:read(path)
| success data |> process(data)
| not_found |> create(path) The pipe. The branch name. The arrow. The continuation.
20 days from sleepless night to programming language.
The Timeline
| Date | Time | Commit | What Was Born |
|---|---|---|---|
| Aug 27, 2025 | 1:18 AM | VISION.md | “Zig with ring buffers” |
| Aug 27, 2025 | 2:42 AM | ROADMAP.md | S-expr chains with branching |
| Aug 31, 2025 | 10:32 PM | beist-os | Lock-free ring buffer (60M events/sec) |
| Sep 2, 2025 | 1:56 AM | beist-core | then field - events carry continuations |
| Sep 10, 2025 | 10:39 PM | beist-rings | Bloom-routed ring buffers |
| Sep 14, 2025 | 5:55 PM | EVENT_CONTINUATIONS.md | Full architecture spec |
| Sep 16, 2025 | 5:46 PM | Koru initial | Language begins |
| Sep 17, 2025 | 7:44 PM | Hello World | It compiles |
Why Two Concepts?
What we found wasn’t just the birth of continuations. It was the birth of a complete event architecture:
The Transport (Rings):
- Lock-free ring buffers
- Pre-allocated memory
- Bloom filter routing
- Zero-copy where possible
- 177M messages/second
The Control Flow (Continuations):
- Events define their branches at compile time
- Routing described in S-expressions (later: Koru syntax)
- Trees carried in event payloads
- Cursor-based traversal
One without the other is incomplete. Ring buffers without continuations are just fast queues. Continuations without ring buffers are just slow trees.
Together, they’re a new way to think about event-driven systems.
The Artifacts
The original documents from that night are archived here:
- VISION.md (August 27, 2025, 1:18 AM) - “Zig with ring buffers”
- ROADMAP.md (August 27, 2025, 2:42 AM) - S-expression chains
- BEIST/OS ARCHITECTURE.md (August 31, 2025) - The ring buffer implementation
- EVENT_CONTINUATIONS.md (September 14, 2025) - The full specification
These aren’t polished design documents. They’re rough sketches from 2 AM, written by someone who couldn’t sleep because an idea wouldn’t let go.
That’s how languages get born.
What This Enables
Because event continuations compile to exhaustive switches on tagged unions, you get:
- Compile-time branch verification: Miss a branch? Compiler error.
- Zero runtime overhead: No event loop, no dispatch tables, no allocations.
- Fearless refactoring: Add a branch to an event, and every flow that uses it must handle it.
The continuation tree is a compile-time contract. The runtime cost is zero.
What’s Next?
Event continuations as a language feature are just the beginning. The same primitive - branches, payloads, and explicit next-steps - has implications beyond local control flow:
- Phantom obligations: Because the compiler sees all branches exhaustively, it can track what you MUST handle before scope exit. Auto-dispose, compile-time resource safety, GC ergonomics without GC runtime - all downstream from event continuations.
- Wire protocols: What if HTTP responses carried continuation trees instead of data to interpret?
- An operating system: We’re building a layer where event continuations are the computational primitive.
For now, the core insight stands:
Event continuations compile away completely. They’re a beautiful abstraction for the programmer that becomes zero-cost at runtime.
The Commits Don’t Lie
commit bec355b
Author: [Anonymous]
Date: Wed Aug 27 02:42:08 2025 +0200
docs: add S-expression workflow language spec and examples That’s the birth certificate. August 27, 2025, 2:42 AM, Central European Summer Time.
Fourteen months earlier, the same person wanted to make a fun game in Godot. The game still doesn’t exist. But the programming language does.
This post was created through digital archaeology - searching git histories across beist-core, beist-os, beist-rings, beist-zig, and koru to find the exact moments of inception. All timestamps are from actual commits. The evolution is documented in code.