# `Statifier.Interpreter`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier/interpreter.ex#L1)

Appendix D's outer loop, ported function for function (ADR-0002), with its
loop state reified onto `%Statifier.MachineState{}` per
`docs/observability.md` constraint 1: any `%MachineState{}` value is a
complete, resumable interpreter position, and this module keeps nothing
of that position on the call stack.

## The map of the loop

Appendix D's `interpret`/`mainEventLoop`/`microstep`/`exitInterpreter`
collapse onto this module's functions:

| This module | Appendix D |
|---|---|
| `microstep/2` | `microstep(enabledTransitions)` verbatim |
| `microstep/1` | `mainEventLoop`'s inner `while running and not macrostepDone` loop body, hoisted into a value so a paused position is data, not a stack frame |
| `macrostep/1` | that same inner loop, folded to quiescence |
| `main_event_loop/1` | the outer-loop iteration(s) one call of `initialize/2` or `handle_event/2` performs - the invoke pass, its re-entry `continue`, and the trailing `exitInterpreter()` |
| `exit_interpreter/1` | `exitInterpreter` |
| `initialize/2`, `handle_event/2` | `interpret`'s two entry seams |

## Stepping it

Constraint 1's payoff is that a step debugger is `microstep/1` in iex with
no support code. Fold to quiescence, then hand it an event and drive the
next macrostep one round at a time, inspecting the position between calls:

    # in iex, with `machine` already compiled
    {machine_state, _effects} = Interpreter.initialize(machine, trace: true)
    machine_state = MachineState.begin_macrostep(machine_state)

    # one round at a time; a `{:quiescent, _, _}` return ends the macrostep
    {machine_state, effects} = Interpreter.microstep(machine_state)
    machine_state.configuration
    Interpreter.microstep(machine_state)

Every binding above is an ordinary value: keep an earlier `machine_state`
to rewind to it, or round-trip one through `:erlang.term_to_binary/1` to
resume in another process. `macrostep/1` is the same loop run to its fixed
point, so stepping and folding are the same code path, not two.

## Rehydrating a position

A host driving this module directly - rather than through
`Statifier.Session` - already has everything it needs to resume a saved
position; no function in this module exists solely for that purpose
(ADR-0060). Use `Statifier.Position.to_binary/1` and `from_binary/2`
instead of the raw `:erlang.term_to_binary/1` shown above: the identity
check `from_binary/2` runs against the supplied `Statifier.Machine.t()` is
the entire point - a version-1 `term_to_binary` blob decoded against the
wrong chart revision would silently rebuild a `%MachineState{}` that walks
a document it was never measured against.

