The second arrow of the parser pipeline: a generic
%Statifier.Parser.DOM.Element{} tree in, a typed %Statifier.Document{}
tree out (docs/architecture.md, "Parser: DOM first, then lowering").
The accumulator contract
lower/2 performs one traversal. Every builder in
Statifier.Lowering.Builders lowers its children first via
walk_children/2, appending each child's errors to its own in document
order; lower/2 sorts the whole accumulated list by
location.start_offset before returning, so the report reads in document
order regardless of the order a parent happened to emit its own error
relative to its children's.
ctx also carries :source - the source binary being lowered, seeded by
lower/2 and copied forward unchanged by walk_child/4 to every builder
in the tree. build_content/2 is the one builder that reads it, to slice
<content>'s markup children (ADR-0041) back out of the original bytes.
Lowering still never re-parses anything; slicing only reads spans it
already has.
A builder that cannot place or build something still returns the best
partial result it can (or nothing, when it cannot build one at all) so the
walk keeps going and later errors are still found - but that partial tree
is never handed back to the caller. lower/1 returns {:ok, document}
only when the accumulated error list is empty; any non-empty list means
{:error, errors}, full stop, regardless of how much of the tree built
successfully. Handing back a document that lowering itself does not trust
would just move the "is this actually usable" question onto every caller.
Dispatch is context-free
The dispatch map's keys are exactly the supported element names; no
build_* function in Builders takes a parent element name as an
argument. A builder does not know or care what its parent was - the
parent, when it exists, is what decides whether the child's tagged
result has anywhere to go (Builders.place/3). The
parent's own element name is known only to itself, and is used solely to
word a {:misplaced_element, name, parent_name} error about one of its
own children - it is never handed down to a child builder.
Relaxed input
Both dispatch sites (lower/1, walk_child/4) accept an element with no
namespace as SCXML's own vocabulary, not only one resolved to the SCXML
namespace. Statifier.Lowering.Namespace.scxml_vocabulary?/1 is the
mechanism and its moduledoc states the commitment; this is a pointer for
the reader who starts here instead.
Summary
Functions
Lowers a generic %Statifier.Parser.DOM.Element{} tree - the parsed
<scxml> root - into a typed %Statifier.Document{} tree, in one
traversal that dispatches every child through Statifier.Lowering.Builders
by element name. source is the same source binary
Statifier.Validator.validate/2 takes; it is seeded into ctx under
:source for the one builder that slices bytes out of it.
Functions
@spec lower(root :: Statifier.Parser.DOM.Element.t(), source :: binary()) :: {:ok, Statifier.Document.t()} | {:error, [Statifier.Lowering.Error.t()]}
Lowers a generic %Statifier.Parser.DOM.Element{} tree - the parsed
<scxml> root - into a typed %Statifier.Document{} tree, in one
traversal that dispatches every child through Statifier.Lowering.Builders
by element name. source is the same source binary
Statifier.Validator.validate/2 takes; it is seeded into ctx under
:source for the one builder that slices bytes out of it.
Returns {:ok, document} only when the whole walk accumulated no errors;
any error at all, anywhere in the tree, produces {:error, errors} with
the errors sorted into document order - never a partial document, even
when most of the tree built successfully. An element outside the SCXML
vocabulary (and not using the relaxed no-namespace fallback) is reported
as {:foreign_element, name, uri, location}; a non-<scxml> root name is
{:unexpected_root, local_name, location}.