July 1, 2026

Systems Programmability in the AI Era: A Pure Go I/O Stack

A pure Go I/O stack for real-time communication systems, designed around performance, correctness, and systems programmability.

  • Go
  • io_uring
Systems programmability stack overview
Systems programmability stack overview.

Abstract

This article introduces a pure Go I/O stack (https://code.hybscloud.com) for real-time communication systems. The main idea is systems programmability while keeping performance in view. By systems programmability, I mean the ability to build systems code from explicit, composable program facts rather than implicit conventions.

In low-latency systems, performance and correctness are not separate topics. Before the AI era, experienced systems developers often had to keep many performance-critical runtime facts in their own heads:

  • file descriptor and socket lifecycle,
  • buffer ownership, lifetime, and reuse,
  • syscall results and errno,
  • event and completion polling and dispatching,
  • multishot lifecycle and zero-copy notification tracking,
  • concurrency, contention, and backpressure,
  • data structures and cache locality,
  • scheduler wakeups and context switching.

That can work when the person reading the code already understands the implicit conventions.

When AI agentic coding tools help developers write and modify this layer, these facts need to appear in the program structure without weakening correctness or performance.

If they disappear into callbacks, side maps, local retry loops, or implicit runtime conventions, the code can still run, but it becomes harder to reason about.

This stack keeps those facts visible: the lower layer keeps runtime facts explicit, iox gives I/O observations a shared meaning, and the upper layer represents computation, context, completion-driven system steps, and protocol steps.

The goal is to provide a source-visible programming model for cases where latency, allocation, buffer lifetime, completion identity, and protocol order must remain visible in the source structure.

Article Map

This article has five parts.

Part one explains the motivation. Why do real-time communication systems need explicit structure? Why is “just test and fix” not enough for some systems code? Why does performance work often become correctness work?

Part two explains the package map and runtime layer. This includes iox, zcall, iofd, sock, framer, iobuf, lfq, spin, atomix, dwcas, and uring.

Part three explains the computation representation layer. This includes kont, cove, takt, and sess.

Part four gives code sketches and the workflow: Lift, Reduce, Verify, Compile.

Part five explains performance motivation, measurement points, design requirements, limitations, and the closing summary.

The central idea is simple: do not hide the facts that affect performance and correctness. Instead, raise them into source-visible program structure.

Part I: Motivation and Thesis

This part states the thesis before the package details. The core point is that real-time communication systems need performance and correctness facts to stay visible in a single source structure.

The Short Version

The stack can be read as one equation:

runtime facts
+ iox outcome semantics
+ suspended computation
+ context evidence
+ completion-driven system step
+ protocol step
= systems programmability.

Each package contributes one piece:

  • iox names I/O outcomes.
  • zcall, iofd, sock, framer, iobuf, lfq, spin, atomix, and dwcas keep low-level systems facts visible.
  • uring turns Linux io_uring mechanics into programmatic concepts.
  • kont represents suspended computation.
  • cove attaches context evidence to suspended computation.
  • takt turns backend completions into system steps.
  • sess represents protocol steps as effects.

Together, these packages point in one direction: high-performance I/O that remains understandable, checkable, and programmable under real runtime pressure.

Overview diagram for the systems programmability stack
Runtime facts lifted into computation structure.

Background: Why I Care About This Now

The AI era is changing how software is written. More code can now be generated, modified, reviewed, and refactored by coding tools. That is useful, but it also changes the risk profile.

For systems programming, the old loop of generate, test, observe a failure, patch, and test again is not enough by itself. It can even become risky if the program structure hides the facts that matter.

In a real-time I/O stack, some bugs are hard to catch with ordinary tests.

They may depend on things like timing, kernel completion order, a buffer reused slightly too early, a completion arriving after a route changed, or a protocol branch that is locally valid but globally invalid for the session.

That is why I care about systems programmability. The goal is not just to make a program faster after it is written; the goal is to build the program from facts that are easier to inspect from the start.

For developers, that means the code should make clear which operation is pending, what progress was made, what context is required, which completion belongs to which computation, what protocol step is valid, and where the program is allowed to suspend.

For AI agentic coding, the same point matters even more. A coding tool should not have to infer important runtime state from scattered callbacks, side tables, implicit goroutine state, or undocumented retry loops; the structure should expose the facts directly.

This stack moves in that direction by making high-performance systems code more programmable: it represents the details that affect correctness instead of hiding them.

The Workload Context

The workload I keep in mind is real-time communication: online game servers, metaverse open worlds, interactive media systems, and low-latency collaborative services.

In these systems, latency does not come from one place. It comes from many small costs that multiply.

A world update may first pass through interest management, where the system decides which players should see which entities. That decision controls fanout. Fanout controls how many messages are built, and message building then controls serialization, buffer use, queue pressure, syscall pressure, and network pressure.

After that, the runtime still deals with copying, allocation, GC pressure, socket readiness, kernel submission, kernel completion, queue handoff, wakeup behavior, retry policy, timers, ordering, loss, and stale state.

That is why I do not treat performance as only throughput. For real-time communication systems, tail latency and correctness are tightly connected:

  • A hidden allocation can become a latency spike.
  • A hidden queue can become unbounded backlog.
  • A hidden retry loop can become a storm.
  • A hidden completion identity can resume the wrong computation.
  • A hidden protocol state can accept the wrong message.

The practical question is: which facts should remain visible in source code? This stack is one answer. It keeps the following facts visible:

  • progress and temporary no-progress,
  • buffer lifetime and reuse,
  • bounded queue pressure,
  • syscall and descriptor state,
  • socket operation state,
  • kernel completion identity,
  • suspended computation,
  • context assumptions,
  • system-level completion steps,
  • and protocol order.
Workload pressure diagram for real-time communication systems
Runtime pressure in real-time communication systems.

Why This Matters

Real-time communication systems are difficult because small hidden facts can become production incidents:

  • A file descriptor may be closed in one path while another path still references it.
  • A buffer may be reused too early while the kernel still owns it.
  • A retry loop may hide the difference between temporary no-progress and real failure.
  • A completion may arrive later, but the code may no longer know which suspended computation it belongs to.
  • A connection may reconnect, but an old completion may still look locally valid.
  • A protocol step may be valid in one handler, but invalid in the whole session.

These are more than testing problems. They are representation problems.

If the source structure hides the important facts, tests must rediscover those facts indirectly.

The question I care about becomes: can more correctness work move into the coding phase?

My current answer is to make the relevant runtime and computation facts explicit in the program structure. That is why I use the phrase source-visible.

Here, source-visible does not just mean that source code is available. It means runtime, computation, and control-plane facts remain visible in the source structure.

This gives developers and coding tools clearer structure to inspect, and it keeps performance decisions grounded in real runtime facts rather than only in high-level API convenience.

What I Mean by Programmability

Programmability is more than API convenience. A convenient API may be easy to call, but still hide the facts that the runtime must eventually handle.

Here, programmability means something more specific: the important behavior of the system can be described, composed, checked, and translated back into executable code.

For this stack, programmability has several layers:

  • At the I/O layer, the program should know whether an operation completed, made no progress for now, made progress and remains live, or failed.
  • At the buffer layer, the program should know which memory is owned by the caller, which memory is registered, which memory may be selected by the kernel, and when memory can be reused.
  • At the queue layer, the program should know whether a producer-consumer path has capacity or whether backpressure is now visible.
  • At the completion layer, the program should know which completion belongs to which submitted operation.
  • At the computation layer, the program should know where it can suspend, where it can resume, and what continuation is still live.
  • At the context layer, the program should know which assumptions must still hold.
  • At the protocol layer, the program should know which message order is valid.

That is why I describe the stack as systems programmability: building systems code from explicit, composable program facts.

Not every program needs this structure. The scope is more specific: some real-time communication systems need it when performance and correctness must be handled together.

What This Stack Is Not

It is not a general-purpose Go runtime. It is not a whole-program verifier. It does not automatically prove protocol duality.

It does not remove the need for tests, benchmarks, or production validation. It also does not claim that every system should bypass standard Go runtime paths.

The scope is more specific. Some systems need a path where low-level facts remain explicit: readiness, completion identity, buffer ownership, registered memory, multishot state, route lifetime, backpressure, and protocol order.

For those systems, this stack gives a more explicit programming model and a more direct runtime path for high-performance systems code.

Part II: Runtime Facts and Package Map

This second part moves from the thesis into the runtime layer. The goal here is to show why the lower packages exist before discussing the computation representation layer above them.

Package Map

The current public package index is https://code.hybscloud.com/. The packages covered in this article are: iox, zcall, iofd, sock, framer, iobuf, lfq, spin, atomix, dwcas, uring, kont, cove, takt, and sess.

They are best read as one stack around iox, not as a flat package catalog.

The lower layer is about runtime facts: syscalls, file descriptors, sockets, message boundaries, buffers, queues, short waits, atomics, and kernel completions.

The upper layer is about computing and the control plane: effects, continuations, context evidence, completion routing, system steps, and protocol steps.

The package roles are:

  • iox: I/O outcome semantics for progress and control-plane state.
  • zcall: low-level syscall results and errno.
  • iofd: Unix file descriptor lifecycle and non-blocking descriptor behavior.
  • sock: Unix socket primitives over iofd and zcall.
  • framer: message-boundary layer above stream transports.
  • iobuf: bounded buffer pools, aligned memory helpers, and I/O vector helpers.
  • lfq: bounded lock-free FIFO queues for producer-consumer paths.
  • spin: pause, spin, yield, and short-wait policy primitives.
  • atomix: atomic operations with explicit memory ordering.
  • dwcas: double-word compare-and-swap for composite atomic state.
  • uring: Linux io_uring mechanics as visible runtime facts.
  • kont: continuations, algebraic effects, frames, suspension, and trampoline evaluation.
  • cove: context evidence around values and suspended computations.
  • takt: completion-driven system-step layer.
  • sess: protocol steps represented as effects.
Layered package stack diagram
Layered package map around iox.

The Core Problem: Hidden Facts

Many production bugs in low-level asynchronous systems come from facts that are real but not clearly represented. For example:

  • the operation made progress, but returned a non-nil status;
  • the socket is not ready now, but it has not failed;
  • the multishot operation produced one completion, but remains live;
  • the buffer was selected by the kernel, but application code must recycle the correct buffer id;
  • the route is still active, but one completion must not resume the wrong computation;
  • the player context changed, but the old completion still arrives;
  • the protocol branch was chosen, but the peer handler is still written as ordinary callback code.

These facts matter for performance or correctness. If a runtime path hides them, higher-level code must reconstruct them by convention.

That usually leads to more side maps, more repeated checks, more callback state, more retry loops, and more scattered lifecycle rules.

The approach in this stack is to keep the facts local, typed, and composable.

iox: Progress Is Not Control

iox is small, but it is the semantic center of the stack. The main idea is:

I/O observation = progress already made + control-plane state for the next step

Go’s standard io model already has a practical shape:

n, err := r.Read(buf)

For blocking I/O, this is often enough. For non-blocking and completion-driven I/O, the error slot also carries control information. iox names the main cases:

  • nil means the current step completed.
  • ErrWouldBlock means no further progress is possible now. The next valid move is to yield or wait for readiness or completion.
  • ErrMore means progress happened, and the operation remains live. The next valid move is to keep observing the live frontier.
  • Any other error means real failure.

The most important rule is: process progress first, then interpret control-plane state.

If n > 0, that progress is real, and the control-plane state must not erase it. This is especially important near io_uring, where one completion may carry both a result and route-lifecycle meaning.

iox gives the rest of the stack a shared local vocabulary: finish, wait, continue, or fail. This vocabulary later lets takt route a completion into the next computation state.

It also gives the higher layer a cleaner signal for scheduling policy:

  • If the current operation made progress, the stack can continue evaluation.
  • If the current operation made no progress, the stack can yield, wait, or back off by explicit policy.
  • If the operation remains live, the stack can keep the live frontier visible.
  • If the operation failed, the normal I/O path ends.

The point is not that iox schedules anything by itself. It does not. Its value is that it names the state that a scheduler, event loop, or completion layer needs to see.

Concretely, iox stays a small vocabulary rather than a runtime. iox.Classify turns a raw result into one of the four cases above, and iox.IsWouldBlock is the narrow predicate used where only the no-progress case matters (the Quickstart below uses it on the completion path).

For the no-progress case, iox.Backoff offers an adaptive wait policy that a caller may apply on its own schedule; iox never applies it on the program’s behalf.

Two pieces of an observation stay separate on purpose: the progress already made and the control-plane state for the next step. This split is a convention, not an ordering or a lattice.

The progress count answers “what happened in this step”, and the control-plane state answers “what may happen next”. Keeping the two apart is exactly what lets a higher layer commit progress before it interprets the next move, which is why a partial read that also reports ErrWouldBlock is never silently dropped.

iox outcome semantics diagram
iox separates progress from control-plane state.

Runtime Base: zcall, iofd, sock, framer

The runtime base keeps syscall, descriptor, socket, and message-boundary facts visible.

zcall provides syscall primitives. Its role is to keep the kernel call, raw result, and errno explicit. This matters when the caller intentionally owns scheduling and resource-safety decisions.

iofd provides Unix file descriptor abstractions. Its role is to keep descriptor state, close behavior, and non-blocking read/write behavior in one place.

This matters beyond convenience: file descriptor lifecycle is a real systems concern. Leaked descriptors, double-close patterns, and invalid close ordering are common sources of hard production failures.

sock builds socket primitives above iofd and zcall. Its role is to keep socket creation, address handling, options, accept, connect, read, write, and non-blocking behavior explicit. Go’s net package is a strong default; this package exists for paths where the stack needs more direct control over sockets.

framer restores message boundaries above byte streams. Protocols usually carry messages, not raw byte fragments. Without a framing layer, each protocol tends to rebuild length parsing, partial-read handling, oversize checks, and retry behavior.

The base layer is deliberately practical. It exists because higher-level programmability depends on preserving lower-level facts.

Each package has a narrow role rather than trying to become the whole runtime. zcall keeps the raw call result and errno, iofd keeps descriptor lifecycle and the non-blocking flag, sock keeps socket operations together with their addresses and options, and framer keeps the message boundary.

When the same fact is needed three layers up, it is read from one source instead of reconstructed from scattered conventions.

Runtime Pressure: iobuf, lfq, spin, atomix, dwcas

Real-time I/O paths also carry several kinds of pressure: memory, queues, short waits, and atomic state. This layer keeps each one visible: iobuf for buffers, lfq for queues, spin for wait policy, and atomix with dwcas for atomic state.

iobuf is the buffer layer. It provides bounded, lock-free buffer pools, page-aligned memory helpers, and I/O vector helpers. In low-latency I/O, buffers are not just temporary byte slices. They affect memory use, GC pressure, copy behavior, registered buffer paths, provided buffer paths, and scatter-gather I/O. A bounded pool keeps that pressure visible instead of letting it appear later as allocator and GC noise.

lfq is the lock-free queue layer. It provides bounded FIFO queues in the four standard producer-consumer shapes: SPSC, MPSC, SPMC, and MPMC. This lets the caller declare the producer-consumer shape of a path. The key point is boundedness: a full queue exposes backpressure as a visible would-block fact, and an empty queue exposes no-input state as a separate would-block fact. That is different from letting full queues become unbounded memory growth or empty queues become hidden waiting loops.

spin provides the short-wait policy: a CPU pause hint, an adaptive spin-wait, a non-fair spinlock for very short critical sections, and a cooperative yield. Short waits are not all the same. A hot path can pause, spin briefly, or move to a longer wait, and spin keeps that choice local and explicit instead of scattering ad-hoc loops.

atomix provides typed atomics with explicit memory ordering. The chosen ordering, whether relaxed, acquire, release, or acquire-release, is named at the use site. That makes the synchronization edge explicit, and lets the caller choose the minimal ordering the algorithm needs instead of assuming sequential consistency.

dwcas provides an aligned 128-bit, double-word compare-and-swap. It lets a lock-free algorithm move two machine words together in one atomic step, such as a pointer plus a version, or a state plus a sequence number, on a contiguous, 16-byte-aligned value.

Together, these packages keep runtime pressure visible: buffer capacity, queue fullness, wait policy, contention, and atomic ordering. Overload still requires policy, but the code now has explicit places to observe pressure and choose what to do, instead of leaving allocation, blocking, retry, and ordering behavior implicit.

uring: Linux io_uring in pure Go

uring is the Linux I/O submission-completion layer of the stack. At a glance, it provides a Go interface to Linux io_uring. Its role is more than wrapping syscalls: it absorbs raw io_uring mechanics while keeping important runtime facts visible.

Linux io_uring is powerful, but it has many moving parts.

A program may need to prepare SQEs, submit work to the kernel, receive CQEs, decode user_data, match completions back to operations, manage registered files, fixed buffers, provided buffers, and buffer rings, handle multishot results, and track zero-copy notifications.

These details are operational facts. They decide which operation completed, which file descriptor was involved, which buffer group was used, which buffer id was selected, whether the operation remains live, whether the completion is a zero-copy notification, and when memory may be reused.

If every higher layer handles these details directly, the system becomes difficult to reason about. If the details are hidden too far, higher layers lose the facts they need.

uring takes a practical middle ground: it absorbs the mechanics, exposes the observable facts, and leaves policy above the package.

This separation is important. If the io_uring layer becomes the whole application runtime, it may hide policy decisions inside a low-level wrapper. If it exposes only raw mechanics, every caller must rebuild lifetime and completion rules.

uring sits between those extremes: it absorbs low-level complexity, but it still exports the facts that higher layers need.

For example, higher layers should not need to rebuild SQE lifetime handling, guess how a CQE maps back to submitted work, lose buffer identity, or erase multishot state. They should still be free to decide policy: how to retry, how to back off, how to cancel, how to route, and how to resume computation.

Ring Lifecycle and Options

A ring is created in an unstarted state and configured through an Options value. The options cover practical io_uring knobs: the number of entries, locked-memory limits, read and write buffer sizing, multishot buffer sizing, multi-issuer mode, successful-completion notification, the indirect submission-queue array, 128-byte SQEs, and hybrid polling. By default, a ring asks for a single-issuer shape with deferred task-run and no submission-queue array unless the options request otherwise. This matters because the single-issuer path can skip shared submission locking on the hot path.

Start is where setup actually happens: it registers probe data, registered buffers, and provided buffer rings or groups, advances the rings, and enables the ring.

Stop is an idempotent final teardown, and it has a clear contract: the caller must drain in-flight operations, reap outstanding completions, and stop live subscriptions first.

Two small accessors, SQAvailable and CQPending, expose submission and completion pressure directly, so a loop can see how much room it has before it submits and how many completions are waiting to be reaped.

One option deserves a note because it is a real performance lever. When successful-completion notifications are turned off, write-like operations that support it will skip their success completion, which reduces completion-queue traffic for the common “it worked” case. That is a policy choice the caller makes, and the package keeps it explicit rather than deciding it silently.

SQE and CQE Context

uring makes submission and completion identity explicit. SQEContext is a 64-bit value that encodes the io_uring user_data field, and the package supports three context shapes with different cost and capacity:

  • Direct mode packs the operation, the SQE flags, the buffer group, and the file descriptor inline into the 64-bit context. This is the fast path, and it needs no extra allocation.
  • Indirect mode stores a pointer to a 64-byte IndirectSQE when the inline budget is not enough.
  • Extended mode stores a pointer to a 128-byte ExtSQE that keeps a fuller SQE plus additional runtime context.

Extended mode carries an important systems detail. The ExtSQE.UserData region is raw storage that the garbage collector does not trace, so any pointer-bearing value must stay rooted somewhere the collector can see; the package keeps bounded, GC-visible sidecar roots for its own extended-mode helpers.

A ScopedExtSQE lets caller code borrow an extended context and then either release it or transfer responsibility after submit, so ownership of that context is explicit at the call site rather than left to chance.

The submission path is equally explicit. Reserving a submission slot observes the submission-queue head and tail and returns iox.ErrWouldBlock when the queue is full, so a full ring is a visible no-progress fact rather than a hidden stall.

Publishing a slot writes user_data, handles the submission-queue array when it is enabled, and uses a release barrier before publishing the new tail. The package also keeps slot-owned staging values alive until the kernel consumes the SQE, which covers staged byte buffers, iovecs, socket addresses, path buffers, timespecs, and message headers.

Caller-owned operation buffers still follow the caller’s own lifetime contract: keep the buffer valid until completion.

When a completion arrives, the package decodes it into a CQEView. That view exposes result, flags, the encoded context, operation, file descriptor, buffer group, buffer id, the MORE bit, selected-buffer evidence, notification evidence, and bundle metadata.

Negative results are mapped into the package error model, and EAGAIN maps to iox.ErrWouldBlock, which keeps the kernel’s “try again” answer inside the same iox vocabulary the rest of the stack uses. There are also fast paths, WaitDirect and WaitExtended, for rings that use only direct or only extended contexts.

Raw user_data therefore does not stay a mystery integer: it becomes typed runtime evidence, and a copied completion view is exactly one observation, while any long-lived route state stays above uring.

Buffer Handling

Buffer handling is one of the most important parts of this package. io_uring has several buffer paths: ordinary submitted buffers, registered fixed buffers, provided buffers, provided buffer rings, and bundle receive. Each path has a different ownership shape:

  • With ordinary submitted buffers, the question is lifetime: who keeps the submitted memory alive until the kernel consumes the operation?
  • With registered fixed buffers, the question is stable identity: which registered buffer index is the operation using?
  • With provided buffers, the kernel can choose a buffer from a group, and the completion must report which buffer id was selected.
  • With provided buffer rings, the application must make buffers visible to the kernel, then recycle them correctly.
  • With bundle receive, one completion may describe multiple received buffers. The BundleIterator shape turns that one completion into explicit buffer iteration and recycle behavior.
  • With incremental receive, a single selected buffer can be consumed in parts; an incremental receiver tracks the partial-consumption flag so that “there is more in this buffer” stays a visible fact.

Each path carries a small ownership contract, and uring keeps it stated. Ordinary buffers must stay valid until the operation completes. Registered fixed buffers are referred to by a stable index. Provided buffers are chosen by the kernel from a group, and the selected buffer id comes back in the completion flags. Provided buffer rings must be made visible to the kernel and then recycled with release ordering before the ring tail is published.

This is the kind of complexity uring absorbs. It does not hide that buffers exist; it makes buffer identity, selection, and recycle timing visible to higher layers.

Multishot and Zero-Copy

io_uring is not limited to one request and one completion. Some operations may produce many completions, and some advanced paths may produce related completions that arrive in a difficult order.

uring makes the multishot lifecycle explicit through a subscription that tracks three states: active, cancelling, and stopped. A multishot accept or receive allocates an extended context, anchors its ownership, and keeps that context alive until a terminal completion arrives without the MORE bit.

Dispatch validates extended mode, the raw user-data identity, and sidecar ownership before it hands a completion to a callback.

Cancellation follows kernel reality: requesting cancellation submits an async cancel, but the subscription stays live until the terminal completion is observed. Unsubscribing suppresses future callbacks and issues a best-effort cancel. A live multishot operation is therefore never treated like a normal one-shot request.

The package also makes the zero-copy notification lifecycle explicit. A zero-copy send may produce a data completion and a separate notification completion, and those two can arrive in either order.

A tracker handles all three real arrival orders: data completion first, notification first, and the terminal fallback where no notification arrives. The extended context is released only after the notification or that terminal fallback, and the caller must keep the zero-copy buffer valid and unmodified until then.

A related receive-zero-copy path exists for newer kernels; the package treats it as a capability-checked path, so not every capability is assumed on the baseline kernel.

This is the sense in which uring absorbs systems complexity. It is close enough to the kernel to preserve important performance facts, and structured enough for higher-level code to reason about those facts.

Memory Budget

One more piece keeps memory planning out of scattered call sites. Helper functions turn a memory budget into ring and buffer-group sizing. They clamp the budget and select among power-of-four buffer-size tiers, so a higher layer can ask for “this much memory” and get predictable ring entries and buffer groups instead of hand-tuning each call. This follows the same package shape: the policy input is explicit, and the mechanical sizes are derived in one place.

uring io_uring mechanics diagram
uring exposes submission and completion facts.

Part III: Computing and the Control Plane

This third part explains the upper layer: how computation can suspend, carry context, resume from completions, and keep protocol order visible in the source structure.

kont: Suspended Computation

kont is the foundation of the computation representation layer. In asynchronous systems, local evaluation often reaches a point where it cannot continue immediately. It may need to wait for a read, a write, a timer, a completion, a message, or a protocol step.

Go’s goroutines are a strong general-purpose solution. In this stack, the suspension point itself also becomes a visible program structure.

That is the role of kont.

A simple way to read kont is: two representations, one shared suspension point, and trampoline evaluation.

The first representation is Cont, the continuation-passing representation. It is useful as a reasoning form, and it is close to how developers read computation: do this, get a value, then continue.

The second representation, Expr, turns continuation structure into defunctionalized frames. This means the continuation is no longer only a Go closure but explicit data. (Defunctionalization is the classic technique of replacing closures with a fixed set of data constructors plus an interpreter.)

The frames are a return frame, a bind frame, a map frame, a then frame, an effect frame, and an unwind frame. This gives the runtime a frame-oriented shape.

Reify converts from Cont to Expr, and Reflect converts from Expr to Cont. The shared concept is Suspension. Step and StepExpr evaluate until the computation completes or reaches one pending effect operation. At that point, kont returns a Suspension, which carries the pending operation and a one-shot resumption for the rest of the computation.

Both representations share one iterative evaluator. It walks an explicit chain of frames in a loop rather than through nested Go calls, which is what keeps deep computations from overflowing the Go stack; deep-chain tests exercise exactly this.

The same evaluator runs in different modes depending on what the caller wants: one mode dispatches effects and runs straight through to completion, one mode stops at the first effect and yields a Suspension, and one mode converts frames back into the closure form.

The mode, not a separate engine, is what makes Step run to the next suspension while a full handler runs to the end.

This matters. The paused computation is no longer only hidden runtime state. It becomes a value another layer can store, inspect, resume, or discard.

The affine rule matters too: a suspension can be resumed at most once. (Affine here means “used at most once.”) That prevents one computation frontier from accidentally splitting into two futures.

Because a suspension is a value, the ways to consume it are also value-level operations. Resume advances it to the next suspension or to completion, TryResume does the same without panicking on misuse, and Discard throws the rest of the computation away and releases any pooled frame chain.

Exactly one of these may be applied: a second use of the same suspension panics, which is the affine rule made operational.

One convention is worth stating plainly: a nil resume value is treated as completion with the zero result, so a meaningful nil must be wrapped if it has to be distinguished from “done”.

The trampoline keeps evaluating while local computation can still reduce, and it stops only when a real external result is needed. (A trampoline is a loop that drives one reduction step at a time instead of growing the call stack.) That is both a performance discipline and a programmability discipline.

kont is also more than continuations and suspension; it includes a standard algebraic-effect toolkit built on the same machinery. There are reader, state, and writer effects, an error effect with throw, catch, and an Either result, a resource effect with bracket-style cleanup, and delimited control through shift and reset, along with composed handlers for common combinations.

The point for this article is not the API catalog but the shape: the effectful parts follow an operation-plus-handler discipline, so the same rule applies broadly: evaluate until an effect must be handled, then decide how to continue.

A few implementation choices keep this affordable on hot paths. Performing an effect uses pooled marker values, the evaluator flattens transient frame chains, reuses frame nodes, clears them before returning them to the pool, and named generic identity continuations avoid anonymous-closure allocation on some entry paths.

This reuse relies on the same at-most-once discipline used by suspension. A suspension can be consumed once, and pooled frame chains are returned only after that path is done. So the performance rule and the correctness rule line up.

In the examples below, application operations carry kont.Phantom[Result], which binds an operation type to the result type it will produce. kont.ExprPerform builds the effect that performs an operation, kont.ExprMap transforms its result, and kont.Eff[A] is the common alias for an effectful computation that yields an A.

Suspension, Scheduling, and Context Switching

One performance idea behind this stack is to reduce unnecessary scheduler handoffs. The goal is to keep evaluating while useful local progress is still available, and to yield or wait only when the computation reaches a real no-progress point.

In ordinary asynchronous code, waiting is often tied to a runtime mechanism: a goroutine parks, a callback returns, a future is pending, or a scheduler later wakes the work again.

Those mechanisms are useful, but they can also make different waiting reasons look similar.

In this stack, the distinction is sharper:

  • kont says where the computation can suspend.
  • iox says whether the current I/O observation made progress, made no progress, remains live, or failed.
  • takt says how a backend completion advances the system step.
  • uring supplies the Linux io_uring completion facts.

Together, these facts let a runtime loop ask a more precise question: can this computation still move now? If yes, continue. If no, wait, back off, or park by explicit policy.

This is why the trampoline idea matters. The trampoline keeps reducing the representation step by step in the current execution path, without treating every local reduction as a scheduler handoff. The program can keep moving while local progress is still available, and stops only when it reaches an actual suspension point.

At that point, the pending operation is explicit, the continuation is captured in the suspension, the later completion can be matched back to that suspension, and the next computation state can be decided from the iox outcome.

This is the bridge between performance and programmability: the loop can avoid yielding on every local reduction, while the source code still shows where suspension and resumption are allowed.

Evaluate until suspension diagram
Evaluate until suspension, then resume from completion.

cove: Context Evidence Around Suspension

Suspension creates a context problem. A computation may suspend while one condition is true, then resume after the world has changed.

Here, “world” does not need to be abstract: it can be the small runtime context that matters for one suspended operation. For example, a player is still connected, a match phase is still valid, a send budget still exists, a file descriptor still belongs to this route, a buffer lease is still valid, or a capability is still available.

If these assumptions live only in globals, side maps, or informal convention, the code can resume under the wrong context. cove gives this context a programmatic shape.

A simple way to read cove is: value with context, requirement over context, suspension with context, allowed movement from one world to the next, and finite step credit for bounded checking.

  • View represents a focused value under ambient context.
  • Req and ReqExpr represent requirements over that context.
  • Rule and RuleExpr add names to requirements for diagnostics.
  • Checked and Guarded pair values with requirements or rules, and expose a View when those checks hold.
  • SuspensionView pairs a kont suspension with context.
  • StepWith and StepExprWith run a computation until completion or suspension, while keeping context attached.
  • StepWithIndex and StepExprWithIndex add a finite step index, so a bounded suspension path can be inspected.

A View is a value paired with its ambient context, and the operations on it read like a small context calculus: observe or extract the value, ask for the context, duplicate the context, and extend it through a function that consumes the whole view.

A Cmd is a command that consumes a whole view rather than a bare value, and commands compose in that context-aware way.

This is the coeffect side of the layer: where an effect produces output, a coeffect consumes context, and cove makes that consumption explicit.

Requirements come in two forms. A closure requirement is a predicate over context, with the usual combinators (all, any, not, and a pullback that re-expresses a requirement under a different context).

A data-shaped requirement carries the same logic as an inspectable structure (true, false, atom, not, all, any), with a zero value that means true. The data form is structured but not fully symbolic, because an atom still stores a Go function; that is an honest limitation, not a serialization format.

Rules add diagnostic names on top of requirements so that a failure can say which named condition did not hold, and a Checked or Guarded value exposes its View only when its requirement or named rule holds.

This is where cove composes with kont: the suspension point comes from kont, and cove attaches the context evidence needed to observe or resume that point.

The movement becomes: step the computation, reach a suspension, observe the carried context, check the local requirement, move to an allowed next world, and then resume under that context, or reject the movement according to caller policy.

cove also includes verification-oriented structures: preorders, transitions, relations, forcing, and step-indexed suspension views. A preorder over worlds describes which next world counts as an allowed successor, and a step index bounds how much external progress a checking path may consume. (Step-indexing is a technique that makes such reasoning well-founded by counting steps.) The practical meaning is a small set of questions: what world am I in now? which next world is allowed? which requirement must remain true? how many external steps are allowed for this checking path?

These structures fit together in a specific way. A preorder says which worlds count as successors; a transition check confirms that a concrete move is allowed; forcing asks whether a requirement is monotone, meaning that once it holds it keeps holding as the world advances along the preorder; and a relation states what must hold at a world, step index, and value. CheckCompletedRelation applies that relation when a finite, fuel-bounded frontier completes.

Resuming toward a later world consumes one unit of fuel and requires the later world to extend the current one under the supplied preorder, so “resume” is gated by both a budget and an admissibility check.

Used together, these turn “the world changed while we were suspended” from an implicit hazard into a checked transition.

This matters because many asynchronous bugs are not local value bugs. They are “the world changed while the computation was suspended” bugs.

This does not make cove a whole-program verifier. It helps developers define and check the context rules they care about.

It does not schedule, retry, or dispatch completions. It keeps the context side of suspension explicit, so higher layers can stay programmable without hiding the runtime facts.

takt: Completion-Driven System Steps

takt is the completion-driven system-step layer. It is close to an abstract proactor model: the program submits work first and handles the completion later, rather than waiting for readiness and then acting.

When the completion arrives, the runtime needs to answer two questions: which suspended computation should this completion resume, and what is the next valid movement?

takt answers the first question with backend tokens and live pending maps. iox answers the second question by classifying the completion result. The core movement is:

suspended computation + backend completion + iox outcome -> next computation state

The cases follow the iox outcomes:

  • If the completion is successful, the suspended computation resumes.
  • If the completion reports ErrWouldBlock, the operation made no progress. The pending suspension can be submitted again, and the backend can decide how to re-arm or retry the operation.
  • If the completion reports ErrMore, the operation has a live frontier. For the one-shot loop, that is rejected because the one-shot loop owns one affine suspension per submitted operation. For subscription-style routes, the route-indexed loop has a separate shape for successor observations.
  • If a poll-level failure happens, the loop enters a fatal state and drains pending suspensions. Per-completion failures are still correlated with a token, so the backend can deliver the operation result through the resumption value.

This split matters. One-shot completion, idle polling, live route observation, and fatal poll failure are different control-plane facts, and takt keeps that difference explicit.

It also keeps token reuse and route reuse visible as errors, which avoids silently aliasing two pending frontiers under one identity.

The two loop shapes should stay separate. The one-shot loop owns at most one pending suspension per token, and reusing a token that is still live is reported as an error rather than silently overwriting the earlier frontier. It also rejects a live-frontier (ErrMore) completion as unsupported, because a one-shot token is not the right identity for a stream.

The subscription loop is the route-oriented counterpart: it identifies a route by a token plus a generation number, so a token reused after a route retires cannot be mistaken for the old route.

A stream completion that reports “more” keeps the route live, a final completion retires it, an unknown route is treated as stale and ignored, and a cancellation marks the route as cancelling but leaves it live until a terminal completion, a drain, or a fatal backend error.

Completion buffers themselves are a small policy choice rather than a hidden allocation. A completion-memory layer supplies buffers for the loop to poll into: the default keeps it simple, and a bounded option draws from an iobuf pool so completion buffering can be capped. This layer changes only where the buffers come from; it does not change token, backend, or outcome semantics.

In code, a backend implements two methods, Submit(kont.Operation) (takt.Token, error) and Poll([]takt.Completion) (int, error), and takt.NewLoop builds a loop over that backend. A takt.Completion carries a token, a value, and an error, so completion identity and result travel together.

In this stack, takt is the system step. iox classifies the local I/O observation, kont carries the suspended computation, and takt decides which computation advances when the backend reports completion.

sess: Protocol Steps as Effects

sess represents communication protocol steps as effects. In sess, communication has structure: send, receive, choose a branch, offer branches, loop, and close. This is close to the idea of session-typed protocols, where interaction is a sequence of ordered steps. In this Go stack, sess keeps that order visible in the source.

The current source provides Send, Recv, SelectL, SelectR, Offer, Close, and loop helpers. These operations are built as kont effect operations, so a protocol step is also a suspended computation step. The user program asks to send, receive, choose, offer, or close, and the remaining computation stays represented.

In the endpoint transport, sess uses bounded lock-free SPSC queues from lfq. When a queue is full or empty, the operation reports iox.ErrWouldBlock, so queue state remains visible: the requested protocol step is still pending, but it cannot make progress right now.

The transport is deliberately small and explicit. A connected endpoint pair carries four bounded single-producer single-consumer queues, one for data and one for choice in each direction, each with a fixed small capacity, plus a shared atomic close counter.

A choice is encoded compactly: the left branch is a preallocated “true” signal, the right branch is a preallocated “false” signal, and offering a branch dequeues the peer’s choice and returns it as a preboxed left-or-right value.

Send writes a value into the ring, and the receive path type-asserts it back, so the queue stores the value rather than aliasing the sender’s slot. Close increments the shared counter; it never blocks, and it does not enforce later use discipline, which is left to the caller.

Two worlds mirror each other above this transport. The closure world offers helpers such as send-then, receive-bind, close-done, select, and offer-branch, written in direct continuation style. The expression world offers the same helpers built from explicit frames and pools, which is the form a frame-based runtime executes.

Recursion is first-class through loop helpers whose step returns “continue with this next state” or “finish with this result”; the expression-world loop iterates without growing the Go stack, and the package’s tests exercise this with deep recursion.

There is also a composed session-plus-error execution mode where a thrown error can short-circuit a paired session, as well as endpoint delegation, where an endpoint value can itself be sent and received.

Running a protocol does not require Go channels or extra goroutines. The paired runner steps both programs and interleaves the two sides on the calling goroutine, backing off only when neither side can make progress; the single-endpoint convenience runner blocks over iox.ErrWouldBlock using iox.Backoff.

One important limit is that sess provides binary choice directly, so an n-ary choice is expressed by nesting binary choices, and protocol duality and post-close discipline remain the caller’s responsibility rather than a static guarantee.

sess is not a wire framing layer; it is not a network transport, and it does not automatically prove protocol duality. Its role is more specific: make protocol steps explicit and make complex asynchronous protocol behavior easier to represent.

Computation representation layer diagram
Computation representation above the runtime layer.

Part IV: Examples and Workflow

This fourth part shows the shape of user code above the stack. The examples are intentionally sketches: they show how the concepts fit together, not complete production servers.

Quickstart Sketch: kont, takt, and uring

This sketch shows the shape of the stack. It is not a complete server. The point is the connection between layers: the user program describes an operation as a suspended computation, the backend submits it, and the completion loop resumes the correct computation.

type writeDone struct {
    N   int
    Err error
}

type writeOp struct {
    kont.Phantom[writeDone]
    FD  iofd.FD
    Buf []byte
}

The program is small:

program := kont.ExprMap(
    kont.ExprPerform(writeOp{
        FD:  fd,
        Buf: payload,
    }),
    func(done writeDone) int {
        if done.Err != nil {
            return -1
        }
        return done.N
    },
)

For this completion-driven sketch, the ring asks for successful write completions:

ring, err := uring.New(func(opt *uring.Options) {
    opt.NotifySucceed = true
})
if err != nil {
    return err
}
if err := ring.Start(); err != nil {
    return err
}
defer ring.Stop()

The backend owns the runtime mechanics. On submit, it claims an extended SQE, packs the takt token into the submission context so the completion can be correlated later, and submits the write:

type backend struct {
    ring   *uring.Uring
    next   takt.Token
    routes map[takt.Token]writeOp
}

func (b *backend) Submit(op kont.Operation) (takt.Token, error) {
    write, ok := op.(writeOp)
    if !ok {
        return 0, errors.New("unsupported operation")
    }

    ext := b.ring.ExtSQE()
    if ext == nil {
        return 0, iox.ErrWouldBlock
    }

    b.next++
    tok := b.next

    uring.ViewCtx(ext).Vals1().Val1 = int64(tok)
    ctx := uring.PackExtended(ext).WithFD(write.FD)

    if err := b.ring.Write(ctx, write.Buf); err != nil {
        b.ring.PutExtSQE(ext)
        return 0, err
    }

    b.routes[tok] = write
    return tok, nil
}

The poll side maps completions back. The token decoded from user_data and the token-keyed routes map confirm the same identity from two sides, which keeps the correlation explicit in the code:

func (b *backend) Poll(out []takt.Completion) (int, error) {
    cqes := make([]uring.CQEView, len(out))
    n, err := b.ring.Wait(cqes)
    if iox.IsWouldBlock(err) {
        return 0, iox.ErrWouldBlock
    }
    if err != nil {
        return 0, err
    }

    m := 0
    for i := range n {
        cqe := &cqes[i]
        if !cqe.Extended() {
            continue
        }
        ext := cqe.ExtSQE()
        tok := takt.Token(uring.ViewCtx(ext).Vals1().Val1)

        if _, ok := b.routes[tok]; !ok {
            // Skip stale completions in this sketch.
            b.ring.PutExtSQE(ext)
            continue
        }
        delete(b.routes, tok)

        err := cqe.Err()
        done := writeDone{Err: err}
        if err == nil {
            done.N = int(cqe.Res)
        }

        out[m] = takt.Completion{
            Token: tok,
            Value: done,
            Err:   err,
        }
        m++
        b.ring.PutExtSQE(ext)
    }
    return m, nil
}

Then takt drives the computation:

b := &backend{
    ring:   ring,
    routes: make(map[takt.Token]writeOp),
}
loop := takt.NewLoop[*backend, int](b)

_, _, err := loop.SubmitExpr(program)
if err != nil {
    return err
}

for loop.Pending() > 0 {
    results, err := loop.Poll()
    if err != nil {
        return err
    }
    for _, r := range results {
        fmt.Println(r)
    }
}

The example is a sketch, so it intentionally keeps a few things simple. For instance, the poll buffer is allocated per call (make([]uring.CQEView, len(out))); production code on the zero-allocation path would reuse a fixed slice instead.

The exact wrapper code is secondary.

The important point is the shape: application code describes an operation, kont represents the suspension, uring exposes runtime completion facts, and takt maps completion identity back to computation progress.

Example: cove Around a Game Send

Now consider a game server sending one snapshot to one player. The send is not valid in every context. It may require that the match is running, the connection is still current, and the send budget is still available.

Without an explicit context layer, these checks often spread across handlers and side maps. With cove, the requirement can be represented near the suspended operation:

type gameCtx struct {
    Phase     string
    Connected bool
    ConnGen   uint64
    SendsLeft int
}

var canSend = cove.ExprAll(
    cove.ExprAtom(func(c gameCtx) bool {
        return c.Phase == "match-running"
    }),
    cove.ExprAtom(func(c gameCtx) bool {
        return c.Connected
    }),
    cove.ExprAtom(func(c gameCtx) bool {
        return c.SendsLeft > 0
    }),
)

The operation is still ordinary source-level code:

type sendDone struct {
    Bytes int
}

type sendSnapshot struct {
    kont.Phantom[sendDone]
    Conn   int
    Gen    uint64
    Packet []byte
}

The program can suspend on that operation:

before := currentGameCtx()

program := kont.ExprMap(
    kont.ExprPerform(sendSnapshot{
        Conn:   conn,
        Gen:    before.ConnGen,
        Packet: packet,
    }),
    func(done sendDone) bool {
        return done.Bytes == len(packet)
    },
)

Now the question is not only whether the local context looks valid, but also whether the later world is still related to the world where the send began.

For this example, the world relation says: the connection generation must stay the same, the match must still be running, the connection must stay connected, and the send budget may decrease.

sameSendWorld := cove.Preorder[gameCtx](
    func(before, after gameCtx) bool {
        return (before.Phase != "match-running" ||
            after.Phase == "match-running") &&
            before.ConnGen == after.ConnGen &&
            (!before.Connected || after.Connected) &&
            after.SendsLeft >= 0 &&
            after.SendsLeft <= before.SendsLeft
    },
)

Then the send can also be stepped with an explicit index. Here, 1 means this small verification path expects one external completion before it finishes.

_, step := cove.StepExprWithIndex(
    before,
    cove.StepIndex(1),
    program,
)

if !cove.NeedExpr(step.Ask(), canSend) {
    step.Discard()
    return errors.New("send is not valid in this context")
}

When the completion arrives, the runtime observes a later context. Before resuming the suspended computation, the code checks whether this later context is still an allowed successor world.

after := currentGameCtx()

if !cove.Extends(sameSendWorld, step.Ask(), after) {
    step.Discard()
    return errors.New("stale send completion")
}

sent, finished := step.ResumeTo(
    sameSendWorld,
    sendDone{Bytes: completionBytes},
    after,
)

ResumeTo consumes one step of fuel, and it also requires the next world to extend the previous one. Finally, a relation can check the completed finite trace:

sentOK := cove.Relate[gameCtx, bool](
    sameSendWorld,
    func(w gameCtx, left cove.StepIndex, sent bool) bool {
        return sent &&
            left == 0 &&
            w.Phase == "match-running" &&
            w.Connected &&
            w.SendsLeft >= 0
    },
)

if !cove.CheckCompletedRelation(finished, sent, sentOK) {
    return errors.New("send trace did not satisfy relation")
}

This is the practical meaning of related worlds and step-indexed checking here.

The world relation says which later context is still the same logical send path, the step index says how many external suspension-resume steps this check is allowed to consume, and the final relation says what must be true when the finite path completes.

The key case is a reconnect. After a reconnect, Connected may be true again, and a simple local check may pass. But the connection generation changed, so the world relation rejects the stale completion.

That is the value of making context movement explicit: the program does not just check the current context; it checks whether the current context is a valid successor of the earlier one. This catches a class of bug that a simple local predicate may miss.

Example: sess Protocol Shape

Here is a compact example of a game session. The protocol order stays close to the code.

func runGameSession() (string, string) {
    client := sess.SendThen(
        "Join",
        sess.RecvBind(func(match string) kont.Eff[string] {
            return sess.SelectLThen(
                sess.SendThen(
                    "Ready",
                    sess.RecvBind(func(tick string) kont.Eff[string] {
                        return sess.CloseDone(tick)
                    }),
                ),
            )
        }),
    )

    server := sess.RecvBind(func(join string) kont.Eff[string] {
        return sess.SendThen(
            "MatchFound",
            sess.OfferBranch(
                func() kont.Eff[string] {
                    return sess.RecvBind(func(ready string) kont.Eff[string] {
                        return sess.SendThen(
                            "Tick",
                            sess.CloseDone("started"),
                        )
                    })
                },
                func() kont.Eff[string] {
                    return sess.CloseDone("cancelled")
                },
            ),
        )
    })

    return sess.Run[string, string](client, server)
}

The visible protocol is:

client -> server: Join
server -> client: MatchFound
client chooses: ready
client -> server: Ready
server -> client: Tick
close

There is also a cancel branch. The point is not that this is the only way to write such logic; it is that protocol order, choice, continuation, and backpressure stay represented.

How a Completion Flows End to End

A useful way to see the stack is to trace one write through every layer, because the layers matter most when they connect.

The application builds an operation value and wraps it as a suspended computation. Nothing has run yet; the program is a value.

The loop steps that computation until it reaches the suspension, then hands the pending operation to the backend. The backend claims a submission slot, packs a token into the submission context, and submits to the kernel.

If the ring is full at submit time, that is ErrWouldBlock, not a failure; caller policy can try the submission again later.

Later, the kernel posts a completion. The backend reads the token back out of the completion, looks up the matching pending suspension, and classifies the result with iox.

If the result is success, the suspension resumes with the value and the continuation runs.

If it is a no-progress result, the pending suspension can be submitted again.

If it is an operation failure, the backend can resume the computation with a result value that carries the error.

If it is a live frontier, a one-shot loop rejects it, and a subscription route handles it instead.

Two facts remain explicit throughout this path: completion identity, which is the token, and outcome meaning, which is the iox case. That is the whole point of the upper layer: the runtime mechanics are absorbed, but the facts that decide correctness remain visible.

The Workflow on This I/O Stack: Lift, Reduce, Verify, Compile

The high-level programming workflow has four stages, and it is designed with AI agentic coding in mind:

  • LIFT. Raise real program behavior into a higher-level representation.
  • REDUCE. Reason on the smaller representation instead of the whole production program.
  • VERIFY. Check the lifted high-level shape first, then match it with source-backed runtime facts.
  • COMPILE. Translate the checked shape back into real Go programs above the stack.

Each layer preserves its own facts through this workflow:

  • For runtime packages, this means preserving facts such as file descriptor state, buffer identity, queue pressure, and completion identity.
  • For iox, this means preserving outcome meaning.
  • For kont, this means preserving suspension and continuation shape.
  • For cove, this means preserving context requirements and allowed world movement.
  • For takt, this means preserving token-to-computation routing.
  • For sess, this means preserving protocol order.

This workflow does not promise automatic correctness; it makes correctness obligations visible earlier.

Lift Reduce Verify Compile workflow diagram
Lift, Reduce, Verify, Compile workflow.

Part V: Performance, Measurement, and Limits

This final part explains why some paths stay close to the runtime facts, what becomes easier to measure, and where the limits are.

Why Some Paths Stay Close to the Runtime

A natural question is how this relates to standard high-level Go paths. Those paths are excellent defaults.

This more direct path exists because some performance work depends on facts that higher-level paths may intentionally hide, for example:

  • how many syscalls occur,
  • when the scheduler is involved,
  • whether memory is copied,
  • which buffer is submitted, selected, or registered,
  • which CQE belongs to which operation,
  • whether a completion is multishot,
  • and whether a no-progress state is retryable.

In ordinary application code, hiding these details is often a benefit. In low-latency systems code, those details may be exactly the facts developers need to work with.

This stack keeps those facts visible and then builds a representation layer above them. That is the balance I am exploring: close enough to the runtime to preserve performance facts, and structured enough to improve programmability.

It is useful to be concrete about where costs appear. A high-level read or write may involve internal buffering or a goroutine park-and-wakeup path.

For most software, that is a reasonable tradeoff, because the convenience is worth the cost. In a tail-latency budget measured in microseconds, the same copy or wakeup can become a visible cost you may need to measure and remove.

The stack does not remove those costs by itself. It makes them visible so developers can decide what to keep, measure, or remove.

What Becomes Easier to Measure

This stack is intended to make concrete runtime signals easier to observe:

  • For I/O outcome handling: clearer separation between no-progress, live frontier, completion, and failure.
  • For buffer handling: clearer buffer ownership, reuse timing, allocation pressure, and GC pressure.
  • For queue handling: bounded capacity, explicit backpressure, and visible no-input states.
  • For completion handling: clearer completion identity, route lifetime, and stale completion behavior.
  • For suspended computation: clearer pause and resume structure.
  • For context checking: clearer requirements across suspension points.
  • For protocol code: clearer order, branching, looping, and close behavior.

These are the kinds of facts developers can inspect, test, benchmark, and trace.

Performance Notes

A few concrete choices support the latency goal, and each one is explicit rather than hidden.

The continuation layer evaluates iteratively and pools transient frames. Deep computation does not grow the Go stack, and steady-state stepping reuses pooled frames instead of allocating a fresh chain each time. Performing an effect reuses pooled markers, and several entry paths use named identity continuations to avoid anonymous-closure allocation.

The Linux layer prefers a single-issuer ring with deferred task-run, which skips shared submission locking on the hot path, and it can skip success completions for write-like operations to cut completion traffic. Submission-queue fullness is reported as ErrWouldBlock rather than hidden behind a wait.

The queue and buffer layers are bounded by construction, so capacity pressure is a fact you can read, not a surprise discovered later. From there, developers can build small test programs around the Lift, Reduce, Verify, Compile workflow, then benchmark the stack in their own workload and environment.

Design Requirements

The stack follows a few design requirements:

  • First, preserve progress. If bytes were read or written, that progress must be handled before interpreting the control-plane state.
  • Second, preserve temporary no-progress. ErrWouldBlock should not become an ordinary failure. It means the operation cannot make progress now, and the caller or runtime policy should wait for readiness or completion before retrying.
  • Third, preserve live frontier. ErrMore should not become ordinary success. It means progress happened and the operation may produce more, so the live operation should remain visible.
  • Fourth, preserve identity. Completion identity, buffer identity, route identity, and context identity should remain visible where higher layers need them.
  • Fifth, separate mechanism from policy. uring should expose kernel facts; the service layer should decide retry, cancellation, backoff, and route policy.
  • Sixth, make suspension explicit. If a computation suspends, the operation and continuation should be visible.
  • Seventh, make context explicit. If the surrounding context may change, the allowed movement should be checkable.
  • Eighth, make protocol order explicit. Communication is more than bytes; it has structure: order, choice, loop, and close.

Limitations

The stack is still evolving. Several packages are low-level and require systems-programming care. The examples in this article are sketches: they show the programming shape, not complete production servers.

  • cove checks caller-supplied requirements and relations. It does not infer all correct requirements by itself.
  • sess represents protocol steps. It does not automatically prove protocol duality today.
  • takt represents completion-driven system steps. It is not a general scheduler.
  • uring exposes Linux completion facts. It does not own the whole runtime policy.
  • urex is a work-in-progress higher-layer framework built above these packages.

Future service and protocol layers should stay shallow: they should interpret the representation instead of hiding the runtime facts again.

Common Questions

A few scope questions are worth making explicit.

How does this relate to goroutines? Goroutines are a strong general solution, and they remain the right choice for most Go code. This stack is for cases where the suspension point itself needs to be visible as a value, so one completion can be routed to one computation frontier under explicit policy.

How does this relate to net and io? Go’s net and io packages are excellent defaults. The more direct path here exists for cases where submission, completion identity, buffer ownership, and protocol order need to remain visible in the source structure.

What does cove verify? cove checks the requirements and relations you write. It does not infer all requirements by itself, and it does not prove the whole program correct. Its role is to make correctness obligations explicit and checkable.

How does uring relate to other io_uring bindings? uring is a pure Go io_uring layer focused on exposing runtime facts clearly while leaving scheduling and policy above the package. Higher layers decide retry, cancellation, backoff, and routing.

How should performance be evaluated? The stack keeps performance facts visible so developers can measure them and act on the results. Real performance results should be tested in your own workload and environment.

Summary

The stack is built around one practical idea: do not hide the facts that decide latency and correctness.

  • At the bottom, runtime primitives keep systems facts explicit.
  • At the center, iox gives I/O observations precise meaning.
  • At the Linux completion layer, uring turns raw io_uring mechanics into programmatic concepts.
  • At the computation layer, kont represents suspension, cove represents context evidence, takt represents completion-driven system steps, and sess represents protocol steps.

The result is not just a faster I/O path. The deeper goal is better systems programmability: code that keeps performance facts visible while giving developers a clearer structure for correctness.

Glossary

A short reference for the terms used in this article.

  • Suspended computation: a paused computation represented as a value, with its pending operation and a one-shot way to continue.
  • Continuation: the rest of a computation, made explicit so it can be named, stored, and resumed.
  • Defunctionalization: replacing closures with a fixed set of data frames plus an evaluator, so continuation structure becomes inspectable data.
  • Trampoline: an evaluation loop that reduces one step at a time instead of growing the Go call stack.
  • Affine use: a value may be used at most once; for example, a suspension may be resumed once or discarded.
  • Effect and handler: an operation requested by a computation, and the code that interprets that operation.
  • Coeffect: required context made explicit; where an effect describes what a computation requests, a coeffect describes what context it depends on.
  • World and preorder: a runtime context plus a relation that says which later context is an allowed successor.
  • Step index: a finite budget for how many external steps a check may consume, keeping bounded reasoning well-founded.
  • Proactor: a model where a program submits work first, then handles the completion later.
  • Multishot: one submitted operation that may produce multiple completions before it ends.
  • Zero-copy notification: a completion that tells the program when the kernel is finished with a zero-copy buffer.
  • Provided buffer: a buffer selected by the kernel from an application-provided group, with the selected id reported in the completion.
  • Backpressure: the visible signal that a bounded downstream queue is full; an empty queue is a separate no-input would-block state.
  • SPSC, MPSC, SPMC, MPMC: queue shapes named by producer and consumer count.

References

The references below are grouped by the role they play in this article. They are background sources for readers who want to explore further.

Runtime I/O and Completion Model

  • Jens Axboe. 2019. Efficient IO with io_uring. Retrieved July 1, 2026 from https://kernel.dk/io_uring.pdf
  • liburing contributors. 2026. liburing wiki. Retrieved July 1, 2026 from https://github.com/axboe/liburing/wiki
  • Irfan Pyarali, Tim Harrison, Douglas C. Schmidt, and Thomas D. Jordan. 1997. Proactor: An Object Behavioral Pattern for Demultiplexing and Dispatching Handlers for Asynchronous Events. Pattern Languages of Program Design. https://www.dre.vanderbilt.edu/~schmidt/PDF/Proactor.pdf

Bounded Runtime Pressure and Lock-Free Queues

  • Leslie Lamport. 1977. Proving the Correctness of Multiprocess Programs. IEEE Transactions on Software Engineering SE-3, 2 (March 1977), 125-143. https://doi.org/10.1109/TSE.1977.229904
  • Adam Morrison and Yehuda Afek. 2013. Fast Concurrent Queues for x86 Processors. In Proceedings of the 18th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming (PPoPP 2013), 103-112.
  • Ruslan Nikolaev. 2019. A Scalable, Portable, and Memory-Efficient Lock-Free FIFO Queue. In 33rd International Symposium on Distributed Computing (DISC 2019), LIPIcs 146, Article 28, 1-16. https://arxiv.org/abs/1908.04511
  • Nikita Koval and Vitaly Aksenov. 2020. POSTER: Restricted Memory-Friendly Lock-Free Bounded Queues. In Proceedings of the 25th ACM SIGPLAN Symposium on Principles and Practice of Parallel Programming (PPoPP 2020), 433-434. DOI: 10.1145/3332466.3374508.
  • Ruslan Nikolaev and Binoy Ravindran. 2022. wCQ: A Fast Wait-Free Queue with Bounded Memory Usage. In Proceedings of the 34th ACM Symposium on Parallelism in Algorithms and Architectures (SPAA 2022), 307-319. https://arxiv.org/abs/2201.02179

Continuations, Defunctionalization, and Effects

  • John C. Reynolds. 1972. Definitional Interpreters for Higher-Order Programming Languages. In Proceedings of the ACM Annual Conference (ACM 1972), 717-740.
  • Olivier Danvy and Andrzej Filinski. 1990. Abstracting Control. In Proceedings of the 1990 ACM Conference on LISP and Functional Programming (LFP 1990), 151-160.
  • Andrzej Filinski. 1994. Representing Monads. In Proceedings of the 21st ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL 1994), 446-457.
  • Gordon D. Plotkin and John Power. 2003. Algebraic Operations and Generic Effects. Applied Categorical Structures 11, 1 (February 2003), 69-94. https://doi.org/10.1023/A:1023064908962
  • Gordon D. Plotkin and Matija Pretnar. 2009. Handlers of Algebraic Effects. In Proceedings of the 18th European Symposium on Programming (ESOP 2009), LNCS 5502, 80-94. https://doi.org/10.1007/978-3-642-00590-9_7
  • Gordon D. Plotkin and Matija Pretnar. 2013. Handling Algebraic Effects. Logical Methods in Computer Science 9, 4 (December 2013), Article 23, 36 pages. https://arxiv.org/abs/1312.1399