`from_binary/2` restores every durable field of the position, but two
fields are deliberately per-driver snapshots rather than durable position
state (`Statifier.Position.import/2`'s own docs give the same reason) and
come back `nil`: re-stamp both before the first drive.

    {:ok, machine_state} = Statifier.Position.from_binary(blob, machine)

    machine_state =
      machine_state
      |> Statifier.MachineState.put_routes(routes)
      |> Statifier.MachineState.put_invoke_types(invoke_types)

    {:ok, machine_state, effects} = Interpreter.handle_event(machine_state, event)

Then call any advance entry (`handle_event/2`, `deliver_internal/5`,
`cancel/1`, `microstep/1`, `macrostep/1`) exactly as if the position had
never left this process - every one of them takes a `MachineState.t()` and
trusts it structurally, with no state of its own outside that struct.

What a resumed position must **not** redo is everything `initialize/2`
already did the one time it ran, before the position was ever saved:

- `MachineState.new/2` - the position already carries a real
  `configuration`, `datamodel`, and counters; calling `new/2` again would
  discard all of it and start over.
- `Datamodel.initialize/1` - the datamodel's `<data>` elements have
  already been evaluated once; re-running that pass is `<data>`
  initialization happening twice, not resumption.
- `run_global_scripts/2` - a top-level `<script>` already ran during the
  original `initialize/2`; a resumed position's datamodel already carries
  whatever it wrote.
- `enter_states/2` on the initial transition - the position's
  `configuration` already reflects the states that macrostep entered;
  entering them again duplicates `onentry` content and history recording.

A resumed position skips all four - there is no `initialize/2` call on
this path at all, only the re-stamp above followed by the first advance
entry.

## Counters

`Statifier.MachineState`'s counter contract is the source of truth;
restated here only for where this module writes it. `microstep`'s writer
(`Statifier.MachineState`'s docs name it) has exactly one call site in
this module: the non-empty branch of `run_selected/3`, the private tail
shared by every selection round (eventless, one dequeued internal event,
and the external event `handle_event/2` selects on). A selection round
that finds nothing to run never reaches that call, because no exit or
entry happened - the same "no exit or entry happened" rule
`MachineState`'s counter contract states for why an empty round does not
advance `microstep`.

`macrostep`'s writer has exactly two call sites in this module:
`initialize/2` (the initialization macrostep is macrostep 1) and
`handle_event/2`, once per accepted external event (macrostep 2 onward).
`initialize/2` additionally advances the microstep counter once, directly,
before its own `enter_states/2` call - the pseudocode's
`enterStates([doc.initial.transition])` is not inside `microstep`, so
**the initial entry is microstep 1**, not 0, and nothing the
initialization macrostep emits is stamped `microstep: 0`.

An external event is the one case that is. `begin_macrostep/1` resets the
microstep counter, and `handle_event/2` emits `Trace.EventDequeued` and
(via `run_selected/3`) `Trace.TransitionsSelected` before the round's
first entry has happened, so both carry `microstep: 0` - the round has
begun but no exit or entry has occurred yet, which is exactly what
`microstep: 0` means under `MachineState`'s counter contract. The first
`Trace.ExitSet` of that macrostep is microstep 1.

This is also why the trace effects a selection round emits
(`Trace.EventDequeued`, `Trace.TransitionsSelected`) are always stamped
*one microstep behind* the `Trace.ExitSet` that follows them when the
round is non-empty: both are built from the `machine_state` selection
returned, which is still at the *previous* microstep's count - the
counter only advances afterward, inside `run_selected/3`, right before
`microstep/2` runs. The counter cannot be advanced before the selection
result is known, because an empty result must not advance it.

`round`'s writer (`begin_round/1`) has exactly one call site in this
module: the head of `microstep/1`, both clauses included - so the
`running: false` clause counts a round too, and the first round of a
macrostep's fold is round 1. `handle_event/2`'s own `Trace.EventDequeued`
and `Trace.TransitionsSelected`, and everything `initialize/2` emits
before `main_event_loop/1` begins the fold, are stamped `round: 0` for the
same reason they are stamped `microstep: 0`: no round of this macrostep's
fold has begun yet.

## Deviations, with their reasons (ADR-0002)

- **`microstep/1` is not a pseudocode function name.** It is
  `mainEventLoop`'s inner loop body, hoisted so a debugger can pause
  between rounds without support code (constraint 1). See `microstep/1`'s
  own `@doc`.
- **Quiescence is a tagged return, not a bare atom.** `microstep/1`
  returns `{:quiescent, machine_state, effects}` so the round that ends a
  macrostep can carry out both the machine_state its selection returned
  and that selection's own `Trace.TransitionsSelected`. Every selection
  this module makes emits that trace, with no exception for the terminal
  probe, so `docs/observability.md`'s "includes the empty set" is
  literally true. See `microstep/1`'s own `@doc` for why the
  machine_state half is load-bearing rather than tidiness.
- **The machine_state `Selection` returns is threaded, never discarded.**
  Both `Selection.select_eventless_transitions/1` and
  `Selection.select_transitions/2` return `{machine_state, transitions}`,
  and this module continues with the returned `machine_state` rather than
  the one it passed in - `cond` evaluation lives inside `Selection`, where
  it reshapes that module's private walk without changing either entry
  point's signature or anything in this module.
- **The outer `while running` loop is driven by the caller.** ADR-0003:
  the pure core takes one external event per call, and the session that
  drives it owns the waiting external events and their queue.
  `main_event_loop/1` is the loop's tail - fold to quiescence, then
  `exit_interpreter/1` when `running` went false - not the loop itself.
  See `main_event_loop/1`'s own `@doc` for the exact port-site comment.
- **The invoke pass runs at the end of every fold, inside `main_event_loop/3`.**
  `run_invoke_pass/1` walks `states_to_invoke` sorted into entry order via
  `Machine.document_order/2` (the same sort `enter_states/2` already
  performs, and one the pseudocode itself specifies -
  `statesToInvoke.sort(entryOrder)` - so this is not a deviation), and
  within each state walks its compiled `invoke` list in document order.
  Each invocation's arguments are resolved against one threaded
  `Evaluator.context/1`; any evaluation failure raises `error.execution`
  and yields no `Effect.Invoke` for that invocation, siblings unaffected
  (ADR-0031), and otherwise the invocation is recorded in
  `machine_state.active_invocations` and emits one `{:invoke, _}` effect.
  `states_to_invoke` is cleared once the pass finishes, and one
  `Trace.InvokePass` is emitted last, carrying the states walked and the
  invoke ids started - a phase boundary Appendix D itself names
  (ADR-0012 item 2's parenthetical is illustrative, not closed) that
  predates this bead having any `<invoke>` to trace. When invoking left
  the internal queue non-empty, Appendix D's `continue` re-enters the
  outer loop - a self-call of the private `main_event_loop/3` - which is
  why the round budget has to span it (ADR-0032; see the deviation
  comment above `defp main_event_loop/3`).
- **The finalize/autoforward pass runs once per external event, inside
  `handle_event/2`, before transition selection.** `apply_invoke_passes/2`
  walks the full `configuration` (ADR-0005), and for each state's
  compiled `invoke` list, in document order: when a live invocation's
  invokeid equals the external event's own `invokeid`, its `<finalize>`
  runs in the pure core (`Statifier.Machine.Invoke.finalize`'s
  absent/empty/populated split, spec 6.5); when it autoforwards, one
  `{:autoforward, %Effect.Autoforward{}}` carries the event on, verbatim,
  for the session to deliver - not `Effect.Send`, since 6.4 requires an
  exact copy of every 5.10.1 field and ADR-0003 keeps effect construction
  pure. Neither `if` is inside
  the other's `else`: a matching, autoforwarding invocation does both,
  exactly as Appendix D's own two separate `if`s read. The walk
  short-circuits to a no-op when `active_invocations == %{}` - but even
  that no-op still emits one `Trace.FinalizeAutoforward` with both lists
  empty, the same "includes the empty set" reasoning
  `Trace.TransitionsSelected` already carries into this vocabulary, so the
  pass's absence is never mistaken for a pass that found nothing to do.
- **`returnDoneEvent` becomes a returned effect, appended last.**
  `exit_interpreter/1` builds `{:done, %Effect.Done{}}` instead of
  performing an I/O call (ADR-0003), and appends it after `Trace.Done`
  rather than mid-walk - a mechanical reordering, since effects are a
  returned list and nothing downstream observes the difference in the
  pseudocode's own terms. See `exit_interpreter/1`'s own `@doc`.
- **The macrostep fold is bounded.** Appendix D's inner loop is
  unbounded; a pure core has no external entity to cancel a
  non-terminating macrostep, so the fold spends a round budget and stops
  with a `:budget_exhausted` effect on exhaustion (ADR-0019). See the
  private fold's own comment above `defp macrostep/3`.
- **The round budget spans an invoke re-entry.** `continue` (Appendix D)
  is a self-call of `main_event_loop/3` in this port, and a self-call
  that started a fresh budget every time would reopen the same livelock
  ADR-0019 closed, one loop further out - two states whose `<invoke>`
  arguments deterministically error could hand each other control
  forever. The budget is therefore threaded across every re-entry of one
  `main_event_loop/1` call instead of restarted per fold (ADR-0032). See
  the deviation comment above `defp main_event_loop/3`.
- **A round ordinal counts `microstep/1` invocations.** Appendix D's inner
  loop carries `macrostepDone` and no round variable of any kind
  (ADR-0020); `round` is a hoisting artifact of `microstep/1` itself, like
  the two counters before it. See the comment above `microstep/1`'s two
  clauses.

# `cancel`

```elixir
@spec cancel(machine_state :: Statifier.MachineState.t()) ::
  {:ok, Statifier.MachineState.t(), [Statifier.Effect.t()]}
  | {:error, :not_running}
```

`mainEventLoop`'s cancel path (Appendix D) - `running = false`, then
`continue`, which ends the `while running` loop and reaches
`exitInterpreter()`.

ADR-0002 mechanical deviation: the outer loop is driven by the caller
(`handle_event/2`), so there is no loop here to fall out of; the two steps
the pseudocode reaches by falling out are performed directly, in the same
order. Nothing about the exit itself changes - the states exited, the
`<onexit>` blocks run, the `<donedata>` collected, and the `{:done, _}`
effect produced are all `exit_interpreter/1`'s.

Rejects an already-terminated machine_state with `{:error, :not_running}`,
matching `handle_event/2`: Appendix D would not be inside the loop to check
`isCancelEvent` at all.

# `deliver_internal`

```elixir
@spec deliver_internal(
  machine_state :: Statifier.MachineState.t(),
  kind :: :internal | :platform,
  name :: String.t(),
  origin :: Statifier.Event.Cause.origin(),
  opts :: keyword()
) ::
  {:ok, Statifier.MachineState.t(), [Statifier.Effect.t()]}
  | {:error, :not_running}
```

ADR-0039's re-entry seam: the sole path `Statifier.Session` uses to write
a session-detected `<send>` failure (6.2.4's unsupported/invalid target,
6.2.5's unsupported `type`) - or a `<send target="#_internal">` delivery -
onto `%MachineState{}`'s own internal queue.

Appendix D discovers a routing failure inside `mainEventLoop` and writes
the internal queue in place. ADR-0003 puts routing in the effect
interpreter, which is outside that loop by construction, so ADR-0039
gives the session one way back in: enqueue on the internal queue exactly
as an in-loop `raise` would, then run to quiescence. No selection or
entry/exit procedure changes.

That paragraph is ADR-0002's required citation for a deviation from the
Appendix D pseudocode: the reason is mechanical (where effects run), not
semantic, and the resulting queue state is what the in-loop write would
have produced.

Delegates to `MachineState.raise_internal/4` or `raise_platform/4` by
`kind` - the same two functions the core's own executable content already
uses, so no third internal-queue writer is introduced - then folds
`main_event_loop/1` to quiescence, returning exactly `handle_event/2`'s own
shape.

# `exit_interpreter`

```elixir
@spec exit_interpreter(machine_state :: Statifier.MachineState.t()) ::
  {Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

`exitInterpreter` (Appendix D) - every active state exited in exit order,
the top-level final's `<donedata>` collected, then the terminal effects.

Body, in the pseudocode's own order, with the deviations Decision 10
records:

1. The configuration is captured *before* the walk (`configuration_at_exit`)
   - both `Trace.Done.configuration` and `Effect.Done.configuration`
   document themselves as "the configuration as it stood at exit", which
   the walk would otherwise leave empty.
2. `states_to_exit` = `Machine.exit_order/2` over the configuration, and
   `Trace.ExitSet` is emitted over it, stamped against `pre_exit_state`
   (`machine_state` as it stood before the termination sweep) - the same
   phase-boundary row `exit_states/2` emits, at the one other place this
   engine exits states. ADR-0012 item 2 binds the row to the boundaries
   Appendix D itself names, and `exitInterpreter` names one: its
   `statesToExit` is the same variable `exitStates` computes. Its
   `configuration` field is read from the post-sweep `machine_state`:
   the sweep provably empties the configuration, so this is
   always `MapSet.new()`, but it is read rather than hardcoded so it
   cannot drift from the walk. `Trace.Done` carries the configuration as
   it stood *at* exit (non-empty, from `configuration_at_exit`), but it
   arrives after the walk and means "the run ended here", so it is not a
   substitute for a marker that means "these are about to be exited" and
   its empty `Trace.ExitSet.configuration` counterpart.
3. Each state, in exit order, runs its `onexit` blocks
   (`ExitEntry.run_onexit_blocks/2` - the same per-state body
   `exit_states/2`'s `depart/2` runs), then each of its live invocations
   is cancelled (`ExitEntry.cancel_invocations_for_state/2` - the same
   shared walk `depart/2` runs, so the two exit paths cannot drift),
   before it leaves the configuration.
4. **No history recording.** Appendix D's `exitInterpreter` has no
   history-recording loop at all - unlike `exitStates`, which has two
   consecutive `for s in statesToExit` loops for exactly that reason.
   "Port as written" therefore means recording nothing here; this walk
   never touches `machine_state.history_values`.
5. `returnDoneEvent(s.donedata)` fires for the one state, if any, that is
   `Machine.final?/2` with `parent == 0` - `isFinalState(s) and
   isSCXMLElement(s.parent)`, the same test
   `ExitEntry.raise_completion_events/2` already makes. Since the root is
   compound, at most one child is active, so at most one top-level final
   is ever in the exit set. Its donedata (`ExitEntry.donedata/2`) becomes
   both `Trace.Done`'s and `Effect.Done`'s `donedata`. A failed
   `<content expr>` raises `error.execution` onto the *returned*
   `machine_state`'s internal queue - nothing ever dequeues it, since the
   event loop has already stopped by the time this function runs, but that
   is Appendix D's own consequence (5.6/5.7's error rule is unqualified,
   and `exitInterpreter` runs after the loop) rather than a deviation this
   port introduces. It
   is still observable: `MachineState.internal_events/1` on the returned
   terminal state shows it.
6. The terminal effects are appended last, `{:done, %Effect.Done{}}` last
   of all - `returnDoneEvent` becomes a returned effect rather than an I/O
   call (ADR-0003), and moving its emission to the end of the list is a
   mechanical reordering: effects are a returned list, not an I/O call,
   so nothing the pseudocode's own terms observe changes order. Both
   `Trace.Done` and `Effect.Done` are populated from the same
   `configuration_at_exit` and `donedata` locals, so the trace row and the
   core effect always agree on the terminal position.

`status: :done` is set only here, at the very end - the window
`MachineState`'s moduledoc describes between `running: false` (from
top-level final entry) and `status: :done` (once this walk finishes).

# `handle_event`

```elixir
@spec handle_event(
  machine_state :: Statifier.MachineState.t(),
  event :: Statifier.Event.t()
) ::
  {:ok, Statifier.MachineState.t(), [Statifier.Effect.t()]}
  | {:error, :not_running}
```

`mainEventLoop`'s external-event tail (Appendix D) - the external-event
counterpart to `internal_round/1`'s dequeue-and-select, driven by the
caller instead of a blocking queue read (ADR-0003, `main_event_loop/1`'s
own `@doc`). Rejects when the machine is not running - Appendix D's own
`running` flag, so the guard reads as the pseudocode's loop condition -
otherwise begins a new macrostep, runs the finalize/autoforward pass,
selects on `event`, runs whatever it enables, and folds to quiescence.

# `initialize`

```elixir
@spec initialize(machine :: Statifier.Machine.t(), opts :: keyword()) ::
  {Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

`interpret`'s entry seam (Appendix D) - binds `opts` into a fresh
`%MachineState{}`, enters the top-level initial states, then runs the
initialization macrostep to quiescence. Documents that reach a stable
configuration or even terminate before any external event is ever sent
are corpus-normal (`enter_states/2` can already set `running: false` on a
top-level `<final>` entry, and `main_event_loop/1` runs
`exit_interpreter/1` when it does).

`opts` passes straight to `MachineState.new/2` - no option is interpreted
here, so a new one is a `MachineState` change, not an entry-point change.

Cannot fail: a `%Machine{}` is valid by construction
(`docs/architecture.md`), so this returns the same untagged
`{machine_state, [effect]}` pair every other loop function in this module
returns, rather than an `{:ok, _, _}` wrapper with one possible value.

# `macrostep`

```elixir
@spec macrostep(machine_state :: Statifier.MachineState.t()) ::
  {Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

`mainEventLoop`'s inner `while running and not macrostepDone` loop,
folded to quiescence over `microstep/1` (Decision 7) - not a pseudocode
function name itself; `docs/observability.md`'s vocabulary for that same
loop, hoisted so a paused position is a value on `%MachineState{}` rather
than a stack frame (constraint 1). The fold ends one of three ways:

- **Quiescence** - `microstep/1` returns
  `{:quiescent, machine_state, effects}`: no eventless transition is
  enabled and the internal queue is empty. That round's own effects (the
  terminal eventless probe's `Trace.TransitionsSelected`) are appended
  like any other round's, and its `machine_state` is the one this fold
  returns - never the one it passed in, which is what keeps a write made
  by the final selection from being dropped. The returned `machine_state`
  is still `running`, so this function then appends
  `Trace.MacrostepStable` with the configuration and counters as they
  stand.
- **Termination** - a microstep entered a top-level `<final>`, setting
  `running: false` mid-fold. This is not quiescence -
  `Trace.MacrostepStable`'s own moduledoc reserves that trace for reaching
  a stable configuration - so no `Trace.MacrostepStable` is emitted;
  `Trace.Done` is the vocabulary row for this case, emitted by
  `exit_interpreter/1`.
- **Budget exhaustion** (ADR-0019) - the private fold spent
  `machine_state.max_macrostep_rounds` without reaching quiescence. The
  position comes back exactly as the last round left it: `running` stays
  `true` and `status` stays `:running` (no `exit_interpreter/1` runs, no
  `Effect.Done` is built - faking termination would be a semantic lie).
  `Trace.MacrostepStable` is not emitted, and a
  `{:budget_exhausted, %Effect.BudgetExhausted{}}` core effect is
  appended last.

The three outcomes are therefore mutually exclusive per macrostep.
`macrostep/1` is public rather than starting private, because it is
exactly `microstep/1` driven to a fixed point and a stepper and the fold
should be the same code path (`docs/observability.md:41-42`).

# `main_event_loop`

```elixir
@spec main_event_loop(machine_state :: Statifier.MachineState.t()) ::
  {Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

`mainEventLoop`'s outer `while running` loop (Appendix D), one call's
worth of iterations (ADR-0032). The loop itself is
driven by the caller - one call of `initialize/2` or `handle_event/2`
reaches here having already run its own selection round - so this
function is what is left of the pseudocode's loop body after that: fold
to quiescence with the private `macrostep/3`, run the invoke pass, clear
`states_to_invoke`, and `continue` (a self-call of the private
`main_event_loop/3`) when invoking left the internal queue non-empty;
otherwise run `exit_interpreter/1` when the fold left `running` false, or
stop and emit the one terminal effect this call produces.

Delegates immediately to the private `main_event_loop/3`, which carries
the round budget across every re-entry (ADR-0032) exactly as the private
`macrostep/3` already carries it across rounds of one fold.

# `microstep`

```elixir
@spec microstep(machine_state :: Statifier.MachineState.t()) ::
  {Statifier.MachineState.t(), [Statifier.Effect.t()]}
  | {:quiescent, Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

`mainEventLoop`'s inner `while running and not macrostepDone` loop body,
hoisted into a named, resumable round (Decision 1) - not a pseudocode
function name itself. One call makes exactly one round of progress, and
begins it: both clauses call `MachineState.begin_round/1` first, so the
returned position's `round` names which round this one was (ADR-0020).

- Not `running` - returns `{:quiescent, machine_state, []}`, with `round`
  advanced and nothing else changed.
- An eventless transition is enabled - the round runs it, exactly as
  `microstep/2` above.
- No eventless transition is enabled - falls to `internal_round/1`,
  which dequeues one internal event (if any) and selects on it.

Never runs two rounds and never inspects the call stack for a paused
position: the returned `machine_state` *is* the position, on both the
progressing and the quiescent return.

**Quiescence carries a machine_state and an effect list of its own**
(Decision 2, revised). The round that ends a macrostep still ran a
selection - `Selection.select_eventless_transitions/1` - and that call
returns a machine_state. A bare `:quiescent` atom has nowhere to put it,
so the fold would fall back to the machine_state it passed *in* and drop
whatever the final selection wrote, on the one round every macrostep ends
with.

Be precise about which writes that loses, because the obvious candidate
is not one of them. The two `Selection` entry points enqueue
`error.execution` on a failed `cond` - `condition_match/2`
itself stays a pure query and never enqueues, so the write happens in
`select_transitions/2` and `select_eventless_transitions/1` on the way
out - and that enqueue is
self-rescuing: it leaves the internal queue non-empty, so
`internal_round/1` takes its dequeue branch instead of the terminal one
and the write survives even under the old shape - traced end to end and
pinned by `InterpreterAcceptanceTest`'s
"a failed cond becomes a catchable error.execution" tests. What the old
shape lost was any selection-side write that does *not* touch the
internal queue - a datamodel write, a memo, a diagnostic - since only a
queue write changes which branch runs. Carrying the machine_state out
closes that gap without having to predict which kind of write a future
selection-side change lands on.

The effect slot is the other half: it is what the terminal eventless
probe's own `Trace.TransitionsSelected` rides out on, which is why
`docs/observability.md`'s "includes the empty set" holds with no
exception.

# `microstep`

```elixir
@spec microstep(
  machine_state :: Statifier.MachineState.t(),
  enabled_transitions :: [Statifier.Machine.Transition.t()]
) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
```

`microstep(enabledTransitions)` (Appendix D) - exit the states
`enabled_transitions` leave, run each transition's own content in
document order, then enter the states they reach. The three calls run in
exactly this order with nothing between them, matching the pseudocode
line for line; the effect list is each block's effects concatenated in
the same order.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
