# `Statifier.Interpreter.Datamodel`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier/interpreter/datamodel.ex#L1)

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.)
  An `expr` failure 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`)

1. `env_ids = MapSet.new(Map.keys(machine_state.datamodel))`, captured
   **first**, before any seeding - `MachineState.new/2`'s only writers of
   `datamodel` at this point are the `:datamodel` option and
   `SystemVariables.initial/2`, so key presence here is definitionally
   "provided by the environment at instantiation time" (5.3.2's own
   phrase).
2. Seed: `Map.put_new(datamodel, id, :undefined)` for every `%Machine.Data{}`
   in `machine.data_elements`, regardless of binding - 5.3.3 makes
   *creation* unconditional under both bindings, and writing `:undefined`
   directly means a seeded-but-unbound id answers `=== undefined`/
   `!== undefined` rather than raising `UndefinedVariableError`
   (Decision 3).
3. Bind, in ascending `d_index` order: under `:early`, every `d_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 own `initialize/1` `@doc`
   below). A top-level `d_index` whose id is in `env_ids` is skipped
   entirely, leaving its seeded `:undefined` in 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.

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`).

# `enter_state`

```elixir
@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.

# `initialize`

```elixir
@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.

# `write_location`

```elixir
@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.

1. Resolve `path_source` against `datamodel_context.data` (the normalized
   view, so a bracket key such as `items[i]` reads `i` exactly as an
   expression would).
2. Reject a resolved root beginning with `"_"` (spec 5.10 - a system
   variable).
3. 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.
4. Write into the *raw* `machine_state.datamodel`, never the normalized
   `.data` read in step 1.
5. 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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
