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

How a block type contributes a **compiler-declared `<data>` root** to the
chart's one top-level `<datamodel>` (ADR-0004's 2026-08-29 foreach
amendment, F2 and F3), and the collision refusal F6 records.

Until `core.foreach` there was no such thing: every core type wrote into
the datamodel the host had already declared, and the compiler emitted no
`<datamodel>` at all. A loop cannot. Its cursor and its snapshot of the
list are the compiler's own state, its `item_as`/`index_as` bindings are
roots an author named, and predicator refuses to read a root nothing
declared - so the four names have to be declared somewhere, and early
binding means "somewhere" is the top of the document, before any state
is entered.

## The mechanism: declare in place, hoist once

A block type emits `declare/2` elements **among its own state's
children**, where everything else it emits already is, and the compiler
lifts every one of them out of the assembled emission into a single
top-level `<datamodel>` just before serialization (`hoist/1`).

Declaring in place rather than through a new `StatifierBlocks.BlockType`
callback is what keeps this small. A callback would be a change to
ADR-0002's declaration surface for one block type's benefit; a `<data>`
element is something `emit/2` can already return, it goes through
`StatifierBlocks.Compiler.Attribution` on the ordinary path, and so it
arrives here already carrying the owner and the config key a finding
against it needs. Nothing about the block type's contract changes.

### Determinism (decision 6)

`hoist/1` walks the assembled tree in document order and emits the roots
in the order it meets them, outermost first. The walk is over lists, in
the order `emit/2` built them, so the same document produces the same
bytes - the property decision 6 requires and the reason no sort happens
here. A document declaring no roots gets **no `<datamodel>` element at
all**, so every chart this package compiled before this mechanism
existed still compiles to the same bytes.

### Provenance (decision 5)

A hoisted element is moved, never rebuilt: it carries the owner
Attribution stamped on it, so the provenance map stays total over the
bytes it becomes. The `<datamodel>` wrapper belongs to no block and is
attributed to the root block, exactly as `<scxml>` is.

## F6: `:duplicate_binding`

Early binding makes a declared root global, so a nested loop re-using an
enclosing loop's `item_as` would overwrite the outer binding and the
outer body would carry on with the inner loop's last item. The compiler
refuses the document instead: a root declared **inside** the subtree of
a block that already declares the same name is a `:duplicate_binding`
finding, reported by `StatifierBlocks.Compiler` as an Emit-stage error
against the block whose binding collides, carrying the `config_key` the
name was typed into (`item_as` or `index_as`) and therefore the author's
fault.

The rule this module applies is **nesting**, not the whole document -
"collide across nesting", as F6 words it - and that is deliberately the
narrow carve-out from decision 9's delegation rather than a general
id-uniqueness check. Two roots of one name that are *not* nested are
still refused, one stage later and by the delegated check: statifier's
own id-uniqueness pass reports `{:duplicate_id, name}` over the two
`<data>` elements, and `StatifierBlocks.Compiler.Chart` maps it back to
the declaring block and its config field. F6 describes itself as
pre-empting exactly that finding for the case where the overwrite would
otherwise be silent, and pre-empting it anywhere else would be a
widening of decision 9 this module has no mandate for.

Author-declared `<data>` ids, F6's other half, are covered by the same
walk the day they exist: no config field declares one and no block type
emits one for a name an author typed as an id. Nothing here
special-cases `core.foreach`, so a declaration arriving as a `<data>`
element is checked against the loops beneath it without this module
being touched.

## Host-declared roots (the `:declare` compile option)

That day arrived for the *host* rather than for the author.
`declarations/1` turns `StatifierBlocks.Compiler.compile/3`'s
`:declare` option - a list of `{id, expr}` pairs in declaration order -
into the same `declare/2` emissions a block type contributes, and the
compiler prepends them to the root block's own children before
`hoist/1` runs. Three things follow from that placement rather than
from any new code here:

  * **Order.** `hoist/1` lifts an element's own declarations before it
    descends, so the host's roots lead the single `<datamodel>` in the
    order the option lists them and block-declared roots follow in
    document order.
  * **Collision.** A host root is in scope for everything beneath it,
    so a block declaring the same name is F6's `:duplicate_binding`,
    reported against that block and its config key exactly as a nested
    loop's collision is. A name repeated *within* the option list never
    reaches the walk: `declarations/1` refuses it, because there is no
    block to name in a finding about a list the host wrote.
  * **Nothing else.** No `<datamodel>` is emitted for a document that
    declares nothing, option absent or `[]` alike, so every chart
    compiled before the option existed still compiles to the same
    bytes.

An id must be a bare lowercase identifier, the rule `core.invoke`
applies to `assign_to`: the host is declaring a location the chart will
assign to and read in a guard, and predicator's grammar is what both
ends have to agree on. An `expr` is either `nil`, for a root that reads
as `undefined` until something assigns it, or a non-empty expression
written verbatim into the attribute. Run creation still wins over
`expr` - a run seeded with a value for the id starts from that value
(SCXML 5.3.2), which is upstream's behaviour and not this module's.

## Document-declared roots (ADR-0001 decision 11)

