356 lines
12 KiB
Markdown
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`.
|