# `Statifier.Machine`
[🔗](https://github.com/riddler/statifier-ex/blob/v2.0.0/lib/statifier/machine.ex#L1)

The compiled, valid-by-construction interpreter input
(`docs/architecture.md:47-51`, principle 4). `Statifier.Compiler.compile/1`
is the only producer; the interpreter is the only consumer that matters -
every other layer stops at `%Statifier.Document{}`.

## Layout (ADR-0005)

`states` is a tuple of `Statifier.Machine.State.t()` in document order,
index 0 being the synthesized `:scxml` root - the spec treats `<scxml>` as
a state for LCCA and transition-domain purposes, so giving it a real index
removes the special case from `get_transition_domain` rather than adding
one; its `parent` and `id` are both `nil`. Every compiled state carries its
own `index`, its `parent` index (`nil` only at the root), and `last` - the
highest index in its own subtree, making the range `index..last`
self-inclusive and contiguous by construction. `descendant?/3`, `ancestor?/3`,
`lcca/2`, `document_order/2`, and `exit_order/2` below are exactly the
integer/range comparisons ADR-0005 was adopted for - no precomputed cache,
no ancestor-path table.

The stored range is self-inclusive, but `descendant?/3` is not: it is
Appendix D's `isDescendant`, which is a proper-descendant test, so it
compares `ancestor < descendant` against that range. The predicates here
carry Elixir's `?` form rather than the spec's `isFoo`, per ADR-0002's
2026-08-09 amendment, and each names its Appendix D counterpart in its own
`@doc`.

`id_to_index` is partial: an entry only for a state with a non-nil,
non-empty id, mirroring `Statifier.Validator.Checks.Ids`'s own uniqueness
set. There is no `index_to_id` map - `elem(states, i).id` is already that
function, total with `nil`s, exposed here as `id/2`.

`transitions`, `contents`, and `data_elements` are dense tuples indexed by
`t_index`/`c_index`/`d_index` (ADR-0012 item 3). The compiler's transition
pass populates `transitions` (`transition/2` below is its `elem/2` reader,
mirroring `at/2`); the compiler's executable-content pass populates
`contents` (`content/2` below, the same `elem/2` reader shape); the
compiler's `<data>` pass populates `data_elements` (`data/2` below, same
shape again) - see `Statifier.Machine.Data`'s moduledoc for what a
`d_index` names. It lives in both places because the two carry different
access patterns: here in document order as a dense list for whole-document
passes, and on `Statifier.Machine.State.data` as per-state membership for
entry-time binding.

## Expressions

Every `cond`, every `<log expr=...>`, every `<content expr=...>` or
`<content>` text body compiles once into `expr()`
(`docs/architecture.md:76-82`, ADR-0014). Nothing before the compiler's
expression-compilation seam builds one; the type is declared here because
it is Machine data, not because anything yet produces a `{:compiled, ...}`
value.

## No top-level `initial`

The root state at index 0 already carries resolved `initial` indexes -
giving it a real index in the first place is what lets the root need no
special case here - so a second, Machine-level `initial` field would
duplicate that fact the same way storing both ends of a self-inclusive
descendant range would. `initial/1` reads it off index 0.

## Why `global_scripts` is different: not derivable from `contents`

`global_scripts` (below) looks like it should be the same kind of
redundancy this section just ruled out, and it is not, for a structural
reason rather than a policy one. Every `contents` entry is addressed by a
`c_index` that names its owning block or transition (`Content.owner/0`) -
that address space is the block runner's, built for a node some
`<onentry>`/`<onexit>`/transition/`<if>`-branch/`<foreach>`-body walks
and executes. A top-level `<script>` (spec 5.8) is a child of `<scxml>`
itself, run once at load time by `Statifier.Interpreter.initialize/2`
directly (Phase 3, ADR-0026) - no block ever runs it, so it has no
`c_index`, no block, and no owner to be derived *from*. `global_scripts`
is not a cache of a fact `contents` already states; it is the only place
the fact is stated at all.

## `warnings`: a document-conformance finding, not a validity finding