The document itself carries a second declaration surface: a top-level
`datamodel` key of `StatifierBlocks.Document.DatamodelEntry` structs,
each already checked against ADR-0001's structural rules by
`StatifierBlocks.Document.validate/1` before a document ever reaches the
compiler. `document_declarations/1` turns them into the same
`declare/2` emissions `declarations/1` builds from the compile call's
`:declare` option, and `StatifierBlocks.Compiler` prepends them to the
root block's own children **after** the host's roots and before any
block-declared root, which is what puts them second in the single
`<datamodel>`:

  1. the compile call's `:declare` roots lead,
  2. the document's own `datamodel` roots follow,
  3. block-declared roots follow those, in document order.

**Host wins.** A document root whose id a host root already declares is
dropped by `shadowed/2` before the hoist ever runs, and the compiler
turns the dropped id into one **warning** on the compiled artifact
rather than a refusal - the compile call leads, so the host's
declaration is the one that survives and the document's is silently
fine to have, not silently fine to lose track of. This is different
from F6: F6 is a refusal because an uncaught collision would silently
overwrite a binding a chart still reads from two places, while a
host-shadowed document root is never emitted at all, so there is
nothing left in the chart to overwrite.

A document root colliding with a **block-declared** root is unchanged
F6 `:duplicate_binding`, an **error**, through the same `hoist/1` walk
every other collision goes through: document roots are prepended among
the root block's own children before the hoist runs, so every block in
the document sits inside their scope exactly as it sits inside the
host's. Nothing about `hoist/1` needs to know a root came from the
document rather than the host - the walk does not distinguish the two,
and `shadowed/2` is what keeps a host/document collision from ever
reaching it as a false F6.

# `declaration`

```elixir
@type declaration() :: {String.t(), String.t() | nil}
```

One entry of the `:declare` compile option: a root id and either an
initial expression or `nil`.

# `declaration_finding`

```elixir
@type declaration_finding() ::
  {:invalid_declaration, term()} | {:duplicate_declaration, String.t()}
```

A refusal of the `:declare` option itself, before any walk: an entry
that is not a well-formed declaration, or an id the list declares
twice.

# `finding`

```elixir
@type finding() ::
  {:duplicate_binding, StatifierBlocks.Block.id() | nil, String.t() | nil,
   String.t()}
```

One collision: the block whose binding collides, the config key it was
typed into (`nil` for a root no author named), and the offending name.

# `datamodel`

```elixir
@spec datamodel([StatifierBlocks.Emission.t()]) :: [StatifierBlocks.Emission.t()]
```

The top-level `<datamodel>` holding `roots`, or nothing at all when
there are none.

A list rather than a value, so the caller splices it into `<scxml>`'s
children without a conditional - and so "no roots, no element" is this
module's decision rather than every caller's.

# `declarations`

```elixir
@spec declarations(term()) ::
  {:ok, [StatifierBlocks.Emission.t()]} | {:error, [declaration_finding()]}
```

The `:declare` compile option as `declare/2` emissions, in the order
the option lists them.

`nil` and `[]` are both "the host declares nothing" and produce no
emissions, which is what keeps a document compiled without the option
byte-identical to what it was before the option existed.

`{:error, findings}` when an entry is not a `{id, expr}` pair whose id
is a bare lowercase identifier and whose expr is `nil` or a non-empty
string, or when the list declares one id twice. Every entry is checked,
so a host fixing its call sees all of them at once.

# `declare`

```elixir
@spec declare(String.t(), String.t() | nil) :: StatifierBlocks.Emission.t()
```

A `<data>` declaration for the root `id`, optionally with an initial
`expr`.

A root declared with no `expr` reads as `undefined` until something
assigns it, which is what the loop's bindings want: `item_as` means
nothing until the head state's first pass binds it.

# `document_declarations`

```elixir
@spec document_declarations([StatifierBlocks.Document.DatamodelEntry.t()]) :: [
  StatifierBlocks.Emission.t()
]
```

The document's own `datamodel` entries as `declare/2` emissions, in
list order.

Unlike `declarations/1`, this takes **already-validated** entries: the
Document stage (`StatifierBlocks.Document.validate/1`, run before the
compiler's Emit stage) refuses a document whose `datamodel` is
malformed, so a `%DatamodelEntry{}` reaching here always has a bare
identifier for an id and either `nil` or a non-empty expression for
`expr`. That is why this returns a plain list rather than a tagged
tuple: the shape is a schema rule, checked exactly once by
`StatifierBlocks.Document.validate/1`, and re-checking it here would be a
second place for that rule to drift from the one the document was actually
validated against.

`description` is dropped - it is prose for a human reading the
document, never compiled and never written into the emitted SCXML.

# `hoist`

```elixir
@spec hoist(StatifierBlocks.Emission.t()) ::
  {:ok, {StatifierBlocks.Emission.t(), [StatifierBlocks.Emission.t()]}}
  | {:error, [finding()]}
```

Lifts every `<data>` element out of `emission`, returning the stripped
tree and the roots in document order.

`{:error, findings}` when a root is declared inside the subtree of a
block that already declares the same name (F6).

# `shadowed`

```elixir
@spec shadowed([StatifierBlocks.Emission.t()], [StatifierBlocks.Emission.t()]) ::
  {[StatifierBlocks.Emission.t()], [String.t()]}
```

Splits the document's own root emissions into the ones that survive and
the ids a `host` root already declares.

Host wins: a document root whose id `host` also declares is dropped
from `kept_document_roots` and its id collected into `shadowed_ids`, in
`document` order. Both lists are otherwise `document`'s own order,
unchanged - this makes no claim about the host's own order, and does
not need to, since the caller prepends `host` ahead of whatever this
returns.

---

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