Appendix D's exit and entry blocks, ported function for function
(ADR-0002) - the half of the algorithm that changes the configuration.
Statifier.Interpreter.Selection answers "which transitions fire and
what would they leave"; this module actually leaves and enters states.
machine_state stands in for the pseudocode's globals exactly as
Statifier.Interpreter.Selection's moduledoc states for the same reason
(docs/observability.md constraint 1) - every function here that reads a
global takes machine_state as its first argument.
Return shapes
exit_states/2 and enter_states/2 are the two functions here that both
mutate the position and emit effects, so both return
{MachineState.t(), [Effect.t()]} - the shape docs/observability.md
fixes for microstep/1. Statifier.Interpreter.microstep/2 is their
caller for the ordinary exit/entry path. Effect order within the returned
list is emission order: the trace effect first, then each block's effects
in the order the blocks ran.
run_onexit_blocks/2 and donedata/2 are public for a second caller:
Statifier.Interpreter.exit_interpreter/1's termination walk needs
exactly the same per-state onexit body and the same <donedata> folding
rule that depart/2 and raise_parent_completion/3 already use here, so
this module owns both rather than the interpreter carrying a second
copy.
Ordering
Exit order and entry order are never hand-sorted here: exit_states/2
pipes Statifier.Interpreter.Selection.compute_exit_set/2's MapSet
through Statifier.Machine.exit_order/2 (descending index), and
enter_states/2 mirrors it through Statifier.Machine.document_order/2
(ascending index).
The content seam
Every call site that would run executable content - <onexit>,
<onentry>, an <initial> transition's content, default history
content - goes through the private execute_block/3 seam, which
delegates to Statifier.Interpreter.Content.execute_block/3: that module
owns the block runner and the Trace.ContentExecuted emission that wraps
it. This module owns only where and in what order blocks run, which is
what the exit/entry pseudocode defines.
History recording
exit_states/2 records history in a full first pass over the exit set,
reading the configuration exactly as it stood before any state exited -
before the first onexit runs and before any state is removed. The
pseudocode writes two consecutive for s in statesToExit loops for
exactly this reason: recording inside the delete loop would let a
later-exiting state's deep history see a configuration its own siblings
have already been removed from.
One mechanical deviation in the recorded value itself: the pseudocode's
configuration.toList().filter(f) produces an ordered list, while
machine_state.history_values holds a MapSet. Order is dropped rather
than preserved because nothing reads it - add_descendant_states_to_enter/3
re-enters a restored value through the same descendant and ancestor walks
any other target set goes through, and enter_states/2 sorts the whole
entry set into document order afterwards regardless.
The entry set
compute_entry_set/2, add_descendant_states_to_enter/3, and
add_ancestor_states_to_enter/4 are pure queries: none of them touch
machine_state.configuration, none of them emit an effect. The
pseudocode's three statesToEnter / statesForDefaultEntry /
defaultHistoryContent out-parameters become one accumulator of type
entry_set(), threaded in and returned last - the minimal mechanical
deviation Elixir's lack of out-parameters forces. default_history_content
stores the default transition's t_index rather than its [c_index] list:
strictly more information, and what the content seam actually needs to
build a Content.owner().
Summary
Types
The pseudocode's three computeEntrySet out-parameters, threaded as one
accumulator: the states a transition set will enter, the subset of those
entered via a compound state's <initial> declaration rather than as an
explicit target (so default-entry content runs only for them), and a map
from a history state's parent to the t_index of the default transition
whose content runs because that history was unrecorded.
Functions
addAncestorStatesToEnter (Appendix D) - every proper ancestor of
state_index up to but excluding ancestor is added, each parallel
ancestor also pulling in its uncovered regions.
addDescendantStatesToEnter (Appendix D) - state_index and, depending on
its kind, the descendants entering with it.
for inv in s.invoke: cancelInvoke(inv) (Appendix D) - one
{:cancel_invoke, %Effect.CancelInvoke{}} per live invocation of
state_index: walk its compiled invoke list in document order, and for
each {state_index, invoke_index} found in machine_state.active_invocations
emit a cancel and delete the entry. An invocation whose arguments failed
(ADR-0031), and one whose resolved type was unsupported (6.4.1), never
reached active_invocations, so neither produces a cancel.
computeEntrySet (Appendix D) - the entry-set bookkeeping every
enabled_transitions transition contributes, folded into one
entry_set() accumulator.
returnDoneEvent's s.donedata argument (Appendix D) - state_index's
<donedata>, folded to the value a raised or returned done.* event
carries as data.
enterStates (Appendix D) - the entry-ordered set compute_entry_set/2
computes, added to the configuration with onentry, default-entry, and
default-history content run per state, then completion events raised.
exitStates (Appendix D) - the exit-ordered set of states
enabled_transitions leave, with history recorded before any state
exits and onexit content run per state in exit order.
isInFinalState (Appendix D) - whether state_index is, itself or
through its active descendants, "in a final state": a compound state
answers true when some active child is a :final; a parallel answers
true only when every region does, recursively; anything else (an
atomic :final included) answers false. Ported verbatim.
exitStates's per-state for content in s.onexit: executeContent(content)
(Appendix D) - state_index's onexit blocks, in document order, each
through the execute_block/3 seam with {:onexit, state_index, ordinal}
Types
@type entry_set() :: {states_to_enter :: MapSet.t(non_neg_integer()), states_for_default_entry :: MapSet.t(non_neg_integer()), default_history_content :: %{ optional(non_neg_integer()) => non_neg_integer() }}
The pseudocode's three computeEntrySet out-parameters, threaded as one
accumulator: the states a transition set will enter, the subset of those
entered via a compound state's <initial> declaration rather than as an
explicit target (so default-entry content runs only for them), and a map
from a history state's parent to the t_index of the default transition
whose content runs because that history was unrecorded.
Functions
@spec add_ancestor_states_to_enter( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer(), ancestor :: non_neg_integer() | nil, acc :: entry_set() ) :: entry_set()
addAncestorStatesToEnter (Appendix D) - every proper ancestor of
state_index up to but excluding ancestor is added, each parallel
ancestor also pulling in its uncovered regions.
Machine.proper_ancestors/2 returns all ancestors up to the :scxml
root (no two-argument bound), so the pseudocode's
getProperAncestors(state, ancestor) is expressed as
Enum.take_while(&(&1 != ancestor)) over the one-argument helper. A
nil ancestor (a targetless transition's domain, or a direct call with
no bound) therefore correctly takes every ancestor, since nil never
appears in proper_ancestors/2's result.
@spec add_descendant_states_to_enter( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer(), acc :: entry_set() ) :: entry_set()
addDescendantStatesToEnter (Appendix D) - state_index and, depending on
its kind, the descendants entering with it.
Four cases, matching the pseudocode's own branches:
- history (
enter_history_target/3): a recorded value inmachine_state.history_valuesrestores those states; an unrecorded one registersdefault_history_contentand follows the history's default transition's targets instead. - compound:
state_indexis added, flagged instates_for_default_entry, and its initial targets are entered (enter_initial_targets/3). When the document wrote an<initial>element, the targets come fromMachine.transition(machine, state.initial_transition).targets- the pseudocode'sstate.initial.transition.target- because that transition is also what carries the default-entry contentrun_default_entry/3runs on the way in. Mechanical deviation:Statifier.Compiler.resolve_initial/3only populatesinitial_transitionfor a written<initial>element; a state defaulted through theinitialattribute or the first-child fallback carries its resolved default inState.initialinstead, with no synthesized transition (there being no<initial>content to run in that case either).enter_initial_targets/3readsinitial_transitionwhen set and falls back toState.initialotherwise, rather than asserting non-nil. - parallel:
state_indexis added and each child region not already covered by astates_to_enterdescendant is entered (enter_uncovered_regions/3). - atomic / final / anything else:
state_indexis added and nothing else happens.
@spec cancel_invocations_for_state( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer() ) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
for inv in s.invoke: cancelInvoke(inv) (Appendix D) - one
{:cancel_invoke, %Effect.CancelInvoke{}} per live invocation of
state_index: walk its compiled invoke list in document order, and for
each {state_index, invoke_index} found in machine_state.active_invocations
emit a cancel and delete the entry. An invocation whose arguments failed
(ADR-0031), and one whose resolved type was unsupported (6.4.1), never
reached active_invocations, so neither produces a cancel.
Two callers, both after that state's own onexit blocks and before it
leaves the configuration - both the pseudocode's own order in exitStates
and exitInterpreter, and spec 6.4's "the cancel operation MUST act as if
it were the final <onexit> handler in the invoking state": depart/2
(this module's own exit_states/2 path) and
Statifier.Interpreter.exit_interpreter/1 (the termination walk), the
same second-caller shape run_onexit_blocks/2 above already has.
@spec compute_entry_set( machine_state :: Statifier.MachineState.t(), transitions :: [Statifier.Machine.Transition.t()] ) :: entry_set()
computeEntrySet (Appendix D) - the entry-set bookkeeping every
enabled_transitions transition contributes, folded into one
entry_set() accumulator.
Per transition, in the pseudocode's own order: each written target
(transition.targets, not the resolved history/effective set) through
add_descendant_states_to_enter/3, then the transition's domain
(Selection.get_transition_domain/2), then each effective target
(Selection.get_effective_target_states/2 - already resolves both
history branches, consumed rather than re-resolved here) through
add_ancestor_states_to_enter/4 bounded by that domain. The
written-versus-effective distinction is the pseudocode's own and is
preserved here rather than collapsed into one walk.
@spec donedata( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer() ) :: {Statifier.MachineState.t(), term()}
returnDoneEvent's s.donedata argument (Appendix D) - state_index's
<donedata>, folded to the value a raised or returned done.* event
carries as data.
- No
<donedata>at all, or a%Donedata{expr: nil, params: []}(no<content>or<param>child at all) -{machine_state, :undefined}: "no data", the same spelling every other absent-payload writer uses (docs/adr/0037-unbound-spelled-undefined-at-the-writer.md). {:static, text}-<content>'s text body, which can only originate there (Statifier.Compiler.build_content_expr/2). Coerced throughStatifier.EventData.coerce({:text, text})(Decision 3 of the plan on this module) so<content>21</content>becomes the integer21(W3C test529), not the string"21".{:compiled, _, _}-<content expr>. Evaluated against a freshStatifier.Evaluator.context/1; success is coerced throughEventData.coerce({:value, v})(identity - an evaluated expression already produced a legal data value, so it is never re-run through the text ladder). Failure raiseserror.executionviaMachineState.raise_platform/4and returns:undefineddonedata - not the empty string spec 5.6 names for<content>generally (ADR-0021 records that deviation and its scope), and notnileither: a failed evaluation produced no data, it did not produce a null value (W3C test528).expr: nilwith a non-emptyparamslist -<param>children, folded against oneStatifier.Evaluator.context/1built once for the whole fold. Each param is evaluated in document order; a failure raises its ownerror.execution(spec 5.7: "If the evaluation of 'expr' produces an error, or if neither 'location' nor 'expr' is present, or if the value of 'location' is not a valid location for a data value in the underlying data model, the SCXML Processor MUST place the error 'error.execution' in the internal event queue and MUST ignore the name and value") and is dropped rather than aborting the remaining params. The surviving{name, value}pairs, still in document order, go throughEventData.coerce({:params, pairs}), which returns:undefinedfor an empty result (Statifier.EventData's own moduledoc, Decision 5) - so a<donedata>whose only param fails produces:undefineddata, the same shape as no donedata at all, not%{}. The<content>arm above and this params arm are mutually exclusive by validator check (Statifier.Validator.Checks.Donedata), so there is no case where both are non-empty.
Two callers: raise_parent_completion/3 (a non-top-level final's
done.state.*) and Statifier.Interpreter.exit_interpreter/1 (a
top-level final's terminal {:done, _} effect) - the same folding rule
either way. <donedata> carries no c_index
(lib/statifier/machine/content.ex:25-28), so
Statifier.Interpreter.Content.raise_execution_error/4's {:content, c_index, owner} origin shape does not apply here; the raise below uses
{:state, state_index} instead, the same origin shape
raise_parent_completion/3 already stamps on the done.state.* event
itself - for both the <content> arm and each failing <param>.
@spec enter_states( machine_state :: Statifier.MachineState.t(), enabled_transitions :: [Statifier.Machine.Transition.t()] ) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
enterStates (Appendix D) - the entry-ordered set compute_entry_set/2
computes, added to the configuration with onentry, default-entry, and
default-history content run per state, then completion events raised.
Body, in the pseudocode's own order:
compute_entry_set/2overenabled_transitions.entry_order=Machine.document_order/2overstates_to_enter- ascending index, the mirror ofexit_states/2'sexit_order.entry_orderis reduced througharrive/3, each state added to the configuration, itsonentryblocks run, its default-entry / default- history content run when flagged/registered, then its completion events raised.Effect.trace(pre_entry_state, Effect.Trace.EntrySet, indexes: entry_order, configuration: machine_state.configuration)is emitted last:macrostep/microstep/roundare stamped againstpre_entry_state, captured asmachine_statestood before step 3'sarrive/3reduce (ADR-0012, mirroringexit_states/2), whileconfigurationis read from the post-entrymachine_statebecause "the configuration after this entry set was applied" does not exist until step 3 has run. The payload's position in the returned list is unchanged: it is still first, since the list is concatenated at the end either way.
@spec exit_states( machine_state :: Statifier.MachineState.t(), enabled_transitions :: [Statifier.Machine.Transition.t()] ) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
exitStates (Appendix D) - the exit-ordered set of states
enabled_transitions leave, with history recorded before any state
exits and onexit content run per state in exit order.
Body, in the pseudocode's own order:
exit_set=Selection.compute_exit_set/2overenabled_transitions- an unordered
MapSet.
- an unordered
states_to_invokeloses every member ofexit_set, over that same unordered set -statesToInvoke.delete(s)(Appendix D), which runs before the exit-order sort and beforerecord_history_values/2.exit_setis sorted into exit order viaMachine.exit_order/2intostates_to_exit:compute_exit_set/2returns unordered, and ordering it is this function's job, notSelection's.record_history_values/2runs its whole first pass overstates_to_exit, reading the untouched configuration.states_to_exitis reduced throughdepart/2, each state running itsonexitblocks and then leaving the configuration.Effect.trace(pre_exit_state, Effect.Trace.ExitSet, indexes: states_to_exit, configuration: machine_state.configuration)is emitted last:macrostep/microstep/roundare stamped againstpre_exit_state, captured asmachine_statestood after step 2'sstates_to_invokeupdate and before the departure reduce (ADR-0012 -test/fixtures/adr_judge/0012_trace_prestate_captured.diffis the sanctioned shape), whileconfigurationis read from the post-departuremachine_statebecause "the configuration after this exit set was applied" does not exist until step 5 has run. The payload's position in the returned list is unchanged: it is still first, since the list is concatenated at the end either way.
@spec in_final_state?( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer() ) :: boolean()
isInFinalState (Appendix D) - whether state_index is, itself or
through its active descendants, "in a final state": a compound state
answers true when some active child is a :final; a parallel answers
true only when every region does, recursively; anything else (an
atomic :final included) answers false. Ported verbatim.
@spec run_onexit_blocks( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer() ) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
exitStates's per-state for content in s.onexit: executeContent(content)
(Appendix D) - state_index's onexit blocks, in document order, each
through the execute_block/3 seam with {:onexit, state_index, ordinal}
ordinalis the block's position in the state's ownonexitlist, exactly whatMachine.Content.owner()documents it to be.
Two callers: depart/2 (this module's own exit_states/2 path) and
Statifier.Interpreter.exit_interpreter/1 (the termination block, which
runs every active state's onexit blocks the same way, in exit order,
without the rest of exitStates's history-recording and configuration
bookkeeping).