Context Evidence and World Movement

  • Saul A. Kripke. 1963. Semantical Considerations on Modal Logic. Acta Philosophica Fennica 16, 83-94.
  • Andrew W. Appel and David McAllester. 2001. An Indexed Model of Recursive Types for Foundational Proof-Carrying Code. ACM Transactions on Programming Languages and Systems 23, 5 (September 2001), 657-683. https://www.cs.princeton.edu/~appel/papers/indexed.pdf
  • Tarmo Uustalu and Varmo Vene. 2008. Comonadic Notions of Computation. Electronic Notes in Theoretical Computer Science 203, 5 (June 2008), 263-284. https://doi.org/10.1016/j.entcs.2008.05.029
  • Tomas Petricek, Dominic Orchard, and Alan Mycroft. 2014. Coeffects: A Calculus of Context-Dependent Computation. In Proceedings of the 19th ACM SIGPLAN International Conference on Functional Programming (ICFP 2014), 123-135. https://tomasp.net/academic/papers/structural/coeffects-icfp.pdf
  • Marco Gaboardi, Shin-ya Katsumata, Dominic Orchard, Flavien Breuvart, and Tarmo Uustalu. 2016. Combining Effects and Coeffects via Grading. In Proceedings of the 21st ACM SIGPLAN International Conference on Functional Programming (ICFP 2016), 476-489.
  • Wenhao Tang, Leo White, Stephen Dolan, Daniel Hillerstrom, Sam Lindley, and Anton Lorenzen. 2025. Modal Effect Types. Proceedings of the ACM on Programming Languages 9, OOPSLA1 (April 2025), Article 120, 1130-1157. https://arxiv.org/abs/2407.11816
  • Wenhao Tang and Sam Lindley. 2026. Rows and Capabilities as Modal Effects. Proceedings of the ACM on Programming Languages 10, POPL (January 2026), 923-950. https://arxiv.org/abs/2507.10301

