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

356 lines
12 KiB
Markdown

# 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:
```text
E1.case -> E2.case
```
This is sugar for:
```text
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:
```text
f1 apply f2 apply f3 apply params
```
parses as:
```text
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:
```text
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`.