`warnings` (ADR-0033) is a list of `Statifier.Validator.Warning.t()` the
validator produced while checking the source document, stamped onto the
Machine by `Statifier.compile/1`. It defaults to `[]` and is not in
`@enforce_keys`, the same way `global_scripts` is not: a Machine with no
warnings is exactly as valid as one with some.

This is deliberately **not** a second validity axis. Design principle 4
("make invalid states unrepresentable") is about the `Document` ->
`Machine` boundary: a `Machine` only exists because the validator's error
checks all passed, and that boundary is untouched by this field. A warning
is a statement about the *document's* conformance to a spec MUST the engine
nonetheless has defined behavior for either way (ADR-0033's own rationale
for why the finding does not gate compilation) - it says nothing about
whether this `Machine` is well-formed. A `%Machine{}` with a non-empty
`warnings` list is just as valid-by-construction as one with `warnings: []`.

The field exists here rather than as a third element on `compile/1`'s
return because ADR-0012 item 3 already puts every other retained diagnostic
- locations on states, transitions, and executable content, span tables on
compiled expressions - on the Machine, and a warning with a location is the
same kind of thing. It is also, per ADR-0033, the *only* surfacing seam: no
trace effect, no logger, no telemetry - a caller (or a debugger) that wants
a warning finds it here or nowhere.

## `identity`: the chart revision, not the document's validity

