Legal Logic Is Downstream from Legal Flow

· 12 min read

There’s a well-known mantra in programming language design:

Types are how you make illegal states unrepresentable.

It’s a good mantra because it points at something fundamental: logic only works if the state it operates on is valid. If your program can enter states you didn’t account for, then no amount of “correct” logic will save you.

Or more bluntly:

If you ask humans to maintain invariants instead of the compiler, you are asking humans to be compilers — and humans are bad at that.

From this follows a simple but important observation:

Legal logic is downstream from legal state.

Most modern languages stop here. And for state, that’s mostly fine.

But state is only half the story.


The missing dimension: time

Real programs are not just about what exists, but about what happens next.

Order matters. Resumption points matter. “What is allowed to happen now?” matters just as much as “what values exist now?”

Yet in most languages, control flow is implicit.

We rely on conventions, discipline, comments, and tests to ensure that:

  • events arrive in the right order
  • callbacks resume in the right context
  • required steps aren’t skipped
  • handling doesn’t happen twice
  • “impossible” paths remain impossible

When this breaks, the bugs are familiar: race conditions, invalid re-entry, double-handling, out-of-order effects.

These are not state bugs. They are flow bugs.

And they exist because illegal paths are representable.


Why typestate and phantom types aren’t enough

At this point, a reasonable objection appears:

“But we already have typestate, phantom types, linearity, resource tracking…”

Yes — and those are valuable tools.

But notice what they actually encode.

Typestate and phantom types:

  • attach state information to values
  • restrict which operations are legal on that value
  • answer: “What can I do with this thing now?”

They prevent illegal uses.

They do not encode:

  • where execution may legally resume
  • which paths through the program are allowed
  • which temporal transitions are valid

Control flow remains implicit. The continuation remains untyped. The edges between states remain informal.

This distinction matters.


Event continuations: typing the path itself

An event continuation makes the continuation explicit.

Not as a convention. Not as documentation. Not as discipline.

As a typed artifact.

When a continuation is typed:

  • only legal resumption points exist
  • illegal jumps are unrepresentable
  • ordering constraints become structural
  • “what can happen next” is enforced mechanically

This is the key insight:

Event continuations do for control flow what types do for state.

Or more compactly:

Types constrain what can exist. Event continuations constrain what can happen.

Once flow is explicit and typed, entire classes of bugs disappear — not because programmers became more careful, but because the compiler refuses to express invalid paths.


Seeing it in practice: flow without phantom types

Before adding state machines, let’s see how Koru enforces legal flow through shape alone.

Consider an event that can succeed or fail:

~event fetch { url: []const u8 }
| ok []const u8
| error []const u8

Any flow that invokes fetch must handle both branches. Not by convention — structurally:

// ✓ Valid: all branches handled
~fetch(url: "https://api.example.com")
| ok data |> process(data)
| error msg |> log(msg)
// ✗ Compile error: missing required branch 'error'
~fetch(url: "https://api.example.com")
| ok data |> process(data)
// Where does 'error' go? Nowhere. Illegal path.

This is the shape checker at work. No phantom types, no state machines — just the requirement that every declared branch must have a continuation. The illegal path (ignoring errors) is unrepresentable.

Compare to conventional code:

data, err := fetch(url)
if err != nil {
    // Hope the developer remembers this
}
process(data)  // Runs even if err != nil

The Go code represents the illegal path. The developer must choose not to take it. In Koru, the path doesn’t exist.


Adding state: phantom types encode the machine

Now layer in phantom types. These don’t change flow structure — they encode what state values are in as they traverse the flow.

// File operations with phantom state obligations
~event open { path: []const u8 }
| opened *File[opened!]   // [opened!] = obligation created

~event close { file: *File[!opened] }  // [!opened] = obligation consumed
| closed

~event read { file: *File[opened] }    // [opened] = must be in this state
| data []const u8

The ! suffix means “this creates an obligation you must fulfill.” The ! prefix means “this fulfills an obligation.”

Now watch what happens:

// ✓ Valid: obligation created and consumed
~open(path: "config.txt")
| opened file |> read(file)
    | data content |> close(file)
        | closed |> process(content)
// ✗ Compile error: unclosed obligation [opened!]
~open(path: "config.txt")
| opened file |> read(file)
    | data content |> process(content)
    // file has [opened!] — where's close()?

