One build_* function per supported SCXML element, reached through
Statifier.Lowering's dispatch map - the structural fix for v1's
903-line, 73-clause state_stack.ex (docs/architecture.md,
"adding an element touches one builder").
Every builder lowers its own children first, through
Statifier.Lowering.walk_children/2, before building its own struct, so
errors accumulate in document order regardless of nesting depth. No
builder here takes a parent element name as an argument: placement of a
tagged child result into a parent's slot is each container's own
place/3, never a fact the child itself needs to know.
Summary
Functions
Builds a %Statifier.Document.Assign{} from an <assign> element, tagged
{:content_node, assign}.
Builds a %Statifier.Document.Cancel{} from a <cancel> element (spec
6.3), tagged {:content_node, cancel} - <cancel> is executable content
exactly as <send> is, so it lands in a block's content list through
the existing {:content_node, _} placement clauses.
Builds a %Statifier.Document.Content{} from a <content> element,
tagged {:content, content}.
Builds a %Statifier.Document.Data{} from a <data> element, tagged
{:data, data}.
Builds a %Statifier.Document.Datamodel{} from a <datamodel> element,
tagged {:datamodel, datamodel}.
Builds a %Statifier.Document.Donedata{} from a <donedata> element,
tagged {:donedata, donedata}.
Builds the partitioning tag for an <else> element (spec 4.5), tagged
{:else, branch}. Same placement rule as build_elseif/2's doc describes
Builds the partitioning tag for an <elseif> element (spec 4.4), tagged
{:elseif, branch} for build_if/2's own fold to consume.
place/3 has no clause accepting this tag for any parent but %If{}
(Decision 7 of the plan cited on Statifier.Document.If) - anywhere else,
the generic catch-all reports {:misplaced_element, "elseif", parent_name}
with no code of its own, reading the branch's own location field the
same way it reads every other tagged struct's.
Builds a %Statifier.Document.State{} with kind: :final from <final>.
Builds a %Statifier.Document.Block{} from a <finalize> element (spec
6.5), tagged {:finalize, block}.
Builds a %Statifier.Document.Foreach{} from a <foreach> element (spec
4.6), tagged {:content_node, foreach_node}.
Builds a %Statifier.Document.State{} with kind: :history from
<history>.
Builds a %Statifier.Document.If{} from an <if> element (spec 4.3),
tagged {:content_node, if_node}.
Builds a %Statifier.Document.Initial{} from <initial>.
Builds a %Statifier.Document.Invoke{} from an <invoke> element (spec
6.4), tagged {:invoke, invoke}.
Builds a %Statifier.Document.Log{} from a <log> element, tagged
{:content_node, log}.
Builds a %Statifier.Document.Block{} from an <onentry> element, tagged
{:onentry, block}.
Builds a %Statifier.Document.Block{} from an <onexit> element, tagged
{:onexit, block}. See build_onentry/2 - same shared build_block/3,
same one-block-per-element rule.
Builds a %Statifier.Document.State{} with kind: :parallel from
<parallel>.
Builds a %Statifier.Document.Param{} from a <param> element, tagged
{:param, param}.
Builds a %Statifier.Document.Raise{} from a <raise> element, tagged
{:content_node, raise}.
Builds a %Statifier.Document.Script{} from a <script> element (spec
5.8), tagged {:content_node, script}.
Builds the %Statifier.Document{} root from <scxml>.
Builds a %Statifier.Document.Send{} from a <send> element (spec 6.2),
tagged {:content_node, send} - unlike <invoke>, <send> is executable
content, so it lands in a block's content list through the existing
{:content_node, _} placement clauses rather than a dedicated state-level
slot.
Builds a %Statifier.Document.State{} with kind: :state from <state>.
Builds a %Statifier.Document.Transition{} from <transition>.
Functions
@spec build_assign(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Assign.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Assign{} from an <assign> element, tagged
{:content_node, assign}.
Reads location (required - spec 5.4.2) and expr, both raw strings -
neither is resolved or compiled here (Statifier.Document.Assign's
moduledoc forbids Predicator under lib/statifier/document/). A missing
location means no struct can be built at all (:location is
@enforce_keys'd on Assign), following build_raise/2's
required-attribute pattern: {nil, [Error.missing_attribute("assign", "location", element.location)]}.
text is set to Statifier.Parser.DOM.text/1's verbatim, untrimmed
concatenation of <assign>'s direct text children (verbatim except for
the parser's XML 1.0 2.11 line-break fold - ADR-0045) - spec 5.4.2's other
value source, an <assign>'s children "provide an in-line specification
of the legal data value" (5.4.2, 5.9.3), so this builder reads
element.children directly rather than calling
Statifier.Lowering.walk_children/2, the same stray-text exemption
build_data/2 takes. An element child is not silently dropped: each one
produces {:misplaced_element, name, "assign"} instead of a build
attempt of its own - <assign> does not hold a markup subtree.
@spec build_cancel(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Cancel.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Cancel{} from a <cancel> element (spec
6.3), tagged {:content_node, cancel} - <cancel> is executable content
exactly as <send> is, so it lands in a block's content list through
the existing {:content_node, _} placement clauses.
Reads both of 6.3.1's own attributes, each raw and nilable
(Statifier.Document.Cancel's moduledoc: sendid/sendidexpr are
simultaneously representable so the validator can report the shape).
<cancel> has no slot for any child - unlike build_send/2 it owns no
place/3/reverse_lists/1 clause of its own, so any child it is given
falls through to place/3's catch-all and comes back a
{:misplaced_element, _, "cancel"} error, the same way a <log> or
<raise> child does. walk_children/2 and place_children/3 are still
called, purely to surface that error - <cancel> reads no child result
of its own.
@spec build_content(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content, Statifier.Document.Content.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Content{} from a <content> element,
tagged {:content, content}.
Reads expr and sets text to Statifier.Parser.DOM.text/1's
concatenation of <content>'s own direct text children, verbatim and
untrimmed except for the parser's XML 1.0 2.11 line-break fold
(ADR-0045) - the struct's own moduledoc defines text as exactly that
concatenation. <content> is the one element exempt from the stray-text
rule (its text is its payload), so this builder reads element.children
directly rather than calling Statifier.Lowering.walk_children/2, which
would otherwise flag that same text as a stray-text error. An element
child is no longer misplaced (ADR-0041): markup/markup_location are
set from a raw slice of ctx.source when <content> has at least one
element child - unlike text, this slice does not fold line breaks;
it is opaque source bytes, CR included, at this layer (ADR-0045 decision
item 5) - and the child is never dispatched or walked, so no
foreign_element or misplaced_element error is possible here either.
@spec build_data(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:data, Statifier.Document.Data.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Data{} from a <data> element, tagged
{:data, data}.
Reads id (required - spec 5.3.1), expr, and src, all raw strings, and
sets text to Statifier.Parser.DOM.text/1's verbatim, untrimmed
concatenation of <data>'s direct text children (verbatim except for the
parser's XML 1.0 2.11 line-break fold - ADR-0045) - a <data>'s text is
its payload (spec 5.3.2), so this builder reads element.children
directly rather than calling Statifier.Lowering.walk_children/2, the
same stray-text exemption <content>'s own builder takes (ADR-0041). An
element child is not silently dropped: each one produces
{:misplaced_element, name, "data"} instead of a build attempt of its
own - <data> does not hold a markup subtree.
A missing id means no struct can be built at all (:id is
@enforce_keys'd on Data), following build_raise/2's required-attribute
pattern: {nil, [Error.missing_attribute("data", "id", element.location)]}.
@spec build_datamodel(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:datamodel, Statifier.Document.Datamodel.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Datamodel{} from a <datamodel> element,
tagged {:datamodel, datamodel}.
<datamodel> has no attributes of its own (spec 5.2.1). Its only
children are <data> elements, placed into Datamodel.data in document
order via place/3 and reverse_lists/1, the same fold every other
list-valued builder uses. <datamodel> is legal at both the document root
and on a :state/:parallel state (place/3 handles both), and lowering
does not reject a <datamodel> on a :final or :history state either -
that placement rule belongs to Statifier.Validator.Checks.Data, since one
%State{} struct covers all four kinds and lowering has no way to
distinguish them here.
@spec build_donedata(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:donedata, Statifier.Document.Donedata.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Donedata{} from a <donedata> element,
tagged {:donedata, donedata}.
content stays nil when <donedata> has no <content> child and
becomes a %Statifier.Document.Content{} when it does. params holds
however many <param> children are present, in document order - both
slots are built here regardless of spec 5.5's content-model rule, which is
Statifier.Validator.Checks.Donedata's to report
(Statifier.Document.Donedata's moduledoc).
@spec build_else(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:else, Statifier.Document.If.Branch.t()}, [Statifier.Lowering.Error.t()]}
Builds the partitioning tag for an <else> element (spec 4.5), tagged
{:else, branch}. Same placement rule as build_elseif/2's doc describes
place/3accepts this tag only inside an%If{}.
<else> takes no attributes at all (4.5.2 gives it none): any attribute
written is reported as {:unexpected_attribute, "else", name} rather than
silently ignored, though the branch (with cond: nil) still builds either
way - an author who mistyped cond on an <else> should see both "this
attribute is not allowed" and get the <else> behavior 4.5.1 defines
regardless. Takes no element children either, the same exemption
build_elseif/2 takes.
@spec build_elseif(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:elseif, Statifier.Document.If.Branch.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds the partitioning tag for an <elseif> element (spec 4.4), tagged
{:elseif, branch} for build_if/2's own fold to consume.
place/3 has no clause accepting this tag for any parent but %If{}
(Decision 7 of the plan cited on Statifier.Document.If) - anywhere else,
the generic catch-all reports {:misplaced_element, "elseif", parent_name}
with no code of its own, reading the branch's own location field the
same way it reads every other tagged struct's.
Reads the required cond (4.4.1) - following build_raise/2's
required-attribute pattern: a missing cond means no branch can be built
at all. <elseif> takes no children of its own (4.4.1 gives it none, the
same "empty element" shape <else> has); an element child is reported as
{:misplaced_element, name, "elseif"} rather than built, the same
exemption build_data/2 takes for its own childless/text-only content
model.
@spec build_final(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:state, Statifier.Document.State.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.State{} with kind: :final from <final>.
@spec build_finalize(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:finalize, Statifier.Document.Block.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Block{} from a <finalize> element (spec
6.5), tagged {:finalize, block}.
Mirrors build_onentry/2/build_onexit/2's shared build_block/3 - one
Block per <finalize>, carrying its own location - but is not routed
through that helper since its tag is :finalize, not :onentry/:onexit.
An <invoke> with no <finalize> child leaves Invoke.finalize at its
struct default of nil; a written but childless <finalize/> still
builds a %Block{content: []} here, which is how Invoke.finalize
distinguishes absent from empty (6.5, Statifier.Document.Invoke's
moduledoc).
@spec build_foreach(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Foreach.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Foreach{} from a <foreach> element (spec
4.6), tagged {:content_node, foreach_node}.
Reads the two required attributes, array and item (4.6.2), following
build_raise/2's required-attribute shape - a missing one means no
struct can be built at all. Unlike build_raise/2, both are checked
before failing: a <foreach> missing both reports two
{:missing_attribute, "foreach", _} errors rather than stopping at the
first, since neither attribute's presence depends on the other and two
errors is strictly more useful to a document author fixing both at once.
item's legality as a variable name is a runtime check, not
lowering's concern - only its presence is checked here.
The optional index (4.6.2) is read unconditionally and defaults to
nil when absent, following Statifier.Document.Foreach's own default.
Otherwise walks children through the shared dispatch, then places each
tagged result into content via place/3's %Foreach{} clause - no
partitioning tags are produced or consumed here, unlike <if>, since
<foreach> has exactly one child block (Decision 8).
@spec build_history(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:state, Statifier.Document.State.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.State{} with kind: :history from
<history>.
Additionally reads type, mapped to :shallow | :deep with :shallow as
the default (spec 3.10) - an out-of-range value (type="sideways") lowers
to :shallow and still keeps its attribute_locations entry, rather than
erroring, so a future validator check can point at the offending text.
@spec build_if(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.If.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.If{} from an <if> element (spec 4.3),
tagged {:content_node, if_node}.
Reads the required cond (4.3.1: cond is required="true" on <if>) -
following build_raise/2's required-attribute pattern: a missing cond
means no struct can be built at all,
{nil, [Error.missing_attribute("if", "cond", element.location)]}.
Otherwise walks children through the shared dispatch, then folds the
tagged results through place/3's %If{}-specific clauses - spec
4.3.2's partition definition, mechanically: one open branch starts,
carrying <if>'s own cond and this element's own location; each
{:elseif, branch} / {:else, branch} result closes the currently open
branch and opens the next; each {:content_node, node} result appends to
whichever branch is currently open. reverse_lists/1's %If{} clause
restores document order once the fold finishes (branches were built
newest-first, the same convention every other list-valued builder here
follows).
@spec build_initial(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:initial, Statifier.Document.Initial.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Initial{} from <initial>.
transitions holds however many <transition> children are present,
including zero and two - lowering builds what is written; the transition
count is the validator's check to make (Statifier.Document.Initial's
moduledoc).
@spec build_invoke(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:invoke, Statifier.Document.Invoke.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Invoke{} from an <invoke> element (spec
6.4), tagged {:invoke, invoke}.
Reads all eight 6.4.1 attributes with Attributes.value/2 (namelist
with Attributes.list/2, autoforward with Attributes.atom/4 against
"true"/"false", defaulting false per 6.4.1's own stated default) and
records every one's span in attribute_locations. Like <param>'s
deliberate expr/location pair, every mutually exclusive attribute pairing
6.4.1 names (type/typeexpr, src/srcexpr, id/idlocation,
namelist/<param>, and src|srcexpr/<content>) is lowered rather
than refused - Statifier.Validator.Checks.Invoke reports the shape.
<param> children accumulate into params, a <content> child sets
content, and a <finalize> child sets finalize - all three via
place_children/3's own %Invoke{} clauses below.
@spec build_log(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Log.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Log{} from a <log> element, tagged
{:content_node, log}.
Reads label and expr, both nilable, both raw strings - neither is
tokenized or compiled here.
@spec build_onentry(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:onentry, Statifier.Document.Block.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Block{} from an <onentry> element, tagged
{:onentry, block}.
Each <onentry> element becomes one Block with its own location -
three <onentry> elements under one <state> become three entries in
State.onentry, never one flattened list (spec 3.8/3.9, section 4's
error-isolation rule).
@spec build_onexit(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:onexit, Statifier.Document.Block.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Block{} from an <onexit> element, tagged
{:onexit, block}. See build_onentry/2 - same shared build_block/3,
same one-block-per-element rule.
@spec build_parallel(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:state, Statifier.Document.State.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.State{} with kind: :parallel from
<parallel>.
@spec build_param(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:param, Statifier.Document.Param.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Param{} from a <param> element, tagged
{:param, param}.
Reads name (required - spec 5.7.1), expr, and location. expr and
location are both read as raw nilable strings - neither is tokenized or
compiled here (Statifier.Document.Param's moduledoc forbids Predicator
under lib/statifier/document/, the same rule every other Document node
follows). A missing name means no struct can be built at all, following
build_raise/2's required-attribute pattern:
{nil, [Error.missing_attribute("param", "name", element.location)]}.
@spec build_raise(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Raise.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Raise{} from a <raise> element, tagged
{:content_node, raise}.
event is read as a single unsplit string
(Statifier.Document.Raise's moduledoc) - deliberately not tokenized the
way <transition>'s event is. event is required (:event is
@enforce_keys'd on Raise); when absent, no struct can be built and this
returns {nil, [%Error{reason: {:missing_attribute, "raise", "event"}}]}.
@spec build_script(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Script.t()} | nil, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Script{} from a <script> element (spec
5.8), tagged {:content_node, script}.
On a written src, no struct is built at all: this reports
{:unsupported_attribute, "script", "src"} (Decision 5, ADR-0026 decision
2 - no external fetch) and stops there, following build_raise/2's
"cannot build" pattern. src plus child text is deliberately not a
second error - spec 5.8.2 forbids the pair, but the document is already
rejected on src alone, so a second error would add noise without adding
information.
Otherwise text is set to Statifier.Parser.DOM.text/1's verbatim,
untrimmed concatenation of <script>'s direct text children (verbatim
except for the parser's XML 1.0 2.11 line-break fold - ADR-0045) - the
predicator program body (5.8.2). This builder reads element.children
directly rather than calling Statifier.Lowering.walk_children/2, the
same stray-text exemption build_data/2/build_assign/2 take for their
own text-only content models. An element child is not silently dropped:
each one produces {:misplaced_element, name, "script"} instead of a
build attempt of its own - <script> does not hold a markup subtree.
@spec build_scxml(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {Statifier.Document.t(), [Statifier.Lowering.Error.t()]}
Builds the %Statifier.Document{} root from <scxml>.
Reads initial (whitespace-split), name, datamodel, binding (atom,
default :early), version, and xmlns. <state>, <parallel>, and
<final> children are placed into Document.states, in document order;
any other child misses Document's one slot and is reported via place/3
as {:misplaced_element, name, "scxml"}.
@spec build_send(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:content_node, Statifier.Document.Send.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Send{} from a <send> element (spec 6.2),
tagged {:content_node, send} - unlike <invoke>, <send> is executable
content, so it lands in a block's content list through the existing
{:content_node, _} placement clauses rather than a dedicated state-level
slot.
Reads all eleven of 6.2.1's own attributes, each raw and nilable
(Statifier.Document.Send's moduledoc: mutually exclusive pairs are
simultaneously representable so the validator can report the shape).
<param> and <content> children are placed onto Send.params/.content
by place/3's own %Send{} clauses, mirroring %Invoke{}'s.
@spec build_state(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:state, Statifier.Document.State.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.State{} with kind: :state from <state>.
@spec build_transition(element :: Statifier.Parser.DOM.Element.t(), ctx :: map()) :: {{:transition, Statifier.Document.Transition.t()}, [Statifier.Lowering.Error.t()]}
Builds a %Statifier.Document.Transition{} from <transition>.
Reads event and target (both whitespace-split), cond (raw source
string), and type (atom, default :external). An out-of-range type
value lowers to :external and still keeps its attribute_locations
entry, so a future validator check can point at the offending text, the
same rule <history>'s type follows.
A <transition>'s executable content children (<raise>, <log>) are
placed directly into Transition.content, unwrapped - a transition has no
<onentry>-like element in the source to give a block its own location
(Statifier.Document.Block's moduledoc).