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: thedone.statetransition 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/2records that.from_config/2records 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. Acondbuilt verbatim from an author's:expressionfield 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/3records 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.
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.
Builds an element.
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
@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.
@type node_t() :: t() | {:child, StatifierBlocks.Block.id()}
A child of an element: another element, or a placeholder for a compiled child block.
Functions
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.
@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.
@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.
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.
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.