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

The fourth arrow of the parser pipeline: a validated `%Statifier.Document{}`
in, `{:ok, %Statifier.Machine{}} | {:error, [Statifier.Compiler.Error.t()]}`
out (`docs/architecture.md:47-51`). Nothing downstream of this pass ever
sees a `%Statifier.Document{}` again - the interpreter accepts only a
`Machine` (`docs/architecture.md` principle 4).

This phase interns every state to a flat, document-order tuple with parent
pointers and self-inclusive descendant ranges (ADR-0005), resolves every
`initial` to indexes, compiles every `<transition>` element - including an
`<initial>` element's own transition and a `:history` state's default -
into `Statifier.Machine.Transition` via its own transition pass, and
compiles every executable-content node reachable through `onentry`,
`onexit`, or a transition's own content into `Statifier.Machine.Content`,
plus every `:final` state's `<donedata>` into `Statifier.Machine.Donedata`,
via its own executable-content pass.

## One walk, not two (states); a real second pass (transitions, content,
donedata)

The "numbering walk" and "reference resolution" are, for `initial`
specifically, one traversal rather than two: a state's `initial` can only
legally name a descendant of that state (validator checks 3 and 7), and
descendants are always finished - numbered, and their own ids entered into
`id_to_index` - before their ancestor is, because this walk is post-order
on the way out of each subtree. So `id_to_index` already holds every id
`resolve_initial/3` could legally need by the time it runs, and resolving
inline avoids threading the raw `Statifier.Document.State` tree through a
second pass just to re-ask questions the first pass already had the answer
to.

