224 lines
5.0 KiB
Markdown
224 lines
5.0 KiB
Markdown
# Authoring Control Flow Design
|
|
|
|
## Goal
|
|
|
|
Make `wf_authoring` control flow precise enough that future sugar layers can
|
|
delegate to it instead of reimplementing graph semantics.
|
|
|
|
The current problem is `route()`: it accepts both `PathExpr` and `Expr`, then
|
|
dispatches to two different behaviors. Both "route" something, but the caller
|
|
must understand different case semantics depending on the argument type.
|
|
|
|
That is too blurry for the core authoring API.
|
|
|
|
## Design Rule
|
|
|
|
Each public control-flow method should name one decision mechanism.
|
|
|
|
| Method | Decision source | Meaning |
|
|
| --- | --- | --- |
|
|
| `branch` | a node's declared outcome | wire outcome labels to targets |
|
|
| `match` | one graph value | compare that value against equality cases |
|
|
| `when` | one boolean condition | route through `true` / `false` |
|
|
| `choose` | ordered boolean conditions | first true condition wins |
|
|
| `handle` | several source/outcome pairs | send shared outcomes to one target |
|
|
|
|
Future fluent builders or operator sugar must call these methods rather than
|
|
constructing edges/conditions independently.
|
|
|
|
## Keep `branch`
|
|
|
|
`branch()` is already good:
|
|
|
|
```python
|
|
g.branch(tool, {
|
|
"ok": next_step,
|
|
"error": fail_step,
|
|
})
|
|
```
|
|
|
|
It has one clear meaning:
|
|
|
|
```text
|
|
route an existing node outcome
|
|
```
|
|
|
|
It should remain named `branch`. Alternatives such as `on_outcome` are more
|
|
literal but worse to use, and the current method does not suffer from the
|
|
semantic overloading that `route()` does.
|
|
|
|
## Replace `route`
|
|
|
|
### `match`
|
|
|
|
```python
|
|
g.match(
|
|
state("status"),
|
|
{
|
|
"done": finish,
|
|
"retry": retry,
|
|
},
|
|
default=fail,
|
|
)
|
|
```
|
|
|
|
Meaning:
|
|
|
|
```text
|
|
if state.status == "done": finish
|
|
elif state.status == "retry": retry
|
|
else: fail
|
|
```
|
|
|
|
This is the existing `PathExpr` branch of `route()`, renamed to say what it
|
|
actually does.
|
|
|
|
### `when`
|
|
|
|
```python
|
|
g.when(
|
|
state("count").ge(1),
|
|
then=positive,
|
|
otherwise=zero,
|
|
)
|
|
```
|
|
|
|
Meaning:
|
|
|
|
```text
|
|
if state.count >= 1: positive
|
|
else: zero
|
|
```
|
|
|
|
This is the existing `Expr` branch of `route()`, made explicit and easier to
|
|
call correctly.
|
|
|
|
### `choose`
|
|
|
|
```python
|
|
g.choose(
|
|
(state("x").gt(10), big),
|
|
(state("y").exists(), has_y),
|
|
default=fail,
|
|
)
|
|
```
|
|
|
|
Meaning:
|
|
|
|
```text
|
|
if state.x > 10: big
|
|
elif state.y exists: has_y
|
|
else: fail
|
|
```
|
|
|
|
This is an ordered predicate chain. It should lower through the same condition
|
|
construction/connect machinery as `when`, repeated for each clause.
|
|
|
|
`choose` is intentionally one call. Multiple-call fluent syntax can be built
|
|
later on top of it if it proves useful.
|
|
|
|
## Lowering
|
|
|
|
These methods may generate core condition nodes, but callers should not need to
|
|
know the exact node construction details to choose the right API.
|
|
|
|
Canonical lowering:
|
|
|
|
- `branch`
|
|
- no new condition nodes
|
|
- wires declared outcome strings from one source
|
|
- `match`
|
|
- ordered equality-check condition chain
|
|
- one generated condition per case
|
|
- `when`
|
|
- one condition node
|
|
- `true` and `false` edges
|
|
- `choose`
|
|
- ordered condition chain
|
|
- one generated condition per clause
|
|
|
|
Trace behavior should document that `match` and `choose` expand to generated
|
|
condition nodes.
|
|
|
|
## Outcome Names
|
|
|
|
Outcome names are strings at the core wire level, but Python authoring should
|
|
eventually avoid handwritten strings when a `NodeSpec` already declares them.
|
|
|
|
Future improvement:
|
|
|
|
```python
|
|
tool.outcomes.ok
|
|
tool.outcomes.error
|
|
```
|
|
|
|
derived from `NodeSpec.outcomes`, not duplicated constants that can drift from
|
|
the contract.
|
|
|
|
This is orthogonal to the control-flow rename, but it belongs in the same
|
|
authoring quality bar.
|
|
|
|
## Migration
|
|
|
|
`route()` should become deprecated compatibility sugar for one release window,
|
|
then be removed.
|
|
|
|
Recommended behavior during compatibility:
|
|
|
|
- mark `route()` with `@deprecated` so IDEs surface the replacement path
|
|
- `route(PathExpr, cases, ...)`
|
|
- warns and forwards to `match(...)`
|
|
- `route(Expr, {True: a, False: b}, ...)`
|
|
- warns and forwards to `when(...)`
|
|
|
|
The public docs should prefer only:
|
|
|
|
- `branch`
|
|
- `handle`
|
|
- `match`
|
|
- `when`
|
|
- `choose`
|
|
|
|
## Shared Outcome Handlers
|
|
|
|
`handle()` is the reverse-shaped companion to `branch()`: it connects several
|
|
source/outcome pairs to one shared target.
|
|
|
|
```python
|
|
g.handle(
|
|
(lookup_user, "error"),
|
|
(charge_card, "error"),
|
|
to=fail,
|
|
)
|
|
```
|
|
|
|
Meaning:
|
|
|
|
```text
|
|
lookup_user.error -> fail
|
|
charge_card.error -> fail
|
|
```
|
|
|
|
It does not create a join, wait for multiple branches, or inspect state. It is
|
|
just outcome-edge sugar for the common "several things fail the same way" case.
|
|
|
|
## Not In This Pass
|
|
|
|
- fluent/cursor builder APIs
|
|
- operator overloading
|
|
- graph-as-node/subgraph support
|
|
- JSON draft `match` / `when` / `choose` shapes
|
|
|
|
Those later layers should depend on this API once it is stable.
|
|
|
|
## Testing
|
|
|
|
Tests should prove:
|
|
|
|
1. `branch` still wires outcome labels only
|
|
2. `match` reproduces current value-route behavior
|
|
3. `when` reproduces current boolean-route behavior
|
|
4. `choose` lowers ordered predicates correctly
|
|
5. `route` emits deprecation warnings while preserving old behavior
|
|
6. later sugar can delegate to these without needing private builder internals
|