Chapter 26 — Errors, outcomes, progress, and recovery
This chapter is required except for the final section, “Rationale and reconsideration”. It defines the selected model for absence, materialized outcomes, live operation protocols, recovery policy, progress, cancellation, terminal cleanup, fatal termination, and C adapters.
# 26.1 Four closed categories
Wyst keeps four categories distinct. There is no implicit conversion among them.
core.collections.Option<T>stores presence (Some(T)) or explanation-free absence (None).core.collections.Result<T, E>stores a nominal outcome (Ok(T)orError(E)) for inspection, transport, combination, or later retry.- An
operationis a nominal, non-first-class synchronous callable protocol. Itsfailuretransition means that this invocation could not satisfy its declared success contract. Expected states advertised by an API—including EOF, readiness, cache miss, short count, parser alternative, and process or device completion status—remain success data. #fatal_trap(reason: u16) -> never effects(trap)enters the authenticated target trap path. Architectural faults, violated invariants, explicit fatal policy, and trusted-contract failures use traps. Operation handlers never catch or convert a trap.
Compile-time diagnostics are not values. Target-defined behavior and trusted boundary violations retain the taxonomy in Chapter 1. There is no ambient last-error or last-status value, universal pointer-sized failure type, exception unwinding, implicit optional lifting, dynamic handler search, hidden retry, hidden allocation, or automatic conversion between these categories.
# 26.2 Materialized sum types
An enum variant payload may be any compiler-proven fixed_layout_movable
value. Eligible values include ordinary fixed-layout structs, arrays, slices
and other fixed descriptors, nested enums, and concrete generic
instantiations. never, void, raw MaybeUninit<T> storage, atomic storage,
symbolically sized values, and values requiring recursive inline storage are
ineligible. Recursive values require explicit indirection.
For a concrete enum E, let D be the declared unsigned discriminator type.
For every effective payload field, compute its ordinary target layout. The
payload storage alignment is the maximum payload alignment (or one with no
payload); the payload storage size is the maximum payload size rounded up to
that alignment. The payload offset is align_up(sizeof(D), payload_align) and
the enum alignment is max(alignof(D), payload_align). The total size is
align_up(payload_offset + payload_size, enum_align). These values and every
variant's exact field types are semantic facts, not reconstructed from a
representative word type.
Construction writes the discriminator, zeroes every inactive payload byte, then writes the active payload. Padding and inactive bytes are therefore deterministic and raw observation never exposes stale prior-variant data. Only fields of the active variant may be projected. A move transfers the complete enum value and invalidates the source under the ordinary move rules. No payload is implicitly boxed, referenced, allocated, reference-counted, or copied. Equality exists only when the enum's exact active payload alternatives independently satisfy the equality capability; fixed layout and movability do not imply equality. Initialization, destruction, and raw observation use the same active-variant rule.
Generic enum payload parameters declare the closed
fixed_layout_movable bound. Concrete substitution removes an enum variant or
operation member only when its canonical payload is exactly never; the
compiler does not use a general inhabitance proof. Semantic interfaces, final
images, debug data, and ABI facts retain concrete size and alignment. Native
and AAPCS64 use their ordinary aggregate classification, including indirect
argument or result storage for large values.
# 26.3 Canonical stored outcomes
The authenticated core.collections source declares:
pub enum Option<T: fixed_layout_movable>: u8 {
None
Some(T)
}
pub enum Result<T: fixed_layout_movable, E: fixed_layout_movable>: u8 {
Ok(T)
Error(E)
}
The first-order helper families are option_is_some, option_is_none,
option_value_or, option_value_or_else, result_is_ok,
result_is_error, result_value_or, and result_value_or_else. *_value_or
is eager; *_value_or_else is lazy. Callback parameters carry an explicit
closed callable effect bound. These declarations are ordinary authenticated
generic declarations and receive no recognition by a local spelling.
Expression-valued exhaustive match is the extraction and transformation
mechanism. Its scrutinee evaluates exactly once. Every reachable arm has a
tail value of one exact common type; never is compatible with that type.
The ownership state at each join must be exact. All effective variants are
covered or a final explicit else is present. A partial-match opt-out is
illegal in an expression match. Handler-head and enum patterns remain shallow
and irrefutable; deeper inspection uses another exhaustive match.
expect_or_trap<T, E>(value, reason: u16) -> T effects(trap) returns Ok's
payload or calls #fatal_trap(reason). The reason is evaluated exactly once
and is explicit in typed IR. The ARM64 boundary preserves it in x0 before a
reserved canonical BRK #0xf001. The instruction does not create optimizer
undefined behavior. #[deny_effects(trap)] rejects the helper or intrinsic.
There is no force-unwrap punctuation or trust alias. Postfix ? never
applies to Result.
# 26.4 Operation declarations and invocation
An operation declaration has this canonical order:
operation read(dst: noescape @u8, capacity: u64, policy: fn(ReadProblem) -> ReadChoice effects(none)) {
success(ReadCompletion)
progress(ReadProgress) effects(none)
failure(ReadFailure)
cancelled(CancelledRead)
} effects(handler_invoke) {
// synchronous producer body
}
success is required. progress, failure, and cancelled are optional and
may occur only in that order. The members are a closed transition set, not a
second semantic effect system. An operation is a distinct compile-time kind:
it cannot be stored, returned, addressed, or converted to an ordinary function
pointer. Calling it begins execution immediately and creates no operation
object, coroutine frame, task, retained continuation, executor, exception
object, dynamic handler search, or mandatory runtime. An ordinary function
and a success-only operation remain different even when their Native ABI
shapes coincide.
report value, return value, fail value, and cancel value produce the
lexically enclosing member. A call is consumed by one fully braced with set:
return read(dst, capacity, decide) with {
progress(update) { observe(update) }
failure(problem) { translate(problem) }
cancelled(cause) { cancelled_value(cause) }
}
An omitted success arm is inserted as the identity arm before result typing
and ownership checking. Every effective non-success member is handled exactly
once. Terminal arms have one exact result type, with never compatible.
forward progress, forward failure, and forward cancelled require exact
canonical payload type, lifetime, mutability, and ownership compatibility
with the lexical operation. There is no forward all, whole-call forwarding,
implicit forwarding, or failure translation. Handlers use shallow,
irrefutable heads. No return, outer break, or outer continue crosses the
handler boundary.
# 26.5 Exact operation failure forwarding
Postfix ? accepts only a direct operation call whose effective set is exactly
success plus failure, inside an operation with an exactly matching failure
payload, lifetime, mutability, and ownership mode. It yields the success
payload and is identical to a with containing forward failure and implicit
success. The callee and arguments evaluate once, left to right. It introduces
no new effect beyond the call.
? rejects stored Result, progress, cancellation, any required lexical
recovery policy, a mismatched outer failure, progress/resume-only context,
non-direct calls, and public-signature inference. Adding a member to the
callee makes an existing site ill-typed. Child cleanup completes before the
outer failure is committed.
# 26.6 Progress, effects, and liveness
Progress is synchronous, serial, same-strand, unbuffered, notification-only,
and subject to backpressure. report does not return until the handler
returns. It supplies no fairness, wait-freedom, lock-freedom, scheduling,
independent forward-progress, or latency guarantee. A returning progress
handler produces void and resumes the producer exactly once. Terminal
handlers resume it zero times. Progress has no reply value.
Progress handlers are noescape, transitively resume-only, and zero-capture.
They may use their fresh report payload and module-visible declarations, or
operate on caller-owned storage explicitly carried by that payload. Implicit
capture or mutation of an outer local is rejected. The callback cannot be
retained and cannot fail, cancel, return from the producer, target an outer
loop, or call through a typed path that can perform such an escape. A borrowed
progress payload has a fresh report-scoped lifetime ending when the handler
returns and cannot escape.
handler_invoke is a target-neutral member of the single closed effect
catalog and effects(all). Every progress member declares one concrete closed
handler-effect ceiling; omission is illegal. The ceiling is part of protocol
identity. A lexical progress arm is independently inferred and must be a
subset of the ceiling. report charges handler_invoke plus the entire
declared ceiling even if lowering later removes dispatch. The actual arm
effects remain attributed to the function containing with; a separately
compiled producer is checked against the conservative ceiling.
If the ceiling contains execution_suspension, report creates the existing
typed strand-suspension boundary and all context-stability checks apply. No
boundary is invented when authenticated evidence excludes suspension. Denying
handler_invoke rejects reporting; denying an arm effect rejects that arm.
# 26.7 Recovery interaction
Wyst selects an explicitly passed typed recovery-decision capability. It is an
ordinary noescape function-pointer parameter with a visible closed effect
bound, for example fn(AllocationProblem) -> AllocationChoice effects(none).
The producer calls it synchronously, presents one nominal problem, receives
exactly one producer-defined nominal choice, and then explicitly retries,
substitutes, continues, fails, or cancels as its source declares. Unknown
effect bounds are conservative. Exact ordinary callable typing governs
forwarding; there is no separate recovery-forward syntax.
The capability and every caller context are non-retainable. It creates no
continuation and requires no allocation or dynamic handler search. It is not
progress and is never encoded as a reply-bearing report. Allocation recovery
uses AllocationProblem -> AllocationChoice; parser recovery uses
ParseProblem -> ParseChoice; device reconfiguration uses
DeviceProblem -> DeviceChoice. These comparisons exercise retry, substitute,
skip/continue, reconfigure, failure, and cancellation without introducing a
second handler mechanism. The Native ABI uses the ordinary noescape callable
parameter ABI. C uses an explicit callback plus void *context; the adapter
must invoke it synchronously and never retain either value.
# 26.8 Commitment, cancellation, partial completion, and cleanup
A cancellation request or deadline expiry is ordinary explicit input, not a
terminal outcome. The producer decides when to observe it. Terminal commitment
is the invocation's single linearization point: the first success, failure, or
cancellation label committed by the producer wins. A later request, deadline,
event, or cleanup cannot replace it. cancelled acknowledges acceptance of a
cause. An API promising acknowledgement of a specific request includes its
nominal identity or generation in the cancellation payload. Deadline expiry is
a request source unless the API explicitly classifies timeout as success data
or nominal failure. No request, deadline, or handler asynchronously unwinds a
producer.
Terminal lowering performs this order:
- evaluate the selected payload once;
- move it into caller-owned outcome storage;
- construct the exact terminal discriminator (the commitment point);
- disarm destruction of the moved source;
- execute registered
defer { ... }blocks in deterministic innermost-first, reverse-registration order; and - enter the selected terminal handler.
defer registers its block in the current lexical scope. Normal scope exit,
break, continue, ordinary return, and every operation terminal execute the
applicable registrations. A cleanup may contain cleanup-local loops and traps,
but cannot report, return, fail, cancel, goto, or target an outer
loop. A trap during cleanup remains a trap; it does not relabel the committed
outcome. There is no implicit destructor runtime. Explicit cleanup and the
ordinary move state are the current deterministic destruction mechanism.
A terminal payload may own completed output or borrow caller-owned storage that outlives cleanup. It may not borrow producer-local storage released by cleanup. For reads, writes, parsing, transfer, or provider work that completes a prefix before failure, caller-owned output remains caller-owned and the failure payload contains the exact committed extent and nominal cause. Producer-owned partial output instead moves into the failure or cancellation payload. Progress never establishes ownership or durable completion. Retry is explicit caller policy and begins from the committed extent without repeating already committed work.
# 26.9 ABI and C adapter profiles
Native operation lowering adds a hidden noescape progress callback parameter
only when progress is effective and returns one exact outcome enum. The
callback type is fn(P) -> void effects(ceiling). Invocation is synchronous.
The returned enum follows ordinary Native aggregate classification; large
outcomes use caller result storage. AAPCS64 operation adapters are ordinary
explicit wrappers and never redefine the native operation type.
Two C profiles are closed and explicit:
- status/out applies only to success-or-failure operations whose complete failure information has a declared C-compatible status mapping and no rich payload or partial extent. It returns the mapped status and accepts caller-provided success storage. If an extent must exist on both paths it is a separate, always-initialized output with a stated type.
- tagged/out returns a terminal tag and accepts correctly aligned caller-owned storage for rich success, failure, and cancellation payloads. Only the storage selected by the returned tag becomes initialized, except a separately declared always-initialized common extent.
Every generated declaration states which outputs initialize on each tag, ownership, lifetime, aliasing, alignment, cleanup, and partial-extent duties. Progress and recovery cross C only as explicit noescape callback-and-context pairs with authenticated effect bounds and synchronous lifetimes. A C process or device status is not automatically failure; the adapter applies the API's declared nominal mapping. There is no ambient status register or hidden TLS.
The canonical short-read tagged profile always initializes extent, leaves
the first extent bytes of caller-owned output valid, initializes exactly one
of success/failure payload storage, and preserves the nominal failure cause.
Bad alignment, overlapping selected payload storage, uninitialized required
common output, escaping callback/context, or an incomplete status mapping is a
compile-time adapter error.
# 26.10 Semantic records and validation
The materialized-sum record contains
{ nominalIdentity, tagType, size, alignment, payloadOffset, payloadSize, payloadAlignment, variants[] }; each variant is
{ nominalIdentity, tag, fields[] } and each field retains exact type,
offset, size, alignment, ownership, and movability.
The operation record contains { nominalIdentity, parameters[], transitions, effects, progressHandlerCeiling, recoveryParameters[], nativeAbi, cAdapters[] }. transitions contains ordered optional
records for success, progress, failure, and cancelled with exact payload type,
lifetime, mutability, ownership, and concrete layout. nativeAbi records the
hidden callback signature, exact outcome layout, argument/result
classification, and noescape facts. cAdapters records profile, status/tag
mapping, callback/context, output initialization matrix, alignment, aliasing,
cleanup, and common-output obligations.
The current in-memory semantic-interface consumer validates these records against checked type/effect/layout facts before IR construction. Typed IR and the final image preserve nominal outcome types, operation signatures, transition tags, effect ceilings, suspension provenance, and adapter facts. DWARF emits exact enum layouts and operation outcome/callback types. Explain reports render the same fields. A mismatch, missing member, reordered tag, weakened effect, incompatible layout, or unauthenticated adapter mapping is an error; consumers never reconstruct the record from names.
# 26.11 Rationale and reconsideration (nonnormative)
Result-only APIs make storage pleasant but cannot model synchronous progress, backpressure, or exact live failure forwarding without callback conventions. Exceptions and unwinding hide control transfer and cleanup costs, complicate freestanding targets, and erase the exact failure surface. Dynamic conditions and restarts and general algebraic effects or abilities provide expressive recovery but require handler search or continuation machinery that Wyst does not otherwise need. New evidence showing a bounded, statically representable continuation model with better whole-program costs could justify reopening that decision.
Explicit recovery capabilities were selected over lexical request/reply after applying both to allocation, parsing, and device reconfiguration. Capabilities reuse ordinary callable effects, ABI, ownership, noescape analysis, C callback mapping, and separate-compilation summaries. Lexical request/reply makes policy visually local but adds a second closed member set, forwarding rules, grammar, and continuation-like resumption validation. Evidence that capability plumbing dominates real APIs or prevents safe borrowing could justify reconsideration; no alternate spelling is reserved meanwhile.
Reply-bearing progress conflates observation with policy and weakens backpressure reasoning. Ambient status loses provenance and is unsafe under nested calls. Automatic unwrapping and force punctuation hide failure policy. Hidden allocation makes freestanding cost and failure recursive. One-word-only sum payloads force boxing or impoverished errors. These alternatives would be reconsidered only with measured program evidence that outweighs explicit control, exact ownership, and deterministic layout—not for syntax familiarity alone.