StatifierUI.Trace.Manifest (StatifierUI v0.9.1)

Copy Markdown View Source

Turns a compiled %Statifier.Machine{} plus caller-supplied context into the session.start definition message (docs/wire-format.md) - the message that makes every later index (state index, t_index, c_index, d_index) resolvable to a source location without a compiler on the reading end. Pure, no process.

The Machine does not retain the SCXML source text and has no id -> index reverse map beyond the partial id_to_index (deps/statifier/lib/statifier/machine.ex:29), so the state table is built by walking machine.states (a tuple in document order) rather than by any lookup, and source is accepted from the caller rather than derived (decision 7 of the plan).

The Content.Script/Content.Assign location gotcha

Every executable-content node kind carries a :location field holding its own %Statifier.Parser.Location{} span, with two exceptions this module handles explicitly rather than by a generic fallback:

  • Statifier.Machine.Content.Script has no :location field at all - its span lives on :node_location instead (deps/statifier/lib/statifier/machine/content/script.ex:48).
  • Statifier.Machine.Content.Assign has both fields, but its :location is a String.t() - the raw, uncompiled location attribute (a path expression such as "foo.bar"), not a location span. Its span is on :node_location (deps/statifier/lib/statifier/machine/content/assign.ex:42-46).

A naive Map.get(node, :location) || Map.get(node, :node_location) handles Script correctly (its :location key does not exist, so Map.get/2 returns nil and the fallback wins) but silently picks the wrong value for Assign - its :location is a truthy string, so the || never reaches :node_location, and a string ends up where a location object belongs. content_location/1 below dispatches on the struct itself instead, naming Assign and Script as the two kinds whose span is :node_location and treating every other kind's :location as the span it already is.

attribute_locations: key presence is the contract

A state row and a transition row each carry an attribute_locations object alongside the element-level location, built from the Machine field of the same name (Statifier.Machine.State, Statifier.Machine.Transition). It maps an attribute name to that attribute's own value span, and its value is not the interesting part: an entry exists only for an attribute the author actually wrote, so Map.has_key?(row["attribute_locations"], "type") is the "was type written" question that type's own value cannot answer once lowering has applied the :external default. The map is carried verbatim from the Document node through the Machine, so that contract survives the whole trip to the wire.

The object is always present and is %{} for an element that wrote no attributes at all, for the synthesized initial transition no author wrote, and for a Machine compiled by an engine old enough not to populate the field. Those three are indistinguishable on the wire and deliberately so: a consumer that finds no entry for the attribute it wants falls back to the row's element-level location, which is exactly the granularity the format offered before this field existed.

cond_location stays where it is. It is not folded into this object because it carries a fallback the raw map does not - the transition's own location when cond was written without a recorded span - so the two answer different questions. Prefer attribute_locations["cond"] for new work; read cond_location when the fallback is what is wanted.

Summary

Types

Caller-supplied context build/3 cannot derive from the Machine alone.

Functions

Builds the session.start message for machine, stamped session: session and seq: 0.

The session.start schema version this module produces - 1 (ADR-0005's initial value).

Types

opts()

@type opts() :: [
  source: String.t(),
  fixtures: map(),
  parent_session: String.t(),
  invokeid: String.t()
]

Caller-supplied context build/3 cannot derive from the Machine alone.

Functions

build(machine, session, opts \\ [])

@spec build(Statifier.Machine.t(), session :: String.t(), opts()) ::
  {:ok, StatifierUI.Trace.Message.t()} | {:error, term()}

Builds the session.start message for machine, stamped session: session and seq: 0.

Returns {:error, {:invalid_fixtures, opts[:fixtures]}} when :fixtures is supplied and is not a map, and {:error, {:invalid_source, opts[:source]}} when :source is supplied and is not a binary. Nothing else can fail: every other field comes from a compiled Machine, which is well-formed by construction.

version()

@spec version() :: pos_integer()

The session.start schema version this module produces - 1 (ADR-0005's initial value).