# `StatifierBlocks.Compiler`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/compiler.ex#L1)

The one-way compile: a block document plus a palette in, one artifact out
(ADR-0004 decisions 1-4, 6-7).

`compile/3` is a **total function of `{document, palette}`**. No process
state, no global registry, no IO, no clock - the same purity ADR-0002
decision 4 imposes on the callbacks, imposed on the pipeline that calls
them. It returns `{:ok, %StatifierBlocks.Compiled{}}` or
`{:error, [%StatifierBlocks.Compiler.Finding{}]}`, never raises, and never
partially succeeds.

## The pipeline

Each stage runs over the whole document; the first stage that produces
errors stops the compile and reports every error it found. Stopping
rather than accumulating across stages is deliberate - see
`StatifierBlocks.Compiler.Finding`.

1. **Document** - `StatifierBlocks.Document.validate/1`. Structural only;
   no palette is consulted.
2. **Resolve** - every block through
   `StatifierBlocks.Palette.resolve/2`, which also applies an in-memory
   config migration (ADR-0002 decision 8). Nothing is written back. A
   resolved node whose module is a composite is **replaced, in place, by
   the subtree `StatifierBlocks.Composite.expand!/2` returns** (ADR-0004's
   amendment of 2026-09-07, E1), so stages 3-6 read a tree with no
   composite in it and need no knowledge that one was ever there. The
   param map the expansion returns is kept beside the tree, and a finding
   raised against a block inside an expansion is re-anchored to the
   composite block and the param that produced it before it is reported
   (E3). The provenance map is never rewritten: every span keeps the
   expanded block that emitted it.
3. **Config** - every block's `validate_config/1`, the checks beside it
   that read something a block type cannot see from its own config, and
   the one name a **root** block may not declare as an outcome (ADR-0002's
   failure amendment of 2026-09-06, section 4 step 3, and the Note that
   closes it).
4. **Structure** - `StatifierBlocks.SlotValidation.validate/2` (slot
   arity, `:undeclared_slot`) and `StatifierBlocks.Assignability.validate/3`
   (may this block land in this slot, by kind tag and by data-flow type -
   ADR-0003), reported together.
5. **Emit** - bottom-up. Each block's `emit/2` is called with its
   children already compiled and summarized, the scope-shaped cancel for
   any delayed send a direct child armed is added to its own state
   (`StatifierBlocks.Compiler.Cancels`), its emission is attributed
   (`StatifierBlocks.Compiler.Attribution`), and its child placeholders
   are spliced with those children's own emissions.
6. **Chart** - serialize once through
   `StatifierBlocks.Compiler.Serializer`, which writes the bytes and the
   provenance map together, then run those bytes through
   `Statifier.compile/2` and map every finding back through provenance
   (`StatifierBlocks.Compiler.Chart`).

Findings from every stage are reported in document order over blocks -
`StatifierBlocks.Document.blocks/1`'s pre-order - which is how upstream's
own document-order sort survives the trip.

### Config and Structure are reported together

Every other stage in decision 10's table stops the pipeline the moment it
fails, and the errors a caller gets back come from that one stage. Config
and Structure are the one pair that does not: when Config finds something,
Structure still runs, and the refusal carries the **union** of what both
found (the dated Notes of 2026-09-06 on ADR-0004
decision 10 and on ADR-0011 decision 1).

The reason is that they are not in a consequence relation the way the
later stages are. Emit reads the tree Structure has already agreed is
well-formed, so its findings on a document Structure refused would be
artefacts. Structure reads the *document*, not the config values Config
checks, so a mis-typed field on one card and an unsatisfied read on
another are two independent statements about the same document - and an
author who can only see the first has to fix it, recompile, and discover
the second, one round trip per stage. Decision 10's "every finding within
a stage is reported, because they are siblings" is the same argument; this
extends it across exactly the one stage boundary where it holds.

What a block whose config Config refused contributes to Structure is
nothing at all - it is skipped by id, and every other block is checked
exactly as it would have been. That is what the old sequencing bought and
what has to be bought again some other way: *every* source in the
Structure stage reads the refused config. A write signature comes off a
config, so an entry derived from one would make the next block's read
disagree with a type nobody declared; `slots/1` takes a config too, so
slot arity and `:undeclared_slot` on a refused block would be counted
against a slot set that config does not really declare; and
`StatifierBlocks.Shelf` places a block the config named. So the refused
ids reach the environment walk as `:skip_blocks` - the block declares no
read and writes no entry - and the two document-shape sources drop the
findings they anchored on those same ids.

The walk itself continues past a skipped block: its siblings and its
children are checked as they always were, and a child's config is its own.
This is an absence of one block's answers, not a shortened stage.

