The one publish-time function a host calls over a compiled chart:
findings/2 runs every publish-time check this package holds and
returns what they found, each finding naming its row of
docs/publish-time-checks.md (ADR-0073).
A host's publish step is the gate between an edited chart and its first
execution. It calls findings/2 with the chart and a declaration of
the deployment the chart will run in - the send types and invoke types
the host registers, and the event names it says the chart accepts - and
decides what to refuse on. The function reports and refuses nothing:
which rows a host refuses a publish on is the host's decision, and an
editor runs the same function at edit time to show the same findings.
What a check is
Every check inside findings/2 is the publish-time twin of one runtime
refusal, and the table in docs/publish-time-checks.md names the row.
The checks that already exist as public functions are composed in, never
moved: Statifier.Send.Types.unsupported_sends/2 (row S1) and
Statifier.Chart.check_accepts/2 (row S15). The rows the table lists as
NONE land here one at a time, each as one private clause of check/3
and one entry in the row list, never as a module or a public function of
its own. Statifier.Validator and Statifier.compile/2 are untouched:
a chart that compiles today compiles tomorrow, whatever this function
reports.
What a finding is
A finding is a plain map with four keys:
row- the row's id indocs/publish-time-checks.md, a string such as"S1".kind- which finding of that row this is, an atom the row's clause defines, so one row can report more than one kind.location- theStatifier.Parser.Location.t()of the element the finding is about, ornilwhen the finding has no element (a declared name the chart never selects on has none).data- the row's own detail, a map whose keys the row's clause defines.
Findings are ordered by row, in the row list's order, and inside a row in the composed check's own order.
The declaration
The second argument is a keyword list. Every key is optional, and an
absent key is nil, which each check reads as "not declared" in the
sense its composed function gives it:
send_types:- aStatifier.Send.Types.t(), the set a host builds withStatifier.Send.Types.from_send_types/1from the:send_typesmap it will start the chart with.nilis no declaration: the built-in set only, so every non-built-in<send type>is reported.invoke_types:- aStatifier.Invoke.Types.t(), the set a host builds withStatifier.Invoke.Types.from_handlers/1from its:invoke_handlersmap, what row S6's check reads.nilis no declaration: the built-in set only, so every non-built-in<invoke type>is reported.accepts:- the event names the host declares the chart accepts, a list of strings.nilis no declaration: the computed vocabulary is the contract and row S15 reports nothing.
A key outside these three, or a value of the wrong shape, raises
ArgumentError: it is a caller's programming error, not data.
Pure and total over a %Statifier.Machine{} and a declaration: the
function reads the compiled machine and runs nothing, so the same chart
and declaration always give the same findings.
Summary
Types
What the host declares about the deployment: the registered send types, the registered invoke types, and the accepted event names. Every key is optional.
One finding: its row of docs/publish-time-checks.md, which finding of
that row it is, the element's location when it has one, and the row's
own detail.
Functions
Every finding of every publish-time check this package holds, over
machine and the host's declaration, ordered by row.
Types
@type declaration() :: [ send_types: Statifier.Send.Types.t() | nil, invoke_types: Statifier.Invoke.Types.t() | nil, accepts: [String.t()] | nil ]
What the host declares about the deployment: the registered send types, the registered invoke types, and the accepted event names. Every key is optional.
@type finding() :: %{ row: String.t(), kind: atom(), location: Statifier.Parser.Location.t() | nil, data: map() }
One finding: its row of docs/publish-time-checks.md, which finding of
that row it is, the element's location when it has one, and the row's
own detail.
Functions
@spec findings(machine :: Statifier.Machine.t(), declaration :: declaration()) :: [ finding() ]
Every finding of every publish-time check this package holds, over
machine and the host's declaration, ordered by row.
Row S1 composes Statifier.Send.Types.unsupported_sends/2: one finding
of kind :unsupported_send_type per <send> whose literal type the
declared send_types: does not register, at the <send>'s location,
with data: %{type: type}.
Row S2 reads every <send> whose type is built-in (absent, "scxml",
or the SCXML Event I/O Processor URI) and whose target is a literal,
the rule the runtime applies before the send dispatches: a target
Statifier.Send.Target.parse/1 cannot parse is a finding of kind
:invalid_target, at the <send>'s location, with data: %{target: target}, in document order. A registered or unsupported type is not
judged (a registered processor's target is its own route string; an
unsupported type is S1's), and a targetexpr or a typeexpr is left to
the runtime. It needs no declaration.
Row S3 reads every <send> whose literal target names an invocation
(#_<invokeid>) and whose type is built-in (absent, "scxml", or the
SCXML Event I/O Processor URI), the rule the runtime applies against the
route snapshot: a target whose invoke id no <invoke id> in the chart
declares is a finding of kind :unreachable_target, at the <send>'s
location, with data: %{target: target}, in document order. A delayed
send is judged too; the runtime finds the same target unreachable when
its timer fires. An <invoke idlocation> declares no id, a registered or
unsupported type is not judged, and a targetexpr or a typeexpr is
left to the runtime, as is a session id or #_parent, which depend on
who started the execution. It needs no declaration.
Row S6 reads every <invoke> whose type is a literal, the rule the
runtime applies before the invocation starts: a type
Statifier.Invoke.Types.registered?/2 does not register against the
declared invoke_types: is one finding of kind
:unregistered_invoke_type, at the <invoke>'s location, with
data: %{type: type}, in document order. With no invoke_types: only
the built-in scxml types are registered, as registered?/2 answers
for nil. An <invoke> with no type is the built-in scxml type and
is not a finding; a typeexpr is resolved at run time and is not
judged.
Row S9 reads every <invoke> of the built-in scxml type (no type, or
a literal one the built-in handler answers to) whose <content> is
inline text or markup, the rule the runtime applies when it starts the
child: the body is read as the invocation reads it, and a string is
compiled as a standalone chart with invoke_content_markup: true. A body
that does not compile is a finding of kind :child_does_not_compile,
with data: %{errors: errors}, Statifier.compile/2's own error list; a
body that reads as a value rather than a string (a number, null, or
nothing but whitespace) is :content_not_markup, with
data: %{content: value}.
Each finding is at the <invoke>'s location, in document order. A
<content expr>, a src, a typeexpr and a non-built-in type are
left to the runtime; a child that compiles but fails to start is too. It
needs no declaration.
Row S11 reads the same literal write locations row S19 reads, in the
same order, and judges the root of each one that resolves, the rule the
runtime write applies after it resolves the location: a root that begins
with _ is a system variable, a finding of kind :system_variable; any
other root the chart does not bring into being is a finding of kind
:unbound_location. A chart brings a root into being with a <data id>,
a <foreach> item or index name, or an assignment in a <script>
body (top-level or in executable content), each of which the runtime
datamodel can hold when the write runs. Each finding is at the
attribute's location, with data: %{attribute: :location | :idlocation | :namelist, source: source, root: root}. A location that does not resolve
is row S19's, not this row's. A root the host supplies when it starts the
chart (the :datamodel option of Statifier.MachineState.new/2) is not
in the source, so the check cannot see it: a finding on such a root is
one the host reads against what it starts the chart with. An invoking
parent's params never supply one: they fill only the child's declared
top-level <data> ids. It needs no declaration.
Row S12 reads the roots every compiled expression and <script> body in
the chart reads, the rule every evaluation applies: the engine
evaluates with unbound roots refused, so reading a root the datamodel
does not hold raises predicator's undefined-variable error. A root read
that the chart does not bring into being and that is not one of the four
system variables (_event, _ioprocessors, _name, _sessionid) is a
finding of kind :undeclared_root, with data: %{root: root, source: source}, once per root per expression, in the order the expression
reads them. A chart brings a root into being with a <data id>, a
<foreach> item or index name, or an assignment in a <script> body
(top-level or in executable content). Each finding is at the attribute's
location (the element's when the attribute has none of its own): the
top-level scripts first, with no location, then each <data>, each
transition's cond, the executable content, and last each state's
<donedata> and <invoke>s, each in document order. A read behind a
short-circuit (a or b) is reported although the data may skip it, a
literal value is never read, and an expression that did not compile is
not judged by this row. A root the host supplies when it starts the
chart (the :datamodel option of Statifier.MachineState.new/2) is not
in the source, so the check cannot see it: a finding on such a root is
one the host reads against what it starts the chart with. An invoking
parent's params reach only the child's <data> ids, so they add no
root. It needs no declaration.
Row S13 lists every compile failure the compiler deferred to run time,
the ones it stores as {:invalid, error} on the compiled node instead of
failing Statifier.compile/2: a <data expr>, an <assign expr>, a
<script> body (in executable content or at the top level) and a
namelist entry of a <send> or an <invoke>. Each is one finding of
kind :compile_error, at the failing expression's location (the
attribute's span, or the <script>'s), with data: %{element: :data | :assign | :script | :send | :invoke, source: source}, in document order.
The runtime raises error.execution carrying the same error the first
time the node runs; every other expression that fails to compile fails
Statifier.compile/2 itself. It needs no declaration.
Row S14 reads every <send> whose delay is a literal, the rule the
runtime applies when the send runs, whatever its type: a delay that
Statifier.Duration.to_ms/1 refuses (not a duration, or a duration with
a remainder finer than a millisecond) is a finding of kind
:invalid_delay, at the delay attribute's location (the <send>'s
when the attribute has none of its own), with data: %{delay: delay},
in document order. A delayexpr is left to the runtime, and so is every
other failure the row names, since the data decides it. It needs no
declaration.
Row S15 composes Statifier.Chart.check_accepts/2: one finding of kind
:unreachable_name per declared name no descriptor in the chart's
vocabulary matches, with data: %{name: name}, then one of kind
:undeclared_descriptor per descriptor the declaration does not state,
with data: %{descriptor: descriptor}; neither has a location. With no
accepts: the row reports nothing.
Row S16 finds every cycle of eventless transitions none of which
carries a cond, the literal half of a macrostep that never reaches
quiescence and spends the round budget. From each atomic state it
follows the transition the engine must take with no event: the first
eventless transition in document order of the state, then of each
ancestor outward. When that transition has no cond, the state it
leads to is decided by the chart - the state itself for a targetless
transition, the target for an atomic one, the target's initial child,
followed down, for a compound one - and a state reached twice closes a
cycle. One finding of kind :eventless_cycle per cycle, at the
location of the transition taken from the cycle's first state in
document order, with data: %{states: [id]}, the atomic states the
cycle passes through in the order it passes through them, starting
there (an id is nil for a state that wrote none); cycles in document
order. A cond ahead of or on the taken transition, a state inside a
<parallel>, a transition with more than one target, and a history
state or <parallel> entered along the way each leave the state to run
time; a chain that reaches a top-level <final> ends the execution and
is no cycle. It needs no declaration.
Row S17 reads every <foreach>'s literal item and index names, the
rule the runtime applies before the loop runs: a name that begins with
_ is a finding of kind :system_variable; any other name that is not a
bare variable name (a letter or _, then letters, digits or _) is
:illegal_item_name or :illegal_index_name. Each finding is at the
attribute's location, with data: %{attribute: :item | :index, name: name}, in document order, item before index; an absent index is
not judged. It needs no declaration.
Row S18 reads every <script> body's assignment targets, the rule the
runtime applies when the script runs: an assignment whose target's root
begins with _ is a finding of kind :system_variable, with
data: %{root: root}, once per root per script, an assignment inside an
if or while body included. Top-level scripts come first, in document
order, with no location (the compiled chart keeps none for them); then
every <script> in executable content, in document order, at the
<script>'s location. A read of a system variable is not a finding, and
a body that did not compile is not judged by this row. It needs no
declaration.
Row S19 reads every literal write location - an <assign>'s location,
a <send>'s or an <invoke>'s idlocation, and each target an empty
<finalize> writes (a namelist entry, or a <param>'s location) -
and resolves it the way the runtime write does, with every variable used
as a bracket key standing for a valid key, since that value is the
data's. A location that does not parse is a finding of kind
:parse_error; one that names something that cannot be assigned (a
literal, a string, a list, a function call, an operator expression) is
:not_assignable; a membership test, an object literal, a cast, a
duration or a relative date is :invalid_node; a bracket key that is
neither a string, an integer nor a variable is :computed_key. Each
finding is at the attribute's location (the element's when the attribute
has none of its own), with data: %{attribute: :location | :idlocation | :namelist, source: source}: executable content first, in document
order, then each state's <invoke>s in document order, idlocation
before the <finalize> targets. A namelist entry that did not compile
is row S13's, not this row's. It needs no declaration.
See the moduledoc for the finding shape and the declaration's keys.