Asynchronous effects — the calculus of the engine protocol
Ahman, Pretnar, Asynchronous Effects, POPL 2021; and its sequel, Higher-Order Asynchronous Effects, LMCS 2024. The language is Æff.
Æff decouples an algebraic operation’s call from its answer: a computation
issues outgoing signals and receives incoming interrupts, each one-way, and
recovers a synchronous call as a signal, a promise of the reply, and an
await. ral’s engine protocol (engine-protocol) is
this picture with the engine and its host as the two processes — except that ral
keeps the synchronous call primitive, gives interrupts only to the runtime, and
adds a scope at which signals wait.
Correspondences
- Signals are
Surface.↑op(V, M)issuesopand continues asMwithout waiting;surfacehands the host a value and continues. - Interrupts are
Control.↓op(V, M)arrives at any point ofM, from outside it;Interrupt,Cancel,TerminateandAbortarrive at any point of a run, out of band. - Promises are handles. Æff’s
promise … as pyields a promisepandawait p until ⟨x⟩ in Mblocks on it;spawnyields a handle andawaitblocks on it. The sequel adds dynamic process creation, which isspawnproper. - The synchronous call is derived. In Æff a call that wants an answer is a
signal out, an interrupt handler for the reply, and an
await.enquireis exactly that sequence, built in. - Parallel composition is the carrier. Æff’s
P ∥ Qturns one process’s signals into the other’s interrupts; the carrier does the same for engine and host, in one address space or across a socket.
Divergences
- No program-level interrupt handlers. In Æff a computation installs
handlers for the interrupts it expects. In ral only the runtime handles
Control(Scopes::apply); the nearest a program comes is aguardcleanup, which runs on a stop but cannot resume the computation. - The synchronous call stays primitive. Decomposed, an enquiry’s reply could
arrive after its dispatch’s
Report; fused, it cannot, which is what keeps a transcript ordered. A worker, which outlives itsReport, is given no desk. - Signals wait for their scope. An Æff signal propagates at once. A detached
worker’s
surfaceis handled at session scope and rendered at the host’s next boundary (surface-delivers-itself), so the session is a handler Æff does not have: a buffer emptied at a boundary. - Point to point, not broadcast. Æff broadcasts a signal to every parallel process. An engine speaks only to its host; a host fans out by its own machinery (agents).
- First-order payloads, not modal ones. The sequel types code that may cross
between processes with a modality. ral admits only first-order
FOValues at an open payload position, and carries a forked session as opaque sealed bytes.
The same shape elsewhere
- A Unix process and its kernel. A write is a signal, a blocking system call a synchronous operation, a signal an interrupt — and the kernel makes a pending signal visible only on the return to user mode, as the session scope renders only at a boundary.
- JSON-RPC and LSP. Requests with ids and matched responses, notifications
without, and cancellation as a notification (
$/cancelRequest). The engine protocol’s comparison table sets it beside them.
What ral could borrow
- Interrupt handlers for long-lived workers. A long-lived worker that must react to its host mid-flight now has no way to hear it; an Æff-style handler is the principled shape, should that need arrive.
- The session scope as a handler in the calculus. Æff gives signals and interrupts an operational semantics; a boundary-flushed handler would give the same to ral’s deferred channel.