Transition **target** resolution cannot make the same move: a transition's
target is not constrained to be a descendant of its source, so it can be a
forward reference to a state the walk has not reached yet
(`id_to_index` incomplete at the point the transition is visited). `t_index`
and `c_index` assignment themselves *are* done inline, in the same walk
that numbers states - each state's own `<onentry>`/`<onexit>` content, then
its own transitions (its plain transitions, then its `<initial>` element's
transition, in that order - `:history`'s own transitions are its default)
and each transition's own content, are assigned dense index values the
moment the state itself is visited, before the walk descends into that
state's children - mirroring how the state's own `index` is assigned
before its children are numbered, and matching the source's own
`onentry*, onexit*, transition*, initial?` element order (spec 3.3-3.9).
That is enough to keep every `t_index`/`c_index` dense and correctly
interleaved in document order without a second walk. What genuinely waits
for the whole walk to finish is turning each transition's raw target id
list into resolved indexes and compiling every expression (a transition's
`cond`, a `<log expr=...>`, a `<content>`'s folded value) - all of that
needs either the complete `id_to_index` or nothing but the raw node
itself, so `build_transitions/3`, `build_contents/2`, `build_donedata_map/1`,
and `build_data_elements/2` each run once, after `walk_siblings/4` returns.
The first three's errors accumulate together, sorted by
`location.start_offset`, mirroring `Statifier.Lowering.finalize/2`
(`lib/statifier/lowering.ex:137-141`) - a document with a bad `cond` on one
transition and a bad `<log expr=...>` elsewhere reports both.
`build_data_elements/2` never joins that merge - a `<data expr>` that
fails to compile is captured onto the compiled `Statifier.Machine.Data`
node as `{:invalid, error}` rather than failing `compile/1`, so a
document with a bad `cond` *and* a bad `<data expr>` reports only the
`cond`'s error.

`<data>` gets its own dense index space, `d_index`, assigned the same way
`t_index`/`c_index` are: a state's own `<datamodel>`'s `<data>` children
are assigned the moment the state itself is visited, before the walk
descends into its children (`assign_data/3`). The root's own top-level
`<datamodel>` is assigned before `walk_siblings/4` is first called, so
top-level `<data>` occupy `d_index` 0..n-1, preceding every state-scoped
one.

All of the walk's mutable state - `id_to_index`, the states accumulator,
the next unused `t_index`/`c_index`/`d_index`, the
transitions/contents/donedata/data accumulators - travels together as one
`acc()` map, rather than as seven-plus positional arguments:
`Credo.Check.Refactor.FunctionArity`'s limit is 8, and a numbering walk
that also assigns `c_index` and `d_index` genuinely needs more independent
pieces of state than that once each is its own argument.

# `acc`

```elixir
@type acc() :: %{
  id_to_index: %{optional(String.t()) =&gt; non_neg_integer()},
  states_acc: %{optional(non_neg_integer()) =&gt; Statifier.Machine.State.t()},
  t_next: non_neg_integer(),
  transitions_acc: %{optional(non_neg_integer()) =&gt; map()},
  c_next: non_neg_integer(),
  contents_acc: %{
    optional(non_neg_integer()) =&gt;
      Statifier.Document.Raise.t()
      | Statifier.Document.Log.t()
      | Statifier.Document.Assign.t()
      | Statifier.Document.Script.t()
      | Statifier.Document.Send.t()
      | Statifier.Document.Cancel.t()
      | %{if: Statifier.Document.If.t(), branches: [[non_neg_integer()]]}
      | %{foreach: Statifier.Document.Foreach.t(), content: [non_neg_integer()]}
  },
  donedata_acc: %{
    optional(non_neg_integer()) =&gt; Statifier.Document.Donedata.t()
  },
  d_next: non_neg_integer(),
  data_acc: %{
    optional(non_neg_integer()) =&gt; %{
      data: Statifier.Document.Data.t(),
      state_index: non_neg_integer()
    }
  },
  invoke_acc: %{optional(non_neg_integer()) =&gt; [Statifier.Machine.Invoke.t()]},
  invoke_errors: [Statifier.Compiler.Error.t()]
}
```

The numbering walk's threaded state (see moduledoc). `id_to_index` and
`states_acc` belong to the interning pass; `t_next`/`transitions_acc` to
the transition pass; `c_next`/`contents_acc`/`donedata_acc` to the
executable-content pass; `d_next`/`data_acc` to the `<data>` pass -
`data_acc` keyed by `d_index`, each entry carrying its raw
`%Statifier.Document.Data{}` and the index of the state whose own
`<datamodel>` declared it, so `build_data_elements/2` can compile in
`d_index` order and `compile/1` can later group entries back by state
index for `with_data/2`.

`contents_acc`'s value type is asymmetric on purpose: `<raise>`, `<log>`,
`<assign>`, and `<send>` are stored raw, since none of the four has
children of its own to number (`<send>`'s own `<param>`/`<content>` fold
onto its own fields rather than becoming further numbered content nodes,
the same way `<invoke>`'s do) - but an `<if>` and a `<foreach>` do, so
their entries carry the raw node alongside the `c_index` list(s)
`assign_content_nodes/2`'s own recursion assigned: `%{if: DIf.t(),
branches: [[non_neg_integer()]]}` for an `<if>` (one list per branch),
`%{foreach: DForeach.t(), content: [non_neg_integer()]}` for a `<foreach>`
(one flat list, one nesting level shallower) - mirroring
`transitions_acc`'s own `%{transition: ..., source: ..., content: ...}`
shape (`:396-408`).

`invoke_acc`/`invoke_errors` belong to the `<invoke>` pass - the one pass
that does not fit the "numbered during the walk, compiled in a deferred
pass" split every other field above follows. A `Machine.Invoke`'s
`<finalize>` is executable content, so its `Machine.Block` needs the same
`c_next` counter every `<onentry>`/`<onexit>` block draws from
(`assign_blocks/2`), which only exists while the walk is threading `acc`
through `walk_siblings/4` - so the whole `%Statifier.Machine.Invoke{}`,
finalize block included, is built complete during the walk and
accumulated into `invoke_acc`, keyed by owning state index, rather than
deferred the way `donedata_acc` is. Building inline means a `typeexpr`/
`srcexpr`/`<content>`/`<param>` compile failure cannot join the deferred
passes' own `collect/1` merge the way `build_transitions/3`'s and
`build_contents/2`'s do - `invoke_errors` is where those land instead,
concatenated into `compile/1`'s own error merge before the final sort. A
namelist entry's compile failure never reaches `invoke_errors`: it defers
to runtime instead, captured as `{:invalid, error}` on the entry's own
`%Machine.Param{}` (`build_namelist_param/5`, 5.9.4).

# `compile`

```elixir
@spec compile(document :: Statifier.Document.t()) ::
  {:ok, Statifier.Machine.t()} | {:error, [Statifier.Compiler.Error.t()]}
```

Compiles an already-validated `%Statifier.Document{}` into a flat
`%Statifier.Machine{}`: every state interned to a document-order index with
parent pointers and self-inclusive descendant ranges, every `initial`
resolved to indexes, every transition (including an `<initial>` element's
own and a `:history` state's default) compiled to
`Statifier.Machine.Transition`, and every reachable executable-content node
compiled to `Statifier.Machine.Content` or `Statifier.Machine.Donedata`.

Returns `{:ok, machine}` on success. A `Document` that reached this stage
is expected to already be structurally valid - `compile/1` does not
re-run validator checks - so `{:error, errors}` here has two possible
sources: a compiler defect (an id that fails to resolve during the
numbering walk), or a malformed input document whose element class still
rejects at load time under `docs/datamodel.md`'s per-element-class
deferral policy (`cond`, `<log expr>`, `<content expr>`, `<foreach
array>`, and `<param>`, among others - see that policy paragraph for the
full, current list). Callers should not assume the tuple is always
unexpected: routine error-event handling still applies to the
load-time-rejecting classes, while a defect is genuinely unexpected.

---

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