Statifier.Document.State (Statifier v2.0.0)

Copy Markdown View Source

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:

FieldMeaningful onRepresentable misuseCaught by
donedata:finaldonedata on a non-final statethe validator's check 8
datamodel_element:state, :parallela <datamodel> on a :final or :history stateStatifier.Validator.Checks.Data's :datamodel_bad_parent
states:state, :parallelstate children under a :finalthe validator's check 6
statesanya :history child of :final or of another :historythe validator's check 5
initial / initial_element:stateboth forms on one statethe validator's check 4
initial / initial_element:statean initial on an atomic statethe validator's check 3 (target resolution and descendancy)
history_type:historyhistory_type on a non-history kindnothing - unbuildable from lowering, see below
transitions:state, :parallel, :history, :initial slottransitions on a :finalthe validator's check 6, widened to also cover this case
idanynil idnot an error; the spec makes id optional
idanyid="" written explicitlythe 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

t()

@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()]
}