Keyboard shortcuts

Press โ† or โ†’ to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Eventuals

Eventuals are a V3 processor feature and are std-only. They use operating- system threads; FOL does not use Rust async/await, futures, Tokio, continuations, or colored routines.

The package therefore selects fol_model = "memo" for the artifact and declares the bundled internal standard dependency. That declaration enables the processor API; it is not what makes the executable runnable.

The chosen pipe surface is:

var pending = calculate() | async;
var value = pending | await;

| async starts the call on an OS thread and produces a one-shot eventual. | await blocks the current OS thread until that computation finishes. A local binding may name the eventual with its lexical lifetime elided (V3_MEM ยง8.1): evt[T] for an infallible call and evt[T / E] for a recoverable one, e.g. var work: evt[int] = compute(7) | async. The public lifetime-carrying evt[L, T] spelling is also a namable type, naming the region the eventual belongs to. An eventual that crosses a routine signature must spell that lifetime on both sides: a parameter or return type declares L: lif and writes evt[L, T], and the lifetime name must resolve to a declared lifetime parameter. The elided evt[T] form is local-declaration shorthand only.

This signature rule is what makes L enforceable without a region solver. Handles cannot be embedded in aggregates or wrappers, sent through channels, stored at module level, captured by routine values, or enter detached tasks โ€” so the only way a handle travels is through signatures and local moves. Requiring the evt[L, T] spelling wherever a signature carries one means a handle provably cannot outlive the parent scope L names.

The call to the left of | async must resolve directly to a named routine declaration. Both calculate() and a qualified call such as workers::calculate() are supported; qualification selects a declaration and does not introduce runtime dispatch. Stored routine values, stored anonymous routines, and routine parameters are indirect calls and are not async task targets in V3. Receiver-method call syntax is also excluded from the task target surface; call a free named routine directly instead.

Nested routine bodies cannot implicitly capture outer locals. Pass ordinary outer values through declared parameters instead of relying on a hidden closure environment. An outer eventual cannot use that workaround because generic crossings are forbidden; await and handle it in the outer routine.

Eventual bindings are move-only. A plain binding or assignment transfers the eventual and makes the source binding unavailable. An eventual may be consumed at most once: | await consumes the current binding, so a second await is a use-after-consume error. An infallible eventual need not be awaited and is joined at process exit; a recoverable eventual has the stronger must-handle rule below. V3 does not embed eventuals in composite values or pass them through generic parameters, because generic bodies do not yet carry a move-only contract. Await the value before crossing either boundary.

Arguments sent into | async obey the same thread boundary as [>]: borrowed values, ptr[shared, T], and unresolved generic parameters cannot cross it. Omitted defaults are arguments too and are checked against that boundary before the task starts. A generic call remains allowed when inference produces only concrete thread-safe parameter types.

Thread-safe move-only results can cross back through an eventual. The producer transfers the result into the eventual and | await transfers it to the awaiting routine; neither step clones it. Unique pointers are one shipped example of this rule. A result containing ptr[shared, T] is still rejected because its Rc representation cannot cross the OS-thread boundary.

Error behavior stays identical to the synchronous call. An infallible call awaits to T. A routine declared as T / E remains recoverable after await and must be handled with the existing check(...) or || surfaces. Async and await do not introduce a second error channel. A recoverable eventual is a must-handle value: its final owner must await it and immediately handle the result. Moving it to another binding moves that obligation. Lexical fallthrough and any break, return, or report that exits a scope with a live obligation are rejected. At a branch join, all continuing branches must leave compatible state: they may all preserve the same owner for handling after the join, transfer consistently, or discharge the obligation. Consuming or transferring on only some paths is rejected. Dropping it as a statement, overwriting it while live, or discarding the awaited result is also rejected.

| await is not allowed inside edf, and an existing eventual binding cannot be accessed there. Error-only deferred cleanup does not run on normal exits, so it cannot be used to discharge eventual ownership. This restriction remains in force through nested blocks, including a dfr declared inside the edf. Await or transfer the value in ordinary control flow instead.

Program exit joins outstanding async work just as it joins bare [>] tasks. This includes an infallible eventual that was created and then never awaited: it is not detached merely because no source-level binding consumes it. Joining is not error handling, so a recoverable eventual cannot use that path to discard its error. Cancellation, worker pools, and runtime scheduling controls are not part of V3.

An eventual is a one-shot task result, not a resumable generator or coroutine. V3 does not execute generator yield; that statement remains recognized by the parser but is rejected by semantic analysis and lowering. async also does not color the called routine or change its declaration.

The eventual slice is implemented end to end. Its exact positive (including the namable evt[T] local), tier-failure, and must-handle failure example set is maintained in the canonical shipped processor inventory.