Statifier.Lowering (Statifier v2.0.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/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

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