Async Verbs¶
Prove provides structured concurrency via cooperative coroutines. Async verbs form a third family alongside pure and IO verbs.
| Verb | Purpose | Compiler enforces |
|---|---|---|
detached | Spawn and move on — fire-and-forget | No return type. Body runs concurrently; caller does not wait |
attached | Spawn and await — caller blocks until result is ready | Must declare a return type. May call blocking IO (runs in its own coroutine stack) |
listens | Cooperative loop — processes items until Exit | from block must be a single implicit match with an Exit arm. No return type |
Key rules: - listens bodies must not call blocking inputs/outputs functions directly — they run cooperatively and blocking would stall the yield cycle - listens arms can call attached functions via & to perform IO safely in a child coroutine - detached and attached may call blocking IO freely since they have their own coroutine stacks - Concurrency is cooperative — no threads, no data races - Runtime backed by prove_coro stackful coroutines (ucontext_t on POSIX, sequential fallback on Windows)
The & Marker¶
& marks an async call at the call site. It mirrors ! for failable calls:
| Marker | Meaning | Example |
|---|---|---|
! | can fail — propagate error | result = parse(input)! |
& | async invocation — dispatch to coroutine | data = fetch(url)& |
The verb (detached, attached, listens) declares intent at the function level. & only appears at call sites inside async bodies where work is dispatched to another async function.
detached — Fire and Forget¶
Spawns a coroutine and returns immediately. The caller does not wait for completion. Cannot declare a return type (E374). May call blocking IO freely since it runs independently.
/// Log an event — fire and forget, caller moves on immediately.
detached log(message String)
from
console(event.message)
attached — Spawn and Await¶
Spawns a coroutine and blocks the caller until the result is ready. Must declare a return type (E370). May call blocking IO (inputs/outputs) since it runs in its own coroutine stack. When an IO-bearing attached function is called via &, it must be from a listens or another attached body (E398).
/// Read a file — attached does the IO in its own coroutine.
attached load(path String) String
from
file(path)!
/// Fetch and parse data — caller waits for the result.
attached fetch(url String) String
from
load(url)&
listens — Event Dispatcher¶
The listens verb declares an event dispatcher that receives typed events from registered attached worker coroutines and dispatches them to match arms.
Signature pattern:
listens name(workers List<Attached>)
event_type AlgebraicType
from
VariantA => ...
VariantB(data) => ...
Exit => Unit
Key rules:
- First parameter must be
List<Attached>— the registered worker functions event_typeannotation declares the algebraic type for dispatch (required)- Each registered attached function's return type must be a variant of the
event_type - Match arms exhaust the
event_typevariants - One arm must match
Exit(terminates the dispatcher) - Cannot declare a return type (E374)
- Cannot call blocking IO directly (E371) — use
&in match arms - Attached functions in the worker list can have arguments:
[worker(arg)]
The from block uses implicit match — bare arms dispatch on events received from the internal event queue. Workers are spawned as coroutines and send events back to the dispatcher. When the Exit variant is matched, the loop terminates.
Full Example — Event Processor¶
module EventProcessor
narrative: """Process events using all three async verbs."""
System outputs console
Log detached debug
type Event is Data(payload Integer)
| Exit
/// Fire and forget — log without blocking.
detached fire(msg String)
from
console(msg)
/// Produce data events for the dispatcher.
attached double(x Integer) Data<Integer>
from
Data(x * 2)
/// Event dispatcher — receives events from workers.
listens handler(workers List<Attached>)
event_type Event
from
Data(payload) =>
debug(f"got: {string(payload)}")&
Exit()
Exit => Unit
main()
from
fire("hello from detached")&
handler([double(2)])&
Streams — Blocking IO Loop¶
The streams verb declares a blocking loop over an IO context. It runs until an Exit arm is matched, an error propagates via !, or the IO source is exhausted (e.g. stdin EOF). It is the synchronous counterpart to listens in the async family.
| Pattern | IO | Async |
|---|---|---|
| Push, move on | outputs | detached |
| Pull, await | inputs | attached |
| Loop until exit | streams (blocking, parameter-based) | listens (event dispatcher, queue-based) |
How it works:
The parameter carries the loop context — a value that holds whatever the loop needs each iteration (a file handle, a socket, a prompt string). The match arms execute IO using the context on every iteration. The Exit arm terminates the loop; all other arms loop back.
streams read_and_write(ctx Context)
from
Exit => ctx // terminates loop
Active(…) => // IO arm — runs each iteration
data = read_from(ctx.source) // blocking read using context
write_to(ctx.dest, data) // blocking write
Key rules:
- The
fromblock must be a single implicit match with anExitarm - The match subject is the first parameter type
streamsis a blocking IO verb — it cannot be called fromlistensbodiesstreamsbodies may use&to fire-and-forgetdetachedcalls (e.g. logging)- On EOF from an
inputsread, the loop exits automatically
REPL Example — Read stdin line by line¶
System outputs console inputs console
type Session is Active(prompt String)
| Exit
/// Echo stdin lines with a prompt. Exits on EOF.
streams repl(session Session)
from
Exit => session
Active(prompt) =>
console(prompt) // write: print prompt
line as String = console() // read: next stdin chunk
console(f"< {line}") // write: echo it
main()
from
repl(Active("> "))
Each iteration the Active(prompt) arm performs a blocking console() read. When stdin is exhausted the loop exits cleanly.
Network Server Example — Accept connections in a loop¶
// conn is the loop context; Accept(listener) arm calls accept() each iteration
streams serve(conn Connection)!
from
Exit => conn
Accept(listener) =>
client as Socket = accept(listener)!
data as ByteArray = message(client, 1024)!
message(client, data)!
socket(client)
Safety Rules¶
| Code | Trigger | Severity |
|---|---|---|
| E370 | attached declared without a return type | Error |
| E371 | Blocking inputs/outputs/streams call in listens body | Error |
| E372 | attached or listens called without & | Error |
| E374 | detached or listens declared with a return type | Error |
| E398 | IO-bearing attached called outside listens/attached body | Error |
| E401 | event_type must reference an algebraic type | Error |
| E402 | listens first parameter must be List<Attached> | Error |
| E403 | Registered function is not an attached verb | Error |
| E404 | Attached return type doesn't match event variant | Error |
| E405 | event_type on non-listens verb | Error |
| E406 | listens missing event_type annotation | Error |
| E151 | listens body missing an Exit arm | Error |
| I375 | & on a non-async callee | Info |
| I377 | attached& outside listens — runs synchronously | Info |
| I378 | detached called without & | Info |
| I601 | Function body contains todo | Info |