The block runner - spec 4.9's rule for a block of executable content,
ported once here rather than at each of Statifier.Interpreter.ExitEntry's
four call sites: a block runs its nodes in document order; if a node
errors, the rest of the block does not run, and the error becomes an
error.* event on the internal queue - error.execution for every
failure but one, and error.communication for a <send> the core
judges undeliverable (ADR-0048) - the errors-are-events conversion
happens here and only here, never in a leaf node's own
Statifier.ExecutableContent.execute/2 implementation. Other blocks are
unaffected: a block that errors leaves every other block - another
<onentry> on the same state, an <onexit>, a transition's own content -
to run exactly as if nothing had happened, since each call to
execute_block/3 is independent.
Ordering and the trace
Unlike Statifier.Effect.Trace.ExitSet/EntrySet - computed before acting
and therefore emitted first - Trace.ContentExecuted reports what ran,
which is only known once the block has finished, so it is appended after
the block's own effects rather than emitted first. Its c_indexes are the
nodes that actually executed, in execution order: on the happy path that
is the whole block; on an error it is a prefix ending at (and including)
the failing node - the failing node did execute, it just returned
{:error, _} rather than succeeding, so it is not absent from the trace,
only absent from the effect list. An empty block still goes through the
trace gate: with tracing on it emits one Trace.ContentExecuted carrying
the block's owner and c_indexes: [], because a trace consumer cannot
otherwise tell an <onentry/> that ran with no content apart from an
<onentry> that never ran at all - the effect stream would be identical
either way. With tracing off the empty-block clause still costs nothing:
it calls Effect.trace/3 directly rather than building a Context and
running the fold, so it keeps the cheapest possible shape on the untraced
hot path.
Effect.trace/3 is called with the block's final machine_state: no
node in a block calls MachineState.begin_macrostep/1 or
begin_microstep/1, so the counters at the block's start and end are
identical either way. Effect.trace/3's own macro binds a local
machine_state inside a hygienic quote, so passing this function's own
machine_state local is never shadowed by the macro's expansion.
The datamodel context, built once per block
execute_block/3 builds one Statifier.Evaluator.context/1 value up
front and hands it to every node in the block through
Statifier.ExecutableContent.Context's datamodel_context field - the
"never per expression" commitment docs/datamodel.md makes, kept at the
tightest interval that stays correct (see that struct's own moduledoc for
why per-block rather than per-macrostep). This module builds that context
exactly once, before any node in the block runs, and never rebuilds it
itself - the seam named here is taken in
Statifier.Machine.Content.Assign's own execute/2, never in this runner
(docs/architecture.md:112-114's "never a change to the runner"). A node
that does not rebuild the context - every node but
<assign> today - still sees the block's original snapshot for the rest of
the block, datamodel writes included.
Errors-are-events, once
A node's Statifier.ExecutableContent.execute/2 returns {:error, reason}
with reason a bare term() - never a Statifier.Event, per
ADR-0003's error model ("only the interpreter raises error.execution").
This module is that interpreter boundary: on the first {:error, reason}
from a node at c_index, the fold halts and
Statifier.MachineState.raise_platform/4 raises "error.execution" with
cause origin {:content, c_index, owner} and data: reason -
raise_platform/4 rather than raise_internal/4 because spec 5.10.1
classifies error.* as a platform event (the type VALUE), while spec
3.12.2 is what requires the error to go on the internal queue at all.
raise_internal/4 and raise_platform/4
(lib/statifier/machine_state.ex:240, :272) enqueue identically and
differ only in the type stamp, so 3.12.2 is satisfied either way and
only 5.10.1 decides between them - the origin still names the content
node the platform is raising about. Nodes that already ran keep the
effects they already produced, in order; the failing node contributes
none of its own (it returned an error, not a partial success).
A node may name a send id
ADR-0047 widens the runner's error model: a node may name the send id
its failure belongs to, and 5.10.1's MUST ("in the case of error events
triggered by a failed attempt to send an event, the Processor MUST set
this field to the send id of the triggering <send> element") is
unconditional, so the id travels as an event field rather than only
inside data. ADR-0048 widens it once more: the node now names the
event as well as the id, since a <send> can be rejected for two
distinct reasons that raise two distinct error.* names (6.2.4's
invalid/unsupported arm and 6.2.4's "unable to dispatch" arm). This is
Statifier.Machine.Content.Send's rejection kind, :execution | :communication, carried alongside the reason rather than the string
itself - the leaf still names no error.* event of its own.
raise_execution_error/4 gains a clause matching {:send_rejected, send_id, kind, reason}, mapping kind to the event name and
destructuring the rest into data: reason and sendid: send_id; the
inner reason is still what a consumer reading data sees, the same
shape as any other content failure.
This clause is reachable from the fatal arm only: 5.9.1's
pending_errors drain (below) is the non-fatal channel, and no node ever
puts a {:send_rejected, _, _, _} reason there - deliberately, since a
drained error does not abort the block, and 4.9's abort is the whole
point of the ADR-0047/ADR-0048 rejection.
Non-fatal errors, drained here too
Spec 5.9.1: "If a conditional expression cannot be evaluated as a boolean
value ('true' or 'false') or if its evaluation causes an error, the SCXML
Processor MUST treat the expression as if it evaluated to 'false' and
MUST place the error 'error.execution' in the internal event queue." A
leaf node cannot raise that itself (ADR-0003's error model, and
content_acceptance_test.exs's structural sweep mechanically enforces
it), so it records the reason in context.pending_errors instead and
keeps going. run_nodes/2 drains that list after every node it runs -
on the success path and on the failure path alike - through
raise_execution_error/4, the same function the fatal path uses, so the
origin stamp stays {:content, c_index, owner} and there is still exactly
one site in this tree that names "error.execution" for content.
Ordering caveat. Because the drain happens after the node returns, a
pending error is queued after any event the node's own execution (e.g.
an <if>'s selected partition) already raised, whereas spec 5.9.1 reads
as queuing it at the moment the cond is evaluated - i.e. before. Getting
the order right would require either the leaf to raise (ADR-0003) or a
second protocol round-trip per node; neither is justified by any corpus
document today, and no corpus file distinguishes this window.
Spec 4.9's closing sentence mitigates the consequence without sanctioning the deviation: "error events will not be removed from the queue and processed until all events preceding them in the queue have been processed". That establishes FIFO processing of a queued error event; it says nothing about which queue position the error may be inserted at, so it is not license to insert later than 5.9.1 requires. What it does establish is that the event is still eventually processed, which bounds how much the wrong position can cost.
Summary
Functions
Runs the block of content nodes named by c_indexes, in document order,
stopping at the first error - executeContent(content) (Appendix D),
wrapped with the Trace.ContentExecuted emission. See the moduledoc for
the full block-semantics contract.
Functions
@spec execute_block( machine_state :: Statifier.MachineState.t(), owner :: Statifier.Machine.Content.owner(), c_indexes :: [non_neg_integer()] ) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
Runs the block of content nodes named by c_indexes, in document order,
stopping at the first error - executeContent(content) (Appendix D),
wrapped with the Trace.ContentExecuted emission. See the moduledoc for
the full block-semantics contract.