`identity` (see ADR-0052) is the `Statifier.Machine.Identity.t()`
`Statifier.compile/2` stamps from the source it compiled, or `nil` for a
Machine built without going through that boundary -
`Statifier.Compiler.compile/1` called directly, or a Machine an embedder's
`:invoke_source` resolver returned rather than one `Statifier.compile/2`
produced (`Statifier.Invoke.Source.resolve/2`'s `src` clause). It defaults
to `nil` and is not in `@enforce_keys`, the same reasoning as
`global_scripts` and `warnings` above: a Machine with no identity is exactly
as valid as one with one. `identity/1` below is the reader.

## `source` and `compile_opts`: what `identity` was computed from

`source` (the SCXML binary `Statifier.compile/2` compiled) and
`compile_opts` (the persisted subset of the `opts` it was called with) are
stamped onto the Machine together with `identity`, by `compile/2` and only
by `compile/2` - the same three fields, the same one producer, so
`source: nil` and `identity: nil` always co-occur, and a non-nil `identity`
always has the exact `source` it was hashed from sitting beside it.
`compile_opts` is never the caller's whole keyword list: it is filtered
through a closed allowlist (`Statifier.compile/2`'s `@persisted_compile_opts`)
before it is stored, so an option this library does not recognize is never
carried onto the Machine. `source/1` and `compile_opts/1` below are the
readers.

# `expr`

```elixir
@type expr() ::
  {:static, term()} | {:compiled, Predicator.Compiled.t(), source :: String.t()}
```

Every `foo`/`fooexpr` slot in the Machine holds one of these
(`docs/architecture.md:76-82`). `{:static, term()}` is a literal value with
no expression to evaluate; `{:compiled, %Predicator.Compiled{}, source}`
carries the compiled instructions and span table (ADR-0014 items 1-2)
alongside the original source string for diagnostics.

# `program`

```elixir
@type program() :: {:program, Predicator.Compiled.t(), source :: String.t()}
```

A compiled predicator *statement program* - a `<script>` body
(ADR-0026). This is a **sibling** of `expr()`, never one of its arms:
`Predicator.evaluate/3` rejects a statement program outright
(`deps/predicator/lib/predicator.ex:213-217`), so a program has no path
through `Statifier.Evaluator.evaluate/2` and is run instead through
`Statifier.Evaluator.execute/2`. `source` is carried alongside the
compiled instructions for the same reason `expr()`'s `{:compiled, ...}`
arm carries it - diagnostics on a run-time failure.

# `t`

```elixir
@type t() :: %Statifier.Machine{
  binding: :early | :late,
  compile_opts: keyword(),
  contents: tuple(),
  data_elements: tuple(),
  datamodel: String.t() | nil,
  global_scripts: [program() | {:invalid, Statifier.Compiler.Error.t()}],
  id_to_index: %{optional(String.t()) =&gt; non_neg_integer()},
  identity: Statifier.Machine.Identity.t() | nil,
  location: Statifier.Parser.Location.t(),
  name: String.t() | nil,
  source: binary() | nil,
  states: tuple(),
  transitions: tuple(),
  warnings: [Statifier.Validator.Warning.t()]
}
```

# `ancestor?`

```elixir
@spec ancestor?(
  machine :: t(),
  ancestor :: non_neg_integer(),
  descendant :: non_neg_integer()
) ::
  boolean()
```

Whether `ancestor` is one of `descendant`'s proper ancestors -
`descendant?/3` with its arguments swapped, spelled for the reader who
wants "is X an ancestor of Y" rather than "is Y a descendant of X".

# `at`

```elixir
@spec at(machine :: t(), index :: non_neg_integer()) :: Statifier.Machine.State.t()
```

The state at `index`, raised if out of range - every index this module
hands back came from the Machine itself, so an out-of-range index is
always a caller bug.

# `atomic?`

```elixir
@spec atomic?(machine :: t(), index :: non_neg_integer()) :: boolean()
```

Whether `index`'s state is atomic - no children. A `:final` is atomic:
`kind` and atomicity are independent facts.

# `child_states`

```elixir
@spec child_states(machine :: t(), index :: non_neg_integer()) :: [non_neg_integer()]
```

`getChildStates(state1)` (Appendix D): "a list containing all `<state>`,
`<final>`, and `<parallel>` children of `state1`" - `index`'s direct
children with any `:history` pseudo-state child excluded, in document
order.

# `children`

```elixir
@spec children(machine :: t(), index :: non_neg_integer()) :: [non_neg_integer()]
```

`index`'s direct children, in document order - **every** one, `:history`
pseudo-states included. This is *not* `getChildStates` (Appendix D):
Appendix D's `getChildStates(state1)` is defined (`### function
getChildStates(state1)`) as "a list containing all `<state>`, `<final>`,
and `<parallel>` children of `state1`" - `:history` is excluded by
definition. This function returns the raw `children` field as compiled,
because that field is also what lets `State.history_children` be a lookup
rather than a scan over `children` for the `:history` ones. Callers that
want the spec operation want `child_states/2` instead.

# `compile_opts`

```elixir
@spec compile_opts(machine :: t()) :: keyword()
```

The persisted subset of the options `Statifier.compile/2` was called
with - filtered through its closed allowlist, in the allowlist's order,
never the caller's whole `opts` list. `[]` for a Machine built without
going through that boundary, or for a compile that passed none of the
allowlisted keys.

# `compound?`

```elixir
@spec compound?(machine :: t(), index :: non_neg_integer()) :: boolean()
```

Whether `index`'s state is compound: a `:state` or `:scxml` with at least
one child - derived, never stored. A `:parallel` is never compound even
though it has children: it enters every region simultaneously rather than
defaulting into one, so it has no positional default entry the way a
compound `:state` does.

# `content`

```elixir
@spec content(machine :: t(), c_index :: non_neg_integer()) ::
  Statifier.Machine.Content.t()
```

The executable-content node at `c_index`, raised if out of range - every
`c_index` this module hands back (via a block's `content` or a
transition's `content`) came from the Machine itself, so an out-of-range
index is always a caller bug (mirrors `at/2` and `transition/2`).

# `data`

```elixir
@spec data(machine :: t(), d_index :: non_neg_integer()) :: Statifier.Machine.Data.t()
```

The `<data>` element at `d_index`, raised if out of range - every
`d_index` this module hands back (via a state's `data`) came from the
Machine itself, so an out-of-range index is always a caller bug (mirrors
`at/2`, `transition/2`, and `content/2`).

# `descendant?`

```elixir
@spec descendant?(
  machine :: t(),
  descendant :: non_neg_integer(),
  ancestor :: non_neg_integer()
) :: boolean()
```

Whether `descendant` is one of `ancestor`'s descendants - `isDescendant`
(Appendix D), under ADR-0002's predicate-naming amendment.

**Proper**, exactly as the spec defines it ("a child, or a child of a child,
or a child of a child of a child, etc."): `descendant?(m, i, i)` is `false`.
The stored range `index..last` is self-inclusive by construction, so the
lower bound is strict here to exclude the ancestor itself. Appendix D relies
on that strictness - `compute_exit_set` must not exit the transition domain,
and `find_lcca` must reject a candidate that is itself in the list.

# `document_order`

```elixir
@spec document_order(machine :: t(), indexes :: Enumerable.t()) :: [non_neg_integer()]
```

`indexes` sorted ascending - document order, an integer sort (ADR-0005).

# `exit_order`

```elixir
@spec exit_order(machine :: t(), indexes :: Enumerable.t()) :: [non_neg_integer()]
```

`indexes` sorted descending - exit order, the exact reverse of document order.

# `final?`

```elixir
@spec final?(machine :: t(), index :: non_neg_integer()) :: boolean()
```

Whether `index`'s state is a `<final>`.

# `history?`

```elixir
@spec history?(machine :: t(), index :: non_neg_integer()) :: boolean()
```

Whether `index`'s state is a `<history>` pseudo-state.

# `id`

```elixir
@spec id(machine :: t(), index :: non_neg_integer()) :: String.t() | nil
```

The id `index`'s state was written with, or `nil` for the root and for
every nameless state - the total reverse of `index/2`, ADR-0005's "both
directions".

# `identity`

```elixir
@spec identity(machine :: t()) :: Statifier.Machine.Identity.t() | nil
```

The chart identity `Statifier.compile/2` stamped, or `nil` for a Machine
built without a source: `Statifier.Compiler.compile/1` called directly, or a
Machine an embedder's `:invoke_source` resolver returned
(`Statifier.Invoke.Source.resolve/2`'s `src` clause).

# `index`

```elixir
@spec index(machine :: t(), id :: String.t()) :: {:ok, non_neg_integer()} | :error
```

The index a written state `id` was interned to, or `:error` when unknown.

# `initial`

```elixir
@spec initial(machine :: t()) :: [non_neg_integer()]
```

The root's resolved `initial` indexes - there is no Machine-level
`initial` field; index 0 already carries it.

# `lcca`

```elixir
@spec lcca(machine :: t(), indexes :: [non_neg_integer()]) :: non_neg_integer()
```

The least common compound ancestor of every index in `indexes` -
`findLCCA` (Appendix D): the nearest proper ancestor of the first index
that is compound (`compound?/2`) and is a proper ancestor of every index in
the list. Walking up from the first index's parent and stopping at the first
ancestor whose range covers every index is O(depth) with no
precomputation (ADR-0005) - the `:scxml` root always qualifies, so this is
total over any non-empty list of indexes belonging to one machine.

Appendix D tests `stateList.tail()`; testing the whole list is equivalent
and is what the pipeline below does, because a candidate is drawn from the
head's *proper* ancestors and so always passes `descendant?/3` for the head.
The strictness matters for the rest of the list: when a later index is
itself an ancestor of the head - a transition targeting its own ancestor -
that index is not its own proper descendant, so the candidate equal to it is
rejected and the walk continues outward, which is the domain such a
transition must get.

`Statifier.Interpreter.Selection.find_lcca/2` is the spec-named entry point
at the interpreter's surface, a `defdelegate` to this function - one
implementation, two names.

# `parallel?`

```elixir
@spec parallel?(machine :: t(), index :: non_neg_integer()) :: boolean()
```

Whether `index`'s state is a `<parallel>`.

# `proper_ancestors`

```elixir
@spec proper_ancestors(machine :: t(), index :: non_neg_integer()) :: [
  non_neg_integer()
]
```

`index`'s proper ancestors, nearest first, root last - `getProperAncestors`
(Appendix D) called with no `root` bound, i.e. every ancestor up to and
including the `:scxml` root. `index` itself is excluded.

# `source`

```elixir
@spec source(machine :: t()) :: binary() | nil
```

The SCXML source `Statifier.compile/2` compiled this Machine from, or `nil`
for a Machine built without going through that boundary (mirrors
`identity/1`'s `nil` cases).

# `transition`

```elixir
@spec transition(machine :: t(), t_index :: non_neg_integer()) ::
  Statifier.Machine.Transition.t()
```

The transition at `t_index`, raised if out of range - every `t_index` this
module hands back (via a state's `transitions`, `initial_transition`, or
`history_default`) came from the Machine itself, so an out-of-range index
is always a caller bug (mirrors `at/2`).

---

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