The state machine is not in your head. It’s not threaded through values manually. It’s in the flow graph itself. The compiler sees that opened! was produced but never consumed, and refuses to compile.

This is the key difference from traditional typestate:

// Rust typestate: YOU carry the token
let file: File<Open> = open("config.txt")?;
let file: File<Open> = read(&file)?;  // Still Open
let _: File<Closed> = close(file)?;   // Now Closed
// You must thread 'file' correctly through every call
// Koru: the FLOW carries the state
~open(path: "config.txt")
| opened file |> read(file)
    | data content |> close(file)
        | closed |> _
// State transitions happen BY traversing legal paths

In Rust, you carry the state. In Koru, time carries the state.


Phantom types in Koru: typestate as a consequence of flow

Koru does use phantom types, and at first glance this can look similar to typestate patterns in languages like Rust or ATS.

But the role they play is different.

In traditional typestate systems:

  • the phantom type is the state machine
  • transitions are modeled by consuming one value and producing another
  • correctness depends on threading the token through code correctly

The flow remains implicit.

In Koru, phantom types are opaque annotations, not the primary sequencing mechanism.

The state machine does not live inside the value. It lives in the event continuation.

A value may:

  • enter an event continuation in one phantom state
  • traverse a typed flow that enforces ordering and legality
  • emerge in a different phantom state — mechanically, not conventionally

The transition is not encoded by “calling the right function.” It is encoded by passing through a legal path.

Put differently:

Typestate encodes “what this thing is.” Event continuations encode “what just happened.”

Phantom types in Koru therefore reflect flow rather than simulate it. They describe the result of a legal path, not the burden of managing one.

Or in one line:

In Rust, typestate is carried by values. In Koru, state transitions are carried by time.


Flow as a first-class concept

Traditional control flow hides time inside syntax.

Async systems hide it even deeper.

But time is a dimension of program correctness. Treating it as secondary is a mistake.

When flow is implicit:

  • humans must remember invariants
  • reviewers must reconstruct intent
  • tests must cover combinatorial paths

When flow is explicit:

  • legality is structural
  • violations are unexpressible
  • logic becomes simpler, not more complex

Which brings us back to the earlier principle — now completed:

Legal logic is downstream from legal state and legal flow.


When flow is known, resource management stops being a problem

Once control flow is explicit and typed, a surprising thing happens: a whole class of downstream problems collapse.

If the compiler knows all legal paths and all legal termination points, then resource lifetimes are no longer something to be managed — they are something to be derived. Allocation and release become properties of the flow graph, not conventions layered on top of it.

This is why event continuations make general resource disposal possible without a garbage collector, without defer, and without ad-hoc dispose patterns. Resources are acquired along a legal path and are necessarily released when that path ends. There is no forgotten cleanup, because there is no representable path where cleanup is skipped.

In other words:

When flow is explicit, resource safety is no longer a discipline — it is a structural consequence.

That’s not a special case or a clever trick. It’s what falls out naturally when the compiler knows where execution can go, and just as importantly, where it cannot.

And crucially: this safety is zero-cost.

Event continuations are purely a compile-time concept. They exist in the source, guide the compiler’s analysis, enforce exhaustiveness and obligation tracking — and then they vanish. Phantom types are erased completely. The generated code is direct, linear execution: function calls, branches, returns. No runtime protocol checking. No garbage collector. No hidden dispatch. No wrapper objects.

The flow graph that guarantees correctness does not exist at runtime. Only its consequence does — correct code that runs at full speed.

This is the difference between “safe” and “safe and fast.” Most safety mechanisms trade performance for guarantees. Event continuations trade compile time for guarantees. The CPU never pays for the abstraction.


Why Koru doesn’t reinvent types

Koru does not attempt to replace or outdo the host language’s type system.

That battle is already well fought.

Instead, Koru focuses on the missing piece: making control flow non-optional.

In practice, this creates a clean division of responsibility:

  • The host language guarantees legal state
  • Koru guarantees legal flow

In Zig’s case, this pairing works particularly well: explicit semantics, no hidden control flow, and a type system that does exactly what it says — no more, no less.

Koru builds on that foundation instead of competing with it.


Closing thought

We’ve learned — slowly — that asking humans to enforce state invariants is a losing game.

We’re still asking them to enforce flow invariants.

Event continuations are not a convenience feature. They are a correctness feature.

Stop asking humans to be compilers — for control flow.