Session Types and Protocol Structure

  • Kohei Honda. 1993. Types for Dyadic Interaction. In Proceedings of the 4th International Conference on Concurrency Theory (CONCUR 1993), LNCS 715, 509-523. https://doi.org/10.1007/3-540-57208-2_35
  • Kohei Honda, Vasco T. Vasconcelos, and Makoto Kubo. 1998. Language Primitives and Type Discipline for Structured Communication-Based Programming. In Proceedings of the 7th European Symposium on Programming (ESOP 1998), LNCS 1381, 122-138. https://doi.org/10.1007/BFb0053567
  • Philip Wadler. 2014. Propositions as Sessions. Journal of Functional Programming 24, 2-3 (2014), 384-418. https://doi.org/10.1017/S095679681400001X
  • Dominic A. Orchard and Nobuko Yoshida. 2016. Effects as Sessions, Sessions as Effects. In Proceedings of the 43rd ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages (POPL 2016), 568-581.
  • Kohei Honda, Nobuko Yoshida, and Marco Carbone. 2016. Multiparty Asynchronous Session Types. Journal of the ACM 63, 1 (March 2016), Article 9, 1-67. https://www.doc.ic.ac.uk/~yoshida/multiparty/multiparty.pdf

Available Packages

All listed packages are released under the MIT License: https://code.hybscloud.com/

