Statifier.Lowering.Builders (Statifier v2.0.0)

Copy Markdown View Source

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

build_assign(element, ctx)

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

build_cancel(element, ctx)

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

build_content(element, ctx)

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

build_data(element, ctx)

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

build_datamodel(element, ctx)

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

build_donedata(element, ctx)

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

build_else(element, ctx)

@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/3 accepts 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.

build_elseif(element, ctx)

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

build_final(element, ctx)

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

build_finalize(element, ctx)

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

build_foreach(element, ctx)

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

build_history(element, ctx)

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

build_if(element, ctx)

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

build_initial(element, ctx)

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

build_invoke(element, ctx)

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

build_log(element, ctx)

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

build_onentry(element, ctx)

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

build_onexit(element, ctx)

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

build_parallel(element, ctx)

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

build_param(element, ctx)

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

build_raise(element, ctx)

@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"}}]}.

build_script(element, ctx)

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

build_scxml(element, ctx)

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

build_send(element, ctx)

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

build_state(element, ctx)

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

build_transition(element, ctx)

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