What a block type is entitled to know while emitting, and nothing more (ADR-0004 decision 4).
The compiler walks the document bottom-up and calls emit/2 with the
children already compiled. The context it passes carries four things:
block_idandstate_id- the block's own, precomputed;children- slot name to the ordered list of child summaries, each carrying onlyblock_id,state_idanddone_event. A child's emitted SCXML is deliberately absent: a parent that could read it would be a parent that could depend on it, and decision 2 exists to prevent that;document_id- for the rare type that names the document in a send target;role_id/2- decision 3's minting function, reached through this struct so the compiler owns the namespacing.
The palette is deliberately absent. A block type resolving another block
type would be a block type compiling its own children, which is the
compiler's job, and would make emit/2's purity depend on a value it did
not receive.
The "done" role is conventional, not reserved
Decision 2 makes every block's state compound with a <final> child, so
that entering the final raises done.state.<state id> and a parent needs
nothing but the child's id to wire it. done_id/1 mints that child's id
under the role "done"; the core vocabulary uses it uniformly. Nothing
stops a host type from minting the same role itself - it is the same id -
or from using a different one, so long as some final child is reachable.
A block whose state can never reach a final is a block no parent can
sequence after.
The o_ role namespace is reserved
A block type with more than one way to finish emits one <final> per
outcome it reaches (ADR-0004's outcome amendment). Those finals live in
the role namespace under the prefix "o_", they are minted only by
outcome_id/2, and role_id/2 refuses the prefix outright - without the
reservation an outcome final and a hand-minted role could produce the
same id and provenance could not say which it was.
Summary
Types
Everything a parent may know about one compiled child: its block id, the state it compiled to, and the event that state raises when it is done.
Functions
The ordered child summaries in slot, or [] for a slot with no
children - including one the document never wrote.
The done.state event this block's state raises, for a block type that
needs to name its own completion.
The id of this block's conventional <final> child - role_id(ctx, "done") with the error arm discharged, since "done" is a literal role
this module knows is valid.
Builds the context for block_id under document_id, with children
keyed by slot name in the order slots/1 declared them.
The completion event a parent wires on for one outcome:
done.outcome.<state id>.<outcome> (ADR-0004's outcome amendment, 2c).
Mints the <final> id for one declared outcome (ADR-0004's outcome
amendment, 2b)
Mints the id of an auxiliary state this block emits inside its own
state, under role (decision 3).
The summary of one compiled child, for a parent building a summary of its own or wiring a transition to a sibling.
Types
@type child_summary() :: %{ block_id: StatifierBlocks.Block.id(), state_id: StatifierBlocks.Compiler.StateId.t(), done_event: String.t() }
Everything a parent may know about one compiled child: its block id, the state it compiled to, and the event that state raises when it is done.
@type t() :: %StatifierBlocks.Compiler.Context{ block_id: StatifierBlocks.Block.id(), children: %{optional(StatifierBlocks.Block.slot_name()) => [child_summary()]}, document_id: StatifierBlocks.Document.id(), state_id: StatifierBlocks.Compiler.StateId.t() }
Functions
@spec children(t(), StatifierBlocks.Block.slot_name()) :: [child_summary()]
The ordered child summaries in slot, or [] for a slot with no
children - including one the document never wrote.
The done.state event this block's state raises, for a block type that
needs to name its own completion.
@spec done_id(t()) :: StatifierBlocks.Compiler.StateId.t()
The id of this block's conventional <final> child - role_id(ctx, "done") with the error arm discharged, since "done" is a literal role
this module knows is valid.
@spec new(StatifierBlocks.Block.id(), StatifierBlocks.Document.id(), %{ optional(StatifierBlocks.Block.slot_name()) => [child_summary()] }) :: t()
Builds the context for block_id under document_id, with children
keyed by slot name in the order slots/1 declared them.
@spec outcome_event(t(), String.t()) :: {:ok, String.t()} | {:error, {:invalid_outcome, StatifierBlocks.Block.id(), String.t()}}
The completion event a parent wires on for one outcome:
done.outcome.<state id>.<outcome> (ADR-0004's outcome amendment, 2c).
The tag rides on an event rather than on the final's identity, because
done.state.<state id> is generated whichever final is entered and a
parent can therefore not see which one it was. A parent that does not
care wires the prefix done.outcome.<state id> and matches every outcome
of that child; one that discriminates names the full event. Refuses a
malformed outcome name for the same reason outcome_id/2 does.
@spec outcome_id(t(), String.t()) :: {:ok, StatifierBlocks.Compiler.StateId.t()} | {:error, {:invalid_outcome, StatifierBlocks.Block.id(), String.t()}}
Mints the <final> id for one declared outcome (ADR-0004's outcome
amendment, 2b):
outcome_id(block_id, outcome) = "s_" <> block_id <> "__o_" <> outcomeThis is the only home for an outcome final's id. A block type with
more than one way to finish emits one <final> per outcome it reaches
and mints every one of them here rather than by string concatenation, for
decision 3's reason: the ids stay injective, unstate_id/1 still inverts
them, and provenance can still say which block a final came from.
outcome is a name in the role shape - ~r/\A[a-z][a-z0-9_]*\z/, no
"__" - and one that is not is refused with {:error, {:invalid_outcome, block_id, outcome}} rather than raising.
This return shape refines the amendment's sketch
2e writes the signature as returning a bare String.t(). It cannot,
and stay honest: 2f requires an :invalid_outcome Emit finding for a
name failing the role shape, and decision 1 forbids emit/2 raising,
so the error has to be reachable through a return value. The {:ok, _}
arm carries exactly the id 2e names.
@spec role_id(t(), StatifierBlocks.Compiler.StateId.role()) :: {:ok, StatifierBlocks.Compiler.StateId.t()} | {:error, {:invalid_role, StatifierBlocks.Block.id(), StatifierBlocks.Compiler.StateId.role()}} | {:error, {:reserved_role, StatifierBlocks.Block.id(), StatifierBlocks.Compiler.StateId.role()}}
Mints the id of an auxiliary state this block emits inside its own
state, under role (decision 3).
Returns {:error, {:invalid_role, block_id, role}} for a role the
compiler could not invert; a block type that passes a literal role never
sees that arm, and one that builds a role from config handles it as the
ordinary Emit finding it is.
A role beginning with "o_" is refused with {:error, {:reserved_role, block_id, role}}: that namespace belongs to outcome finals and
outcome_id/2 is the only way into it.
@spec summary(StatifierBlocks.Block.id()) :: child_summary()
The summary of one compiled child, for a parent building a summary of its own or wiring a transition to a sibling.