Execution Hosts

PFCL language reference · composure.systems

A host is a program that reads the command records a PFCL function returns and performs the actions they name — see /effects-as-data for the contract itself. This page specifies the shape hosts take for a stateful, standalone PFCL program: a ladder of increasingly capable drivers, from a program that runs once and exits to a fully scheduled event loop. A program picks the lowest rung that fits its actual needs; the pure function it drives is identical at every rung. The ladder itself is the stable part of this specification; which rungs have a reference implementation at any given time is not — check the standalone repo for current status rather than treating this page as a build log.

1.The invariant beneath every rung

A stateful standalone computation is a single pure catalog entry of the shape:

step : (S, I) -> (S, List<Command>)

step takes the current state and one input, and returns the next state plus a list of command records. It is pure, total, hash-addressed, and identical regardless of which rung drives it. It performs nothing — a returned command is a record, not an action taken.

The host is the effectful half, living entirely on the far side of the command boundary, because reading stdin, writing stdout, or blocking on a socket cannot be expressed by a pure function without breaking the purity the language exists to guarantee. "Adapter" and "event loop" are not two categories — they are points on one spectrum, distinguished only by how inputs arrive and how much scheduling sits between arrival and the call to step.

2.The rung ladder

2.1Rung 0 — one-shot batch

Run the function once, perform the returned commands, exit. No carried state, no loop. Shape: main : List<String> -> List<Command>. One input (argv), no state. Use: a pure transform reading argv and printing; a batch writing a computed result via a single data.write command.

2.2Rung 1 — fold over a fixed input

A known, finite list of inputs is folded through step, threading state and accumulating commands, then exit. Non-interactive, fully deterministic, no blocking. Because inputs are ordered and step is pure, the run is a deterministic fold — bit-for-bit reproducible from (initial, inputs). Use: a batch record processor; a scripted regression-test playthrough; replaying an event log.

2.3Rung 2 — interactive stdin loop

Block on a line of stdin, feed it as the next input, perform commands, repeat until EOF or an exit command. One blocking source, no scheduling. Use: an interactive human-facing program; a line-oriented REPL; any prompt/response tool.

2.4Rung 3 — single source with completions

Rung 2 plus dependent effects: an effect's result re-enters as a later input. step emits a request record carrying a correlation tag and returns; the host performs the effect and feeds the completion back as the next input on a later turn. Adds over rung 2: effects can depend on earlier effects' results. Cost over rung 2: one match arm in the host switch that turns a completion into the next input — not a new architecture.

2.5Rung 4 — multiple sources, cooperative-trivial

Select over more than one input source — stdin and a timer, stdin and a socket. The host polls, feeds whichever fires, threads state as before. It now has an event queue and multiple sources, so it resembles an event loop, but with no priorities, no fairness policy, and no step chunking, because every step is assumed cheap. Deliberately omits: priority bands, fairness, step-budget chunking, continuation across cores. Use: a simple TUI; a watch-and-react tool; a small poller.

2.6Rung 5 — the full event loop

Priorities, fairness across sources, step-budget chunking so a heavy step cannot stall the others, and (on capable hosts) continuation migration across cores. This is the scheduled event loop, consumed by a standalone program only when it genuinely needs it — not re-implemented by it. Use: a server; anything concurrent-with-heavy-work.

3.Where standalone programs actually sit

Rungs 0–3 cover the large majority of standalone programs — a CLI tool, a batch transformer, a REPL, an interactive program, a single-request utility. None of these is an event loop, and none needs one. Rung 4 covers simple multi-source tools without heavy steps. Rung 5 is specifically for the concurrent-many-things-with-heavy-work case — the server/platform case. Reaching for the smallest adapter that fits is therefore the correct default, not a workaround; the full loop is the exception, reached for only on crossing into rung 4–5 territory.

RungInputsSchedulingTypical program
0argvnonepure transform CLI
1fixed listnonebatch processor, replay
2stdin, blockingnoneREPL, interactive program
3stdin + effect completionsnonefile-reading CLI
4stdin + timer + socketnone (cheap steps)simple TUI, poller
5manypriorities, fairness, chunkingserver, platform

4.Why this is cheap: one step, many drivers

The pure step is unchanged across the whole ladder — identical whether driven by a fold (rung 1, a scripted test), an interactive loop (rung 2, a human), or the full scheduled loop (rung 5). The host is thin wiring around a fixed command switch; the hashed catalog entry is the artifact that matters. A new host therefore costs almost nothing to add — a small driver around the same switch — and buys a program that runs at exactly the complexity it needs, no more.

5.The rung 4 → 5 cliff

Rungs 0–4 share one property: no cooperative scheduling is needed, because either there is one input source, or there are several but every step is cheap. The cliff is step cost. The moment a step can be heavy enough to stall other sources while it runs, chunking becomes necessary — and chunking requires the entire rung-5 apparatus. There is no half-measure: a program either has cheap steps (any rung 0–4) or it needs the scheduler (rung 5). The question worth asking before climbing to rung 5 is whether the program has, in effect, become a server — in which case the full event loop is the built answer to consume, not a thing to reimplement.

6.Scope

This page specifies the driver ladder and the invariant beneath it. It does not specify the command vocabulary a given host implements — the particular set of command kinds (I/O, storage, network) any concrete host recognizes is a property of that host, not of this ladder. See /effects-as-data for the command-record contract itself.

References

  1. PFCL. Effects as data: the command-record contract between a function and any host. /effects-as-data.
  2. PFCL. Totality: the termination guarantee step must satisfy at every rung. /totality.