prometeu-studio/docs/compiler/pbs/decisions/Dynamic Semantics - Effect Surfaces Decision.md
2026-03-24 13:42:37 +00:00

12 KiB

Dynamic Semantics - Effect Surfaces Decision

Status: Accepted (Implemented) 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,
  • handle processing.

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:

  1. optional is a runtime presence/absence carrier with canonical some(payload) and none states.
  2. some(expr) evaluates expr eagerly and exactly once before forming the some carrier.
  3. opt else fallback evaluates the left operand exactly once and evaluates fallback only when the left operand is none.
  4. optional is trap-free by itself; only subexpression evaluation may trap.
  5. result<E> is the runtime carrier for expected, modelable failure in function return flow.
  6. expr! evaluates expr exactly once and performs immediate enclosing-function error propagation on failure.
  7. handle expr { ... } evaluates expr exactly once and, on error, executes exactly one matching arm.
  8. A handle arm may execute user-defined logic.
  9. A handle arm must terminate with either ok(payload) or err(E2.case).
  10. ok(payload) in a handle arm recovers locally and yields the payload as the value of the handle expression.
  11. err(E2.case) in a handle arm performs immediate enclosing-function return with that error.
  12. handle supports a short remap form E1.case -> E2.case as sugar for a block that returns err(E2.case).
  13. Neither ! nor handle intercepts or converts traps.
  14. apply is the canonical universal call surface for PBS v1 across all callable categories.
  15. apply chains parse right-associatively but preserve the already-closed left-to-right observable evaluation model.
  16. apply does not introduce implicit composition of optional or result surfaces.
  17. bind(context, fn_name) forms a nominal callback value by attaching a struct context to a top-level function target.
  18. bind evaluates 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.
  19. bind is 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 expr eagerly,
  • evaluates it exactly once,
  • then forms the some(payload) carrier from the produced payload.

3.3 Extraction

opt else fallback:

  1. evaluates opt exactly once,
  2. if the result is some(payload), yields the extracted payload and does not evaluate fallback,
  3. if the result is none, evaluates fallback and 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 handle arms, where they control recovery or propagation.

4.2 ! propagation

expr!:

  1. evaluates expr exactly once,
  2. if expr yields success, yields the extracted success payload as the value of the expression,
  3. if expr yields error, performs immediate enclosing-function return with the same err(...).

! does not:

  • remap the error,
  • intercept traps,
  • or continue ordinary evaluation after the propagated error path is chosen.

4.3 handle

handle expr { ... }:

  1. evaluates expr exactly once,
  2. if expr yields success, yields the extracted success payload directly,
  3. if expr yields error, selects exactly one matching handle arm,
  4. executes the selected arm,
  5. requires that arm to terminate with either ok(payload) or err(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 the handle expression yield payload,
  • err(E2.case) performs immediate enclosing-function return with err(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-result failure channels,
  • or a general-purpose exception system.

5. Invariants

  • optional models absence, not failure.
  • Expected recoverable failure remains in result<E>, not in optional.
  • ! is an early-return propagation surface.
  • handle is 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.
  • apply remains 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; apply does not auto-lift through optional or result.

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:

  1. form the call target from lhs,
  2. evaluate any receiver or callable artifact needed for target formation exactly once,
  3. evaluate rhs exactly once,
  4. invoke the resolved target,
  5. 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:

  1. form the target of f1,
  2. evaluate the argument expression for f1,
  3. within that argument expression, form the target of f2,
  4. evaluate the argument expression for f2,
  5. within that argument expression, form the target of f3,
  6. evaluate params,
  7. invoke f3,
  8. invoke f2,
  9. invoke f1.

6.4 Effect boundaries

apply does not introduce implicit effect composition.

If a callable returns:

  • optional<P>, extraction remains explicit through else,
  • result<E> P, propagation or remapping remains explicit through ! or handle.

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,
  • bind does 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.md
  • docs/pbs/specs/10. Memory and Lifetime Specification.md
  • docs/pbs/specs/12. Diagnostics Specification.md

The unresolved cost and retention wording for these surfaces should be completed through:

  • docs/pbs/agendas/Memory and Lifetime - Agenda.md
  • docs/pbs/agendas/Heap Model - Agenda.md

10. Validation Notes

The intended behavior is:

  • some(expr) is eager,
  • opt else fallback is short-circuit on some,
  • ! propagates the same typed error unchanged,
  • handle remaps typed errors without introducing custom logic,
  • apply remains the universal call surface across callable kinds,
  • chained apply does not create implicit optional/result composition,
  • bind captures a struct context identity without copying it,
  • bind forms a callback with real retention/storage consequences,
  • handle may recover with ok(payload) or propagate with err(E2.case),
  • short-form handle arms are sugar for propagation-only remap blocks,
  • trap remains outside both ! and handle.