Datamodel creation and early/top-level binding - the interpret preamble
hook (Statifier.Interpreter.initialize/2).
No Appendix D procedure body to port
initializeDatamodel/initializeDataModel have no procedure body
anywhere in Appendix D. A case-insensitive grep over
spec-cache/appendix-d.txt returns exactly two hits, both call sites, and
they do not even agree with each other: :102, inside interpret, calls
initializeDatamodel(datamodel, doc); :312, inside enterStates, calls
initializeDataModel(datamodel.s, doc.s) - different capitalization,
different arity meaning (whole-document vs. per-state). new Datamodel(doc)
(:101) is never defined either. So there is nothing to diff this module's
bodies against; ADR-0002's port obligation here is the two call sites
(this module's initialize/1, and Phase 5's enter_state/2), not a
pseudocode block. This module's semantics instead come from the prose of
clauses 5.3.2, 5.3.3, and B.2.2:
- 5.3.2 (environment override, scoped to top-level): "The SCXML
Processor MUST use any values provided by the environment at
instantiation time in place of those contained in the top-level
<data>elements. (Top-level data elements are those that are children of the<datamodel>element that is a child of<scxml>)." - 5.3.2 (the failure clause): "If the value specified for a
<data>element (by 'src', children, or the environment) is not a legal data value, the SCXML Processor MUST raise place error.execution in the internal event queue and MUST create an empty data element in the data model with the specified id." ("MUST raise place" is the REC's own typo.) Anexprfailure is 5.9.3's case instead: "If a value expression does not return a legal data value, the SCXML Processor MUST place the error 'error.execution' in the internal event queue." - 5.3.3 (binding): "When 'binding' is assigned the value "early" (the
default), the SCXML Processor MUST create all data elements and assign
their initial values at document initialization time. When 'binding' is
assigned the value "late", the SCXML Processor MUST create the data
elements at document initialization time, but MUST assign the specified
initial value to a given data element only when the state that contains
it is entered for the first time, before any
<onentry>markup." - B.2.2 (no ordering dependencies): "Ordering dependencies between
<data>elements are not permitted... the SCXML Processor MUST evaluate all<data>elements at initialization time but MAY do so in any order it chooses."
Algorithm (initialize/1)
env_ids = MapSet.new(Map.keys(machine_state.datamodel)), captured first, before any seeding -MachineState.new/2's only writers ofdatamodelat this point are the:datamodeloption andSystemVariables.initial/2, so key presence here is definitionally "provided by the environment at instantiation time" (5.3.2's own phrase).- Seed:
Map.put_new(datamodel, id, :undefined)for every%Machine.Data{}inmachine.data_elements, regardless of binding - 5.3.3 makes creation unconditional under both bindings, and writing:undefineddirectly means a seeded-but-unbound id answers=== undefined/!== undefinedrather than raisingUndefinedVariableError(Decision 3). - Bind, in ascending
d_indexorder: under:early, everyd_index; under:late, only the root state's own (Machine.at(machine, 0).data)- the top-level
<data>, which 5.3.3's per-state deferral has no state to defer to (Decision 7, and this module's owninitialize/1@docbelow). A top-leveld_indexwhose id is inenv_idsis skipped entirely, leaving its seeded:undefinedin place - Decision 1's environment-wins rule. A state-scoped<data>is never skipped this way: 5.3.2 scopes the override to top-level only.
- the top-level
Binding a d_index: {:invalid, error} (a <data expr> that failed to
compile, Decision 2) and {:src, uri} (never fetched, ADR-0003) both
short-circuit straight to the failure branch with no evaluation attempted;
any other Machine.expr() goes through Statifier.Evaluator.evaluate/2
and takes the failure branch on {:error, _}. Either way the id keeps the
:undefined step 2 already wrote - 5.3.2's "MUST create an empty data
element" is satisfied by not overwriting the seed, never by writing
:undefined after an error (Decision 3) - and
MachineState.raise_platform(machine_state, "error.execution", {:data, d_index}, data: reason) runs: raise_platform/4, not raise_internal/4,
because spec 5.10.1 classifies error.* as a platform event, matching the
two existing raise sites
(Statifier.Interpreter.Content.raise_execution_error/4,
Statifier.Interpreter.Selection.raise_cond_errors/2).
Summary
Functions
enterStates's per-state datamodel step (Appendix D :312, prose in the
moduledoc's "no Appendix D procedure body" section above) - binds
state_index's own <datamodel> (Machine.at(machine, state_index).data)
under binding == :late.
interpret's datamodel preamble (Appendix D :101-102, prose above), plus
the top-level half of enterStates' per-state binding (:312) that late
binding would otherwise defer forever - see the moduledoc's "no Appendix D
procedure body" section and spec 5.3.3's binding rule for why
the if doc.binding == "early": guard that wraps the interpret call site
in the pseudocode moves off that call site and into this function instead:
a top-level <data> is contained in no state, so under binding="late" it
never reaches enterStates' statesToEnter loop at all, and deferring its
binding to a state entry that never happens would leave it permanently
unassigned - a spec violation, not a conforming late-binding delay.
Writes value at path_source (a raw, uncompiled path expression such as
"foo.bar.baz" or "items[0]") against machine_state's datamodel,
rebinding the written root into datamodel_context for callers that thread
a context across a block (ADR-0028). Extracted from
Statifier.Machine.Content.Assign's defimpl - see that module's moduledoc
for the vivification and system-variable reasoning this function's five
steps carry out unchanged; this is a pure mechanics move, not a new
decision.
Functions
@spec enter_state( machine_state :: Statifier.MachineState.t(), state_index :: non_neg_integer() ) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
enterStates's per-state datamodel step (Appendix D :312, prose in the
moduledoc's "no Appendix D procedure body" section above) - binds
state_index's own <datamodel> (Machine.at(machine, state_index).data)
under binding == :late.
This function does not itself test "is this the first time state_index
has been entered" - Appendix D's s.isFirstEntry (appendix-d.txt:312).
Statifier.Interpreter.ExitEntry.arrive/3 is the only caller, and it tests
membership in MachineState.entered_states (the ADR-0002 substitute for
s.isFirstEntry, documented at that field) before calling this function,
so by the time this function runs, "first entry" has already been decided.
Splitting it this way keeps this module free of MachineState.configuration/
entered_states mutation, which arrive/3 already owns for every other
step of the same pseudocode body.
A no-op, returning {machine_state, []} unchanged, in three cases: under
binding == :early (every d_index was already bound at initialize/1,
before any state was entered), under binding == :late on a state whose
own <datamodel> is empty (data == [] - nothing to bind), and on
state_index == 0 for the reason below.
Why state_index == 0 is a no-op
Index 0 is the document root, and it is in configuration like any other
state, so arrive/3 calls this function for it. But the root's own data
list is the top-level data list - the <datamodel> that is a child of
<scxml> - which initialize/1 has already bound (Decision 3: top-level
<data> binds at initialization under both bindings, since 5.3.3's "the
state that contains it" has no answer for a <data> contained in no
state). Binding it a second time here is duplication, and not harmless
duplication: this function deliberately applies no environment override,
so the second pass would overwrite an environment-supplied value with the
document's own and violate 5.3.2's
"The SCXML Processor MUST use any values provided by the environment
at instantiation time in place of those contained in the top-level
<data> elements."Only binding == "late" reaches this path at all, and only a document
whose environment seeds a top-level id observes it - which is why no
corpus file catches it. Spec 6.4.3 makes it reachable in practice: an
invoked session's <param>/namelist values arrive as exactly this
environment seed, so without this guard a late-bound invoked child would
silently discard the values its parent passed it - the child-session half
of <invoke> that would deliver them does not exist yet, per
docs/architecture.md's "Sessions and invoke" section.
Otherwise: one Evaluator.context/1 for state_index's whole data list
(B.2.2's "no ordering dependencies" licenses this exactly as it does in
initialize/1), each d_index bound through the same bind_value/4 this
module's initialize/1 uses - same seeded-:undefined-on-failure,
raise_platform/4 shape, Decision 2/3 unchanged, and the same
{:datamodel_change, %Effect.DatamodelChange{}} emission per successful
binding (decision 3). There is no environment override on this
path: Decision 1 scopes that skip to top-level <data> only, and a
state-scoped <data> on any state but the root is never top-level.
@spec initialize(machine_state :: Statifier.MachineState.t()) :: {Statifier.MachineState.t(), [Statifier.Effect.t()]}
interpret's datamodel preamble (Appendix D :101-102, prose above), plus
the top-level half of enterStates' per-state binding (:312) that late
binding would otherwise defer forever - see the moduledoc's "no Appendix D
procedure body" section and spec 5.3.3's binding rule for why
the if doc.binding == "early": guard that wraps the interpret call site
in the pseudocode moves off that call site and into this function instead:
a top-level <data> is contained in no state, so under binding="late" it
never reaches enterStates' statesToEnter loop at all, and deferring its
binding to a state entry that never happens would leave it permanently
unassigned - a spec violation, not a conforming late-binding delay.
Returns {MachineState.t(), [Effect.t()]}, like every other function in
Statifier.Interpreter: the first effect on the list is always the
{:datamodel_init, %Effect.DatamodelInit{}} baseline, built from
machine_state right after seed/2 and before the binding fold - the
datamodel as it stands the instant every declared <data> exists but
before any of them has a value (see Statifier.Effect.DatamodelInit's own
moduledoc for what the map does and does not carry). Every effect after it,
in ascending d_index order, is a {:datamodel_change, %Effect.DatamodelChange{}} for one successful <data> binding
(bind_value/4, decision 3) - a failed or environment-skipped
binding emits nothing (decision 5). No trace effect is produced either way
(docs/observability.md's minimum trace vocabulary is a closed table and
datamodel binding is not a member of it); the one observable event a failed
binding can produce, error.execution, is already an ordinary internal
event that travels on machine_state's own queue.
@spec write_location( machine_state :: Statifier.MachineState.t(), datamodel_context :: Predicator.Context.t(), path_source :: String.t(), value :: term() ) :: {:ok, Statifier.MachineState.t(), Predicator.Context.t(), Statifier.Interpreter.Datamodel.Write.t()} | {:error, term()}
Writes value at path_source (a raw, uncompiled path expression such as
"foo.bar.baz" or "items[0]") against machine_state's datamodel,
rebinding the written root into datamodel_context for callers that thread
a context across a block (ADR-0028). Extracted from
Statifier.Machine.Content.Assign's defimpl - see that module's moduledoc
for the vivification and system-variable reasoning this function's five
steps carry out unchanged; this is a pure mechanics move, not a new
decision.
- Resolve
path_sourceagainstdatamodel_context.data(the normalized view, so a bracket key such asitems[i]readsiexactly as an expression would). - Reject a resolved root beginning with
"_"(spec 5.10 - a system variable). - Require the resolved root to already be a key of
machine_state.datamodel- auto-vivification only ever creates intermediate containers, never an undeclared top-level variable. - Write into the raw
machine_state.datamodel, never the normalized.dataread in step 1. - Bind just the written root into
datamodel_context- O(size of that root), not O(size of the datamodel).
Every predicator failure returns as a bare {:error, term()} - the caller
is the sole error.execution conversion site (ADR-0003).
On success, the fourth element is a Statifier.Interpreter.Datamodel.Write
report of the write - the resolved path, the prior_value read at that
full path immediately before the write, and the new_value written.
Reporting is all this function does; building an effect from the report is
each caller's own job.