12 KiB
Dynamic Semantics - Effect Surfaces Decision
Status: Accepted Cycle: Initial effect-surfaces closure pass
1. Context
PBS v1 already commits to several effect and control surfaces in syntax and static semantics, but their runtime behavior must be closed before normative dynamic semantics can be written.
This decision record captures the first closed subset of that work:
optional,result<E>,!propagation,handleprocessing.
Allocation, retention, copy visibility, and heap-facing cost wording are delegated to the memory and heap decision track.
2. Decision
PBS v1 adopts the following baseline runtime rules for optional and result<E> surfaces:
optionalis a runtime presence/absence carrier with canonicalsome(payload)andnonestates.some(expr)evaluatesexpreagerly and exactly once before forming thesomecarrier.opt else fallbackevaluates the left operand exactly once and evaluatesfallbackonly when the left operand isnone.optionalis trap-free by itself; only subexpression evaluation may trap.result<E>is the runtime carrier for expected, modelable failure in function return flow.expr!evaluatesexprexactly once and performs immediate enclosing-function error propagation on failure.handle expr { ... }evaluatesexprexactly once and, on error, executes exactly one matching arm.- A
handlearm may execute user-defined logic. - A
handlearm must terminate with eitherok(payload)orerr(E2.case). ok(payload)in ahandlearm recovers locally and yields the payload as the value of thehandleexpression.err(E2.case)in ahandlearm performs immediate enclosing-function return with that error.handlesupports a short remap formE1.case -> E2.caseas sugar for a block that returnserr(E2.case).- Neither
!norhandleintercepts or converts traps. applyis the canonical universal call surface for PBS v1 across all callable categories.applychains parse right-associatively but preserve the already-closed left-to-right observable evaluation model.applydoes not introduce implicit composition ofoptionalorresultsurfaces.bind(context, fn_name)forms a nominal callback value by attaching a struct context to a top-level function target.bindevaluates its context expression exactly once, captures the same runtime context identity without copying it, and injects that context as the first argument when the callback is invoked.bindis not a general closure mechanism and is trap-free at the language level.
3. optional
3.1 Runtime model
optional has exactly two runtime states:
some(payload)none
none is the canonical absence of payload.
3.2 Construction
some(expr):
- evaluates
expreagerly, - evaluates it exactly once,
- then forms the
some(payload)carrier from the produced payload.
3.3 Extraction
opt else fallback:
- evaluates
optexactly once, - if the result is
some(payload), yields the extracted payload and does not evaluatefallback, - if the result is
none, evaluatesfallbackand yields that value.
else is extraction with fallback, not error handling.
3.4 Trap behavior
optional itself does not trap.
Any trap associated with some(expr) or opt else fallback arises only from:
- evaluating
expr, - evaluating the left operand,
- or evaluating the fallback when it is needed.
4. result<E>
4.1 Runtime role
result<E> is the runtime carrier for success-or-modeled-failure at function boundaries and in expressions whose static type is result<E> P.
In v1, ok(...) and err(...) are special result-flow forms.
They are not general-purpose first-class userland constructors for arbitrary data modeling.
They are valid in:
- function return flow for
result<E>, - and
handlearms, where they control recovery or propagation.
4.2 ! propagation
expr!:
- evaluates
exprexactly once, - if
expryields success, yields the extracted success payload as the value of the expression, - if
expryields error, performs immediate enclosing-function return with the sameerr(...).
! does not:
- remap the error,
- intercept traps,
- or continue ordinary evaluation after the propagated error path is chosen.
4.3 handle
handle expr { ... }:
- evaluates
exprexactly once, - if
expryields success, yields the extracted success payload directly, - if
expryields error, selects exactly one matching handle arm, - executes the selected arm,
- requires that arm to terminate with either
ok(payload)orerr(E2.case).
4.4 handle arm results
The selected handle arm has two admissible result forms:
ok(payload)err(E2.case)
Their semantics are:
ok(payload)recovers locally and makes thehandleexpression yieldpayload,err(E2.case)performs immediate enclosing-function return witherr(E2.case).
The payload in ok(payload) must be compatible with the success payload shape produced by the surrounding handle expression.
The E2.case in err(E2.case) must belong to the target error type required by the enclosing context.
4.5 handle sugar
For the common remap-only case, handle supports a short arm form:
E1.case -> E2.case
This is sugar for:
E1.case -> { err(E2.case) }
Recovery with ok(payload) requires the explicit block form.
4.6 handle scope
handle may execute user-defined logic inside an arm, but it remains specific to modeled result errors.
It does not provide:
- trap interception,
- arbitrary processing of non-
resultfailure channels, - or a general-purpose exception system.
5. Invariants
optionalmodels absence, not failure.- Expected recoverable failure remains in
result<E>, not inoptional. !is an early-return propagation surface.handleis a typed result-processing surface with controlled recovery or propagation.- Success paths produce payload values directly.
- Error paths in
result<E>remain explicit and typed. - Trap remains outside ordinary recoverable error flow.
applyremains the single semantic call surface even when user code uses direct-call sugar.- Callable-specific dispatch differences do not change the user-visible call pipeline.
- Effect boundaries remain explicit;
applydoes not auto-lift throughoptionalorresult.
6. apply
6.1 Canonical role
apply is the canonical universal call surface in PBS v1.
Direct call syntax is only sugar over apply and does not define separate runtime semantics.
The same observable call model applies to:
- top-level functions,
- struct methods,
- service methods,
- contract calls,
- callback calls,
- host-backed calls.
6.2 Shared observable pipeline
For lhs apply rhs, the shared observable runtime pipeline is:
- form the call target from
lhs, - evaluate any receiver or callable artifact needed for target formation exactly once,
- evaluate
rhsexactly once, - invoke the resolved target,
- produce one of:
- normal return,
- explicit
result<Error>propagation, - or
trap.
Callable-specific dispatch strategy may differ internally, but it does not change this user-visible sequencing model.
6.3 Chained apply
apply chains are parsed right-associatively.
For example:
f1 apply f2 apply f3 apply params
parses as:
f1 apply (f2 apply (f3 apply params))
The observable evaluation order still follows the already-closed left-to-right model:
- form the target of
f1, - evaluate the argument expression for
f1, - within that argument expression, form the target of
f2, - evaluate the argument expression for
f2, - within that argument expression, form the target of
f3, - evaluate
params, - invoke
f3, - invoke
f2, - invoke
f1.
6.4 Effect boundaries
apply does not introduce implicit effect composition.
If a callable returns:
optional<P>, extraction remains explicit throughelse,result<E> P, propagation or remapping remains explicit through!orhandle.
PBS v1 does not auto-lift ordinary call chains through optional or result boundaries.
7. bind
7.1 Role
bind(context, fn_name) is the explicit callback-formation mechanism in PBS v1.
It exists to attach a struct context to a compatible top-level function without introducing general lexical closures.
7.2 Context and target
bind requires:
- a context expression whose runtime value is a struct instance,
- a top-level function target whose first parameter is compatible with that struct type,
- and an expected callback type already validated by static semantics.
7.3 Runtime artifact
bind(context, fn_name) produces a nominal callback value that stores:
- the resolved top-level function target,
- the captured runtime identity of the context value.
The context is not copied or rematerialized during binding.
When the callback is later invoked, the runtime behaves as if it calls:
fn_name(context, ...)
where context is the same captured runtime instance.
7.4 Evaluation and identity
The context expression is evaluated exactly once at bind time.
That evaluation exists only to obtain the struct instance that will be attached to the callback.
After capture:
- the same runtime context identity remains attached,
- mutations performed through the callback observe and affect that same context instance,
binddoes not create a detached copy of the context.
7.5 Storage and retention
bind is not semantically free.
Forming a callback through bind requires runtime storage sufficient to keep:
- the callback target identity,
- and the captured context alive while the callback value remains alive.
For the purposes of PBS v1 semantics, bind should be treated as a callback-forming operation with real retention and heap-facing consequences, even though the final memory/lifetime wording belongs in the dedicated memory specification.
7.6 Trap behavior
At the language-semantics level, bind is trap-free.
Incompatible context type, incompatible function target, or invalid callback shape are compile-time errors rather than runtime traps.
bind introduces no ordinary recoverable error surface and no bind-specific trap surface.
7.7 Non-goal
bind is not:
- general closure capture,
- arbitrary local-environment capture,
- or a promise that future closure features must behave identically.
It is the explicit v1 callback-binding mechanism only.
8. Explicit Non-Decisions
This decision record does not yet close:
- the final memory/lifetime wording for allocation, copy, and retention visibility,
- the final catalog of trap sources that may arise from subexpression evaluation.
9. Spec Impact
This decision should feed at least:
docs/pbs/specs/9. Dynamic Semantics Specification.mddocs/pbs/specs/10. Memory and Lifetime Specification.mddocs/pbs/specs/11. Diagnostics Specification.md
The unresolved cost and retention wording for these surfaces should be completed through:
docs/pbs/agendas/Memory and Lifetime - Agenda.mddocs/pbs/agendas/Heap Model - Agenda.md
10. Validation Notes
The intended behavior is:
some(expr)is eager,opt else fallbackis short-circuit onsome,!propagates the same typed error unchanged,handleremaps typed errors without introducing custom logic,applyremains the universal call surface across callable kinds,- chained
applydoes not create implicit optional/result composition, bindcaptures a struct context identity without copying it,bindforms a callback with real retention/storage consequences,handlemay recover withok(payload)or propagate witherr(E2.case),- short-form
handlearms are sugar for propagation-only remap blocks, trapremains outside both!andhandle.