StatifierBlocks.Emission (StatifierBlocks v0.1.0)

Copy Markdown View Source

One SCXML subtree, structurally (ADR-0004 decision 4).

emit/2 returns one of these, never a string. A block type building XML text would own escaping, namespace handling and attribute-value normalization, all easy to get subtly wrong, none of them a block-type author's business, and all of them - per st-ADR-0052's whitespace sensitivity - able to change chart identity by accident. The compiler serializes the whole tree once, at the end, through StatifierBlocks.Compiler.Serializer.

Attributes are a sorted list, not a map

attributes is a list of {name, value} pairs kept in sorted order by element/3, because ADR-0004 decision 6 forbids iterating a bare map anywhere in the pipeline: two maps that are == can enumerate in different orders, and the serializer's output is identity-bearing. Sorting at construction rather than at serialization means the sort happens once, where the pairs are known.

Children, and the child placeholder

A parent never receives its children's emitted SCXML (decision 4: a parent that could read it would be a parent that could depend on it). What it receives is a summary - block id, state id, done event - and what it emits in a child's place is child_ref/1, a placeholder naming that child's block id. The compiler splices each child's own emission in afterwards.

That is what buys decision 6's per-block byte stability: a parent's own bytes are a function of its config and its children's ids, never of their contents, so an unedited subtree compiles to unchanged bytes even when a sibling changes.

Provenance hints (ADR-0004 decision 5)

Two of the three fields below carry no bytes at all. They are hints the compiler reads while building the provenance map, and the serializer never writes them:

  • owner - an attribution judgment. By default every element a block emits belongs to that block, which is right almost everywhere. ADR-0004 decision 5 names the exceptions: the done.state transition a sequence emits belongs to the child it leaves, because "what happens after the authorize step" is the fact an author would recognise. attributed_to/2 records that. from_config/2 records the other half - an element written out of one config field, so a finding against it is the author's rather than a bug.
  • attribute_owners - the same, one attribute value at a time. A cond built verbatim from an author's :expression field is the motivating case: the element is the block type's, the attribute value is the author's, and only the second should carry a config key. attribute_from_config/3 records it.

A hint is never required. A block type that sets none gets the default attribution, which is what every leaf wants.

Summary

Types

An attribution hint, and - once StatifierBlocks.Compiler.Attribution.stamp/3 has run over the tree - the resolved StatifierBlocks.Provenance.owner/0 itself, which is this type with block_id known.

A child of an element: another element, or a placeholder for a compiled child block.

t()

Functions

Records that one attribute's value came verbatim from the config field config_key, leaving the element itself attributed as it was.

Attributes emission and everything under it to block_id rather than to the block that emitted it (ADR-0004 decision 5).

A placeholder for the compiled emission of the child block block_id.

Records that emission was written out of the config field config_key, so a finding landing inside it is the author's rather than a bug.

Types

hint()

@type hint() :: %{
  block_id: StatifierBlocks.Block.id() | nil,
  role: String.t() | nil,
  config_key: String.t() | nil
}

An attribution hint, and - once StatifierBlocks.Compiler.Attribution.stamp/3 has run over the tree - the resolved StatifierBlocks.Provenance.owner/0 itself, which is this type with block_id known.

block_id is nil for "the block that emitted this", which is the default and the common case. role is never set by a block type: the compiler derives it from the state ids the block minted, because a block type naming its own role twice - once in an id and once in a hint - is a place for the two to disagree.

node_t()

@type node_t() :: t() | {:child, StatifierBlocks.Block.id()}

A child of an element: another element, or a placeholder for a compiled child block.

t()

@type t() :: %StatifierBlocks.Emission{
  attribute_owners: [{String.t(), String.t()}],
  attributes: [{String.t(), String.t()}],
  children: [node_t()],
  name: String.t(),
  owner: hint() | nil
}

Functions

attribute_from_config(emission, attribute, config_key)

@spec attribute_from_config(t(), String.t(), String.t()) :: t()

Records that one attribute's value came verbatim from the config field config_key, leaving the element itself attributed as it was.

This is the finer of the two grains, and the one the fault split (ADR-0004 decision 9) actually turns on: an upstream finding whose location falls inside the value is the author's typo, while one against the element around it is not. A config_key for an attribute the element does not carry is dropped, so an optional attribute can be annotated unconditionally.

attributed_to(emission, block_id)

@spec attributed_to(t(), StatifierBlocks.Block.id()) :: t()

Attributes emission and everything under it to block_id rather than to the block that emitted it (ADR-0004 decision 5).

Reserved for the cases the record names, where the block an author would recognise is not the block whose emit/2 wrote the element. Attributing an element to a block that is not in this document at all is a bug in the block type, and the compiler reports it as an Emit finding rather than writing an owner nothing can resolve.

child_ref(block_id)

@spec child_ref(StatifierBlocks.Block.id()) :: {:child, StatifierBlocks.Block.id()}

A placeholder for the compiled emission of the child block block_id.

The compiler replaces it with that child's own subtree; a placeholder naming a block the compiler did not compile in this slot is a compiler bug, not a document error, and StatifierBlocks.Compiler reports it as one rather than emitting a hole.

element(name, attributes \\ [], children \\ [])

@spec element(String.t(), [{String.t(), String.t() | nil}], [node_t()]) :: t()

Builds an element.

attributes is a list of {name, value} pairs; it is sorted by name here, and an attribute whose value is nil is dropped, so a caller can write an optional attribute without a conditional around it.

from_config(emission, config_key)

@spec from_config(t(), String.t()) :: t()

Records that emission was written out of the config field config_key, so a finding landing inside it is the author's rather than a bug.