Appendix: Package Quick Reference

This is a quick map from package to the names used above. It is a reading aid, not a full API list; the source remains the authority.

  • iox: ErrWouldBlock, ErrMore, Classify, IsWouldBlock, Backoff.
  • uring: Uring, Options, Start, Stop, SQEContext, ExtSQE, ScopedExtSQE, CQEView, BundleIterator, MultishotSubscription, and ZCTracker.
  • kont: Cont, Eff, Expr, Perform, ExprPerform, Map, ExprMap, Bind, Then, Reify, Reflect, Step, StepExpr, Suspension, Resume, TryResume, Discard, Phantom, Either.
  • cove: View, Cmd, Req, ReqExpr, Rule, Checked, Guarded, SuspensionView, StepExprWith, StepExprWithIndex, Preorder, Extends, Relate, ResumeTo, StepIndex.
  • takt: Token, Completion, Backend, NewLoop, SubmitExpr, Poll, Pending, RouteID, Subscription, and SubscriptionLoop.
  • sess: Send, Recv, SelectL, SelectR, Offer, Close, SendThen, RecvBind, SelectLThen, OfferBranch, CloseDone, Run, Loop, and ExprLoop.

Closing

For me, the central problem is not only making I/O faster. It is making high-performance I/O more programmable.

Developers building real-time communication systems need both lower latency and clearer correctness structure. That means the source code should expose important runtime facts before they disappear into implicit runtime behavior.

That is the purpose of this stack: preserve runtime facts, name I/O outcomes, represent suspended computation, carry context evidence, route completions as system steps, and keep protocol order visible.

Fast I/O is valuable. Fast I/O with clear program structure is easier to build, review, test, and evolve.

Thanks for reading.

Robin He, July 2026