One <state>, <parallel>, <final>, or <history> element - a single
struct with a kind atom rather than four per-kind structs.
One struct with a kind atom rather than four per-kind structs, with two
refinements beyond that basic shape, both carried forward into this
moduledoc:
The kind set
kind :: :state | :parallel | :final | :history, equal to the element
name that produced it. The rule the set follows: a kind is a thing with
an id that a transition can target. <state>, <parallel>, <final>,
and <history> all qualify - id is optional on each, but present in the
grammar and targetable when given.
:initial is deliberately absent. <initial> has no id attribute at
all (spec 3.6), so it is not a state; it is a slot on its parent,
initial_element below (see Statifier.Document.Initial).
:atomic and :compound are also absent. v1 widened its atom set at
parse time so isCompoundState became a pure atom match
(../statifier/lib/statifier/parser/scxml/state_stack.ex:194-207); v2
does not, for two reasons. First, keeping kind equal to the element name
means lowering never has to decide anything semantic - the widening, if
any, happens in the compiler, the layer ADR-0002 already assigns Appendix
D's predicates to. Second, v1's widening is exactly what produced the
:final-is-not-:atomic divergence that made
collect_atomic_descendants/1 fall through final states
(../statifier/lib/statifier/history_tracker.ex:163-170). The compiler
remains free to stamp a widened discriminator on the compiled state -
this Document layer's kind set constrains only lowering's own output,
never whatever field the compiler's compiled-state struct uses for its
own kind.
Fields that are kind-scoped
Not every field is meaningful on every kind. Rather than four structs each omitting what does not apply, one struct names, per field, which kind it is meaningful on, what a misuse looks like, and which check catches it:
| Field | Meaningful on | Representable misuse | Caught by |
|---|---|---|---|
donedata | :final | donedata on a non-final state | the validator's check 8 |
datamodel_element | :state, :parallel | a <datamodel> on a :final or :history state | Statifier.Validator.Checks.Data's :datamodel_bad_parent |
states | :state, :parallel | state children under a :final | the validator's check 6 |
states | any | a :history child of :final or of another :history | the validator's check 5 |
initial / initial_element | :state | both forms on one state | the validator's check 4 |
initial / initial_element | :state | an initial on an atomic state | the validator's check 3 (target resolution and descendancy) |
history_type | :history | history_type on a non-history kind | nothing - unbuildable from lowering, see below |
transitions | :state, :parallel, :history, :initial slot | transitions on a :final | the validator's check 6, widened to also cover this case |
id | any | nil id | not an error; the spec makes id optional |
id | any | id="" written explicitly | the validator's check 1, widened to treat an explicit empty id as an error |
The remaining "nothing" row is stated here rather than hidden.
history_type on a non-history kind is unreachable from lowering, which
only ever sets it from a <history type="..."> attribute; it is a shape a
hand-built Document (a test fixture, or a future programmatic builder)
could produce, and this moduledoc says so rather than implying a check
exists for it.
A :final carrying transitions is reachable from lowering (spec 3.7
gives <final> no <transition> children, but nothing here refuses to
build one). The validator's check 6 was widened to cover this case rather
than adding a ninth check. This struct was unaffected either way.
id: String.t() | nil no longer overloads nil the way it might if
<initial> were a kind: with <initial> off the kind set entirely,
nil means exactly one thing here - the author omitted an optional id.
An id written as "" is a different thing and lowering keeps it that
way: the validator treats it as an error (spec 3.14 types id as an XML
Schema ID, whose lexical space excludes the empty string), reported by
check 1 as {:empty_id} and, like a nil id, excluded from that check's
uniqueness set.
What this module is not
No Appendix D predicate (is_compound_state?, is_atomic_state?, and
friends) is defined in this module or anywhere under
lib/statifier/document/. Those live on the Machine per ADR-0002;
defining them here would duplicate them in the layer that does not run
them.
Summary
Types
@type t() :: %Statifier.Document.State{ attribute_locations: Statifier.Document.attribute_locations(), datamodel_element: Statifier.Document.Datamodel.t() | nil, donedata: Statifier.Document.Donedata.t() | nil, history_type: :shallow | :deep | nil, id: String.t() | nil, initial: [String.t()], initial_element: Statifier.Document.Initial.t() | nil, invoke: [Statifier.Document.Invoke.t()], kind: Statifier.Document.state_kind(), location: Statifier.Parser.Location.t(), onentry: [Statifier.Document.Block.t()], onexit: [Statifier.Document.Block.t()], states: [t()], transitions: [Statifier.Document.Transition.t()] }