Statifier.Lowering (Statifier v2.15.0)

Copy Markdown View Source

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/2 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 child 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.

Only <scxml> may be the root

<scxml> is the one element that may be a document's root, and it is never a child. lower/2 builds it directly and refuses every other root name as {:unexpected_root, local_name}, whatever that name is, so a root such as <state> is refused rather than handed to a builder whose result has nowhere to go. <scxml> is not a key of the dispatch map either, so an <scxml> at any walked child site (a child that walk_child/4 looks up) misses the map and is reported as {:unsupported_element, name}, like any other name the map does not hold. A builder that reads its element children itself instead of walking them answers for an <scxml> there as for any other element child, as its own @doc states; for example, under <data> it is a {:misplaced_element, name, "data"}, and inside <content> or <assign> it is part of the opaque markup slice.

Relaxed input

Both sites that resolve an element's namespace (lower/2 for the root, walk_child/4 for each walked child) accept an element with no namespace as SCXML's own vocabulary, not only one resolved to the SCXML namespace; walk_child/4 is the one site that looks a name up in the dispatch map. 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

lower(root, source)

@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}, whether or not that name is legal as a child (<state>, <transition>, ...); an <scxml> at a walked child site is {:unsupported_element, name, location}.