Refusal semantics are unchanged. A document with a Config finding still
does not compile; it now says more about why.

### The Structure stage is whole

Decision 10's table names three things in this stage: slot **arity**,
`:undeclared_slot`, and assignability. ADR-0004's amendment of
2026-08-31, section D3, adds two more to the same row by the operator's
ruling - `:drafts_block_misplaced` and `:duplicate_drafts_block`,
the two placement facts a block type's `io/1` cannot carry, owned by
`StatifierBlocks.Shelf`. All of them run here, and their
findings are reported together rather than either short-circuiting the
other: an undeclared slot key does not induce an assignability finding
(a slot with no declaration gets `slot_accepts` `:any`, which admits
everything), and an arity violation is a count, which no assignability
rule reads. Neither is a consequence of the other - they are siblings,
which is decision 10's own rule for what one stage reports - so
`StatifierBlocks.SlotValidation.validate/2` and
`StatifierBlocks.Assignability.validate/3` are both always run and their
findings concatenated. Before this, the pipeline never visited a slot
`slots/1` did not declare, so an undeclared slot's blocks were absent
from the emission rather than misplaced in it - a silent drop.

## The stages before Emit, on their own

`structure_findings/3` runs stages 1 to 4 - Document, Resolve, Config and
Structure - and stops (ADR-0004's Amendment of 2026-09-22, G1 to G3). It
returns `[]` when those four stages find nothing and otherwise exactly the
findings `compile/3` refuses with, in the same order: the two functions share
one private prefix, so no message builder, skip rule or re-anchoring rule
exists twice. A host's publish step calls it to learn why a document would
not compile before Emit without emitting a chart.

The prefix is total. A Document-stage refusal is returned without walking the
tree it refused, so a document whose `root` is not a block, or whose `slots`
value is not a map of block lists, is refused by both functions rather than
raising while its findings are put in document order.

## Options

  * `:known_invoke_types` - decision 8's opt-in lint. A set (or list) of
    invoke types the caller believes will be registered; every emitted
    type absent from it becomes a **warning**, never an error. See
    `StatifierBlocks.Compiler.InvokeTypes`.
  * `:field_candidates` - the values a host offers per field, keyed
    `{type_name, field_key}`, the same map the editor's own
    `field_candidates` assign takes. A **closed** list - `[{value,
    label}]` - turns a value it does not offer into a **warning**, never
    an error; an open list - `{:open, choices}` - reports nothing, and a
    field the map does not name is not looked at. Absent, the lint does
    not run.
  * `:entry_type` - ADR-0003 decision 4's caller-supplied context: the
    type flowing into the document's root. Defaults to absent, which
    `StatifierBlocks.Assignability` reads as `:unknown`. ADR-0004's own
    typespec lists only the first option, because it delegated
    assignability wholesale to ADR-0003 (decision 11) without noticing
    that ADR-0003's context is caller-supplied and therefore has to
    arrive through this function. This is that arrival, not a second
    decision about what assignability means.
  * `:datamodel` - the host's declared datamodel, which
    `StatifierBlocks.Compiler.SensitivePaths` reads to refuse a document
    that carries a declared-sensitive path into a position the chart
    evaluates against the datamodel (ADR-0002 decision 7's `sensitive?`
    key and the secrets rule behind it). Absent, or declaring nothing
    sensitive, the check does not run and produces nothing - absence is
    not unknown-ness (ADR-0005 `11f`). See
    `StatifierBlocks.Compiler.SensitivePaths.datamodel/1` for the shapes
    it accepts, and that module for the criterion the refusal applies.

    The same document is read by two other checks, and it is read once:
    the structure stage's typed-environment read check (ADR-0011
    decisions 2 and 3), and the config stage's declared-payload refusal,
    where a `core.on_event` that declares its `payload` refuses a
    `capture` pair reading a member that payload does not carry
    (ADR-0002's amendment of 2026-09-06). A compile with no `:datamodel`
    runs neither, and refuses nothing.
  * `:child_use` - compile this document **for use as a child** of
    another chart (ADR-0004's 2026-08-29 amendment, C1). The emission
    gains one top-level `<final>` per outcome the root block declares,
    reached from `done.outcome.<root state id>.<outcome>` and carrying
    `<donedata><param name="outcome" expr="'<outcome>'"/></donedata>`,
    which is how a child's outcome crosses the invoke boundary: raised
    events are internal to the session that raises them, so the only
    thing a parent observes is the completion event and the data the
    child sent with it (SCXML 3.7 and 5.5). Defaults to `false`, and a
    document compiled without it is byte-identical to what it was before
    the option existed. The parent half is
    `StatifierBlocks.Core.Subchart`.

    Beside those per-outcome finals, one **shared** top-level `<final>`
    is emitted when a block below the root declares a failure-classed
    outcome - one `StatifierBlocks.BlockType.failure_outcomes/2` names -
    that the document did not handle (ADR-0002's failure amendment of
    2026-09-06, section 4). It is minted from the root block's id under
    the role `child_failed`, one transition per unhandled pair reaches
    it from the root block's own state, and it carries
    `<param name="outcome" expr="'error'"/>` beside the reserved
    `<param name="statifier_persistence:execution_status" expr="'failed'"/>`.
    The outcome it reports is `error` rather than the failing block's
    own outcome name, because `StatifierBlocks.Core.Subchart` appends
    `error` to its outcomes whether or not the author listed it, so it
    is the one word a parent is guaranteed to have a route for. A
    document with no unhandled failure below its root gains nothing
    here, and a root block may not declare an outcome named `failed`:
    that would ask for this final's state id a second time, and it is
    refused as a Config-stage finding.
  * `:terminate` - compile this document **as a root document that
    finishes** (ADR-0004's 2026-08-29 root-termination note). The
    emission gains one top-level `<final>` per outcome the root block
    declares, reached from `done.outcome.<root state id>.<outcome>` and
    carrying **no** `<donedata>` - except that a final for a
    failure-classed outcome carries the one reserved run-status `<param>`
    described below - so the session reaches `:done` when the
    root block completes. Without it a compiled root document never
    terminates: the root block's own outcome finals are children of the
    root compound state, so completing the root block raises
    `done.outcome` internally and the session stays active forever, which
    is what leaves a durable run uncompleted. Defaults to `false`, and a
    document compiled without it is byte-identical to what it was before
    the option existed.

    Beside those per-outcome finals, one **shared** top-level `<final>`
    is emitted when a block below the root declares a failure-classed
    outcome the document did not handle, exactly as under `:child_use`
    (ADR-0002's failure amendment of 2026-09-06, section 4). Here it is
    minted from the root block's id under the role `root_failed`, one
    transition per unhandled pair reaches it from the root block's own
    state, and its `<donedata>` holds only the reserved
    `<param name="statifier_persistence:execution_status" expr="'failed'"/>` -
    the key `statifier_persistence`'s ADR-0011 decision 4 (proposed)
    fixes, which a durable stepper reads to decide that the
    execution failed.
    So a root document whose nested step fails still reaches `:done`,
    and says that it failed when it gets there. A document with no
    unhandled failure below its root gains nothing here, and a root
    block may not declare an outcome named `failed`: that would ask for
    this final's state id a second time, and it is refused as a
    Config-stage finding.

    `:terminate` and `:child_use` are the same emission shaped for two
    different uses, and a document is compiled for one or the other:
    passing both is refused with an `:emit` finding rather than resolved
    silently, because both would put a transition on the same
    `done.outcome` event on the root block's own state and document order
    would quietly decide which top-level `<final>` a run reaches.
  * `:declare` - the **`<data>` roots the host declares for this
    document** (ADR-0004's 2026-08-29 host-declared-roots note): a list
    of `{id, expr}` pairs, in declaration order, where `expr` is either
    an expression written verbatim into the attribute or `nil` for a
    root that reads as `undefined` until something assigns it.

        Compiler.compile(document, palette, declare: [{"targets", nil}, {"parked", "false"}])

    Each pair becomes one `StatifierBlocks.Compiler.DeclaredRoots`
    declaration, prepended to the root block's own children before the
    hoist, so the host's roots lead the single `<datamodel>` in the
    order given and block-declared roots follow in document order. An
    id must be a bare lowercase identifier - `core.invoke`'s
    `assign_to` rule - and an id the option repeats, or an entry that
    is not a well-formed pair, is refused as an Emit-stage finding
    against the root block. An id a *block* also declares is F6's
    `:duplicate_binding` against that block, through the same walk a
    nested loop's collision goes through.

    This is the compile call's declaration surface, and it **leads**:
    the document has a second one, ADR-0001 decision 11's top-level
    `datamodel` key (added 2026-08-31), whose roots follow the host's
    in the single `<datamodel>` - `:declare` roots, then the document's
    own, then block-declared roots, all in document order. A root both
    declare is host-wins: the compile call's declaration is the one
    emitted, the document's is dropped, and the artifact carries a
    **warning** (`:shadowed_document_root`) rather than a refusal, since
    the compile call is what a host controls and the document edit that
    would silence the warning is not the one that fixes anything. No
    block type declares a root of its own - that surface is still
    untaken, ADR-0002's to take. Absent or `[]` emits no `<datamodel>`
    unless the document or a block declares a root, so a document
    compiled without the option is byte-identical to what it was before
    the option existed. Run creation still wins over `expr` (SCXML
    5.3.2) - a run seeded with a value for the id starts from that
    value, which is the engine's behaviour and not this package's.

    See `StatifierBlocks.Compiler.DeclaredRoots`'s "Document-declared
    roots" section for the full precedence rule and for why a document
    root colliding with a *block*-declared root stays F6's
    `:duplicate_binding` error rather than becoming a second kind of
    warning.

## Determinism (decision 6)

> For a fixed `{document canonical bytes, palette, compiler version}`, the
> generated SCXML is **byte-identical** on every machine and every run,
> forever.

All three inputs are real, and `StatifierBlocks.CompilationRecord`
records all three. What this module contributes to the guarantee is that
it never iterates a bare map: slots are visited in `slots/1` declaration
order (ADR-0002 decision 6 made that order meaningful), children in
document order, and attributes are sorted at construction by
`StatifierBlocks.Emission.element/3`.

The guarantee is **not** reversible and must not be read as one. Equal
output does not imply equal input: a `metadata`-only edit changes the
document hash and produces identical SCXML, because `metadata` is not
compiled. A host may use "same triple" to skip a recompile, and may
**not** use "same SCXML" to conclude the document is unchanged.

The document's `datamodel` key (ADR-0001 decision 11) is part of the
document's canonical bytes and therefore of the triple's first input,
so the guarantee is unweakened by its existence - moving an entry's
`id` or `expr` moves the document hash and, because those are compiled,
moves the generated SCXML too. But it is a second instance of the same
non-reversibility clause above: an entry's `description` is prose, not
compiled, so two documents differing only in a `description` hash
differently and still produce byte-identical SCXML.

# `option`

```elixir
@type option() ::
  {:known_invoke_types, Enumerable.t()}
  | {:field_candidates, %{optional({String.t(), String.t()}) =&gt; term()}}
  | {:entry_type, StatifierBlocks.Assignability.type_expr() | :unknown}
  | {:datamodel, term()}
  | {:child_use, boolean()}
  | {:terminate, boolean()}
  | {:declare, [StatifierBlocks.Compiler.DeclaredRoots.declaration()]}
```

`:known_invoke_types` enables decision 8's optional two-registry lint;
`:field_candidates` enables the opt-in warning for a value outside a
host's own closed candidate list;
`:entry_type` is ADR-0003 decision 4's caller-supplied context;
`:datamodel` is the host's declared datamodel, read by the sensitive-path
refusal, the typed-environment read check and the declared-payload
refusal; `:declare` is the `<data>` roots the host declares for this
document. See the moduledoc.

# `compile`

```elixir
@spec compile(StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), [option()]) ::
  {:ok, StatifierBlocks.Compiled.t()}
  | {:error, [StatifierBlocks.Compiler.Finding.t()]}
```

Compiles `document` against `palette`.

Total: `{:ok, %StatifierBlocks.Compiled{}}` or
`{:error, [%StatifierBlocks.Compiler.Finding{}]}`, never a raise and
never a partial success. Errors come from the first failing stage only
(decision 10), with one exception the moduledoc's "Config and Structure
are reported together" section states: those two stages run as a pair, and
a refusal carries the union of their findings. Warnings ride on the
artifact when the compile succeeds.

# `compiler_version`

```elixir
@spec compiler_version() :: String.t()
```

Decision 6's third determinism input: this package's version.

# `structure_findings`

```elixir
@spec structure_findings(StatifierBlocks.Document.t(), StatifierBlocks.Palette.t(), [
  option()
]) :: [
  StatifierBlocks.Compiler.Finding.t()
]
```

The findings stages 1 to 4 refuse `document` for - Document, Resolve,
Config and Structure - without emitting anything (ADR-0004's Amendment of
2026-09-22, G1 to G3).

Returns `[]` when those four stages find nothing, and otherwise the
findings they found: the same `StatifierBlocks.Compiler.Finding` values
`compile/3` refuses with, struct for struct and in the same document
order, because both run one private implementation of those stages. When
`compile/3` succeeds, or refuses at a stage after Structure, this returns
`[]`.

Total: never a raise on a `%Document{}` and a `%Palette{}`, whatever the
document holds. A Document-stage refusal comes back as one finding with
`block_id: nil`; every other finding names a block the stored document
holds, a finding inside a composite's expansion re-anchored to the
composite the author placed.

`opts` is `compile/3`'s own option list. The stages this runs read
`:datamodel` and `:entry_type`; every other option is ignored, so a host
passes one keyword list to both calls. Only errors come back: the warnings
a compile reports come from stages after Structure.

---

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