Statifier.Publish (Statifier v2.9.0)

Copy Markdown View Source

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 in docs/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 - the Statifier.Parser.Location.t() of the element the finding is about, or nil when 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: - a Statifier.Send.Types.t(), the set a host builds with Statifier.Send.Types.from_send_types/1 from the :send_types map it will start the chart with. nil is no declaration: the built-in set only, so every non-built-in <send type> is reported.
  • invoke_types: - a Statifier.Invoke.Types.t(), the set a host builds with Statifier.Invoke.Types.from_handlers/1 from its :invoke_handlers map, what row S6's check reads. nil is 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. nil is 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

declaration()

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

finding()

@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

findings(machine, declaration \\ [])

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