The public entry point for statifier-ex.
This module is the library's four-function surface (ADR-0006), the only
place a caller needs to import: compile a document, initialize a state
chart, send an event synchronously, and read the active leaf states. Every
layer beneath it - Statifier.Document, Statifier.Machine,
Statifier.MachineState - stays reachable for tooling, but a caller who
only wants to run a state chart never has to name them.
Two boundaries hold at every function in this module:
- String ids only at this boundary, and nowhere deeper (ADR-0005).
Below
Statifier, states and transitions are addressed by interned integer index; this module is where a caller's string ids go in and where they come back out. - Effects are returned, never interpreted (ADR-0003). Every function
that can produce
Statifier.Effect.t()values hands them back as data; none of them is inspected, logged, or executed here. A caller that wants to act on:log,:done, or a:traceeffect does so itself.
All four functions land in this module: compile/2 (with compile/1's
default opts \ []) runs the parse pipeline; initialize/2,
send_event/2, and active_leaf_states/1 wrap Statifier.Interpreter.
Summary
Functions
The active leaf states of machine_state, as a MapSet of string ids.
Compiles SCXML source into a Statifier.Machine.
Initializes machine into its starting Statifier.MachineState, running
the initialization macrostep to quiescence.
Sends one event to machine_state, running a macrostep to quiescence and
returning the resulting position.
Starts a Statifier.Session for machine on Statifier.SessionSupervisor,
under the ADR-0027 session runtime.
Types
@type error() :: Statifier.Parser.ParseError.t() | Statifier.Lowering.Error.t() | Statifier.Validator.Error.t() | Statifier.Compiler.Error.t()
The union of every error struct any pipeline stage compile/2 runs can
produce.
Functions
@spec active_leaf_states(machine_state :: Statifier.MachineState.t()) :: MapSet.t(String.t())
The active leaf states of machine_state, as a MapSet of string ids.
Statifier.MachineState.active_leaf_states/1 returns interned integer
indexes; this is the boundary ADR-0005 reserves for translating them to
the ids a caller wrote in the document, and nothing beneath Statifier
ever returns a string id. Statifier.Machine.id/2 returns nil for the
root and for every nameless state - a state with no id cannot be named by
any caller's expectation either, so it is dropped rather than raised or
given a synthetic name that no document actually wrote.
@spec compile(source :: binary(), opts :: keyword()) :: {:ok, Statifier.Machine.t()} | {:error, [error()]}
Compiles SCXML source into a Statifier.Machine.
Runs the full pipeline - Statifier.Parser.parse/1,
Statifier.Lowering.lower/2, Statifier.Validator.validate/3,
Statifier.Compiler.compile/1 - in that order, stopping at the first stage
that fails. Statifier.Parser.parse/1 is the one stage that reports a
single error rather than a list; this function wraps it so every failure
from every stage has one shape: {:error, [error()]}.
Validator.validate/3 returns three elements on both arms (ADR-0033). On
success its warnings ride onto the returned Machine.t()'s warnings
field rather than a third element of this function's own return, so a
document with warnings still compiles and the caller finds the findings on
the machine (Statifier.Machine's moduledoc explains why they live there).
Its error arm's extra element is collapsed back to this function's own
{:error, [error()]} shape so this stage's failure looks like every other
stage's.
opts defaults to [], so every existing compile/1 call keeps its exact
behavior. The one recognized option today is invoke_content_markup: true (ADR-0042): it relaxes Statifier.Validator.Checks.Boilerplate's
root-namespace check (spec 3.2.1's xmlns requirement) to accept a root
that declares no namespace at all, the same leniency
Statifier.Lowering.Namespace.scxml_vocabulary?/1 already applies to
lowering dispatch. It exists to compile the verbatim source slice
Statifier.Invoke.Source.resolve/2 extracts from an <invoke><content>
element - G.6 (informative) places an undeclared-namespace child of
<content> in the SCXML namespace by ordinary XML scoping, a fact the
slice's standalone compile can no longer see for itself. It is not a
general validation off-switch: a root that declares a namespace other than
SCXML's still fails, in this mode and without it alike, and version must
still be "1.0" either way.
Two more recognized options, :chart_name and :chart_version, ride
straight through to Statifier.Machine.Identity.of_source/2 and are
ignored by every pipeline stage before it: Validator.Context.build/3
reads only :invoke_content_markup from this same opts, so the two
identity options pass through the pipeline untouched and no stage needs a
change to carry them. They stamp the returned Machine.t()'s identity
field (Statifier.Machine.identity/1) alongside the content hash
compile/2 always takes over source.
A successful compile also stamps source (the exact bytes passed in) and
compile_opts (opts filtered through the closed
@persisted_compile_opts allowlist, in the allowlist's own order) onto the
returned Machine.t(), so it can reproduce itself later - see
Statifier.Machine's moduledoc for the invariant this keeps with
identity.
@spec initialize(machine :: Statifier.Machine.t(), opts :: keyword()) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
Initializes machine into its starting Statifier.MachineState, running
the initialization macrostep to quiescence.
A straight pass-through to Statifier.Interpreter.initialize/2, mirroring
that function's own untagged {machine_state, [effect]} pair rather than
wrapping it in an {:ok, _, _} this facade would have to invent: a
%Machine{} is valid by construction, so initialization cannot fail.
opts is Statifier.MachineState.new/2's own option set (:trace,
:datamodel, :max_macrostep_rounds), passed straight through and
interpreted by neither this function nor Interpreter.initialize/2
itself.
The returned effects are data, never performed here (ADR-0003) - a caller that wants the initialization log/trace effects has them; a caller that does not is free to discard them.
@spec send_event( machine_state :: Statifier.MachineState.t(), event :: Statifier.Event.t() | String.t() ) :: {:ok, Statifier.MachineState.t(), [Statifier.Effect.t()]} | {:error, :not_running}
Sends one event to machine_state, running a macrostep to quiescence and
returning the resulting position.
A straight pass-through to Statifier.Interpreter.handle_event/2:
{:error, :not_running} comes back unchanged when machine_state has
already terminated, rather than being reinterpreted into some other
shape. As with initialize/2, the returned effects are handed back as
data and never inspected, logged, or executed here (ADR-0003).
event may be a Statifier.Event.t() or a plain name string; the string
clause is a convenience over Statifier.Event.external/2 and carries no
data of its own - a caller who needs event data builds the %Event{}
directly.
@spec start_session(machine :: Statifier.Machine.t(), opts :: keyword()) :: {:ok, pid()} | {:error, term()}
Starts a Statifier.Session for machine on Statifier.SessionSupervisor,
under the ADR-0027 session runtime.
opts is Statifier.Session.start_link/2's own option set, passed
through unchanged. This is the runtime-placed alternative to calling
Statifier.Session.start_link/2 directly: a session started here lands
on Statifier.SessionSupervisor (restart: :temporary, so a crash is
never silently restarted into a fresh, unrelated session id - ADR-0027
decision 4) and registers itself under Statifier.Registry during
init/1, so it becomes reachable by other sessions as
#_scxml_<sessionid> (ADR-0027 decision 2). A session started instead by
a bare Statifier.Session.start_link/2 call is legal but stays
unregistered, and therefore unreachable by id from any session but
itself.
Requires Statifier.Supervisor to already be placed somewhere in the
embedder's own supervision tree - this library ships no application
callback (ADR-0027 decision 1), so nothing starts one automatically. The
return value is DynamicSupervisor.start_child/2's own
{:ok, pid} | {:error, term} (see its docs for what term can be, most
commonly Statifier.SessionSupervisor itself not being started).