Statifier.Interpreter.Content (Statifier v2.0.0)

Copy Markdown View Source

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

execute_block(machine_state, owner, c_indexes)

@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.