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

The typed parse target: what lowering produces, and what the validator and
the Machine compiler consume.

This is the layer `docs/architecture.md:47` names directly: "Document
(typed structs, source locations, uncompiled expressions)". Interning,
indexing, and expression compilation are the compiler's job, not this
layer's - a `%Statifier.Document{}` tree is walkable and unambiguous, not
fast, and it deliberately stays the pre-validation type so it can hold the
malformed shapes the validator exists to report (`docs/architecture.md:33-36`,
principle 4).

## Raw expressions

`cond`, `log`'s `expr`, and `content`'s `expr` are `String.t() | nil` -
the exact predicator source the author wrote, uncompiled. Compilation into
the `{:static, term()} | {:compiled, %Predicator.Compiled{}, source}` sum type
happens once at Machine-build time (`docs/datamodel.md`), so nothing in
`lib/statifier/document/` may reference `Predicator`.

## The `attribute_locations` contract

Every node with source attributes carries an `attribute_locations` map,
keyed by an attribute name atom drawn from a fixed, known set per element,
valued by that attribute's **value span** - the text inside the quotes,
which is what ADR-0014's arithmetic needs and what a diagnostic underlines.
The node's own `location` already covers the whole element for anything
coarser.

The map's defining rule: **an entry exists only for an attribute that was
actually written in the source.** Lowering applies defaults (`:external`,
`:early`, `:shallow`), so a field's runtime value alone cannot say whether
the author wrote it or the default applied. `Map.has_key?(node.attribute_locations, :type)`
is exactly the "was it written" question, and the same lookup is the
location a diagnostic would want. A struct with a non-nil default (like
`Statifier.Document.Transition.type`) and no corresponding key in
`attribute_locations` means the default applied, not that the author wrote
the default's value explicitly.

Inherited from `Statifier.Parser.DOM.Attribute`: the stored string is
entity-expanded while the span covers raw source, so an offset *inside* a
value containing an entity or character reference does not map 1:1 onto
the source.

## `xmlns` versus `namespace`

`xmlns` is the literal attribute as the author wrote it (or `nil` when
absent) - it never resolves prefixes. `namespace` is the URI the root
element's name actually resolves to after prefix scoping
(`Statifier.Lowering.Namespace.resolve/2`), stamped on by
`Statifier.Lowering.lower/1`. The two differ exactly for a prefix-declared
root such as `<s:scxml xmlns:s="...">`, where `xmlns` is `nil` but
`namespace` is the resolved URI; the validator's check 9 reads `namespace`,
not `xmlns`, so a spec-conformant prefixed document is not misreported as
missing its namespace.

## Why `<scxml>` is its own struct, not a `State` with `kind: :scxml`

`%Statifier.Document{}` is the root, and there is no `kind: :scxml` on
`Statifier.Document.State`. The alternative -
`%Document{root: %State{kind: :scxml}}` - was considered because the
compiler interns the root as state index 0 with `kind: :scxml`, so that
LCCA and `get_transition_domain` fall out without a special case. It is
rejected here for three reasons.

First, the compiler's root state is a **synthesis**, not a copy: it needs an
index, a range, and resolved initial indexes, none of which the Document
has. Building it from `%Document{}`'s fields is one function either way, so
the `:scxml` kind costs the compiler nothing to create and buys the
Document layer nothing to carry.

Second, the split would put `<scxml>`'s attributes in two places: `initial`
and `location` on the root state, `name`/`version`/`binding`/`datamodel` on
the wrapper, because those four are not state fields on any other kind. One
struct holding all of `<scxml>`'s attributes is the honest shape.

Third, the generic-walk argument that favored the single `State` struct
(`Statifier.Document.State`'s moduledoc) does not transfer here. A walk over
every state is `document.states |> Enum.flat_map(&walk/1)` - one entry
clause, not a per-kind dispatch - and `Document.states` has the same
`[State.t()]` type as `State.states`, so the recursion below the entry
point is uniform either way.

The compiler synthesizes its index-0 root state from these fields directly
rather than copying a `%State{}` that never existed at this layer.

# `attribute_locations`

```elixir
@type attribute_locations() :: %{optional(atom()) =&gt; Statifier.Parser.Location.t()}
```

Value spans only, keyed by the attribute's name as an atom drawn from a
fixed, known set per element. See the moduledoc for the "written, not
defaulted" contract this map carries.

# `content_node`

```elixir
@type content_node() ::
  Statifier.Document.Raise.t()
  | Statifier.Document.Log.t()
  | Statifier.Document.Assign.t()
  | Statifier.Document.If.t()
  | Statifier.Document.Foreach.t()
  | Statifier.Document.Script.t()
  | Statifier.Document.Send.t()
```

The executable-content node types lowering currently supports.

# `state_kind`

```elixir
@type state_kind() :: :state | :parallel | :final | :history
```

The kinds a `Statifier.Document.State` can have. Equal to the element
name that produced it - `<initial>` is not a member (it has no `id` and is
a slot on its parent, not a targetable state; see `Statifier.Document.Initial`),
and neither are the compiler-only widenings `:atomic` / `:compound`.

# `t`

```elixir
@type t() :: %Statifier.Document{
  attribute_locations: attribute_locations(),
  binding: :early | :late,
  datamodel: String.t() | nil,
  datamodel_element: Statifier.Document.Datamodel.t() | nil,
  initial: [String.t()],
  location: Statifier.Parser.Location.t(),
  name: String.t() | nil,
  namespace: String.t() | nil,
  scripts: [Statifier.Document.Script.t()],
  states: [Statifier.Document.State.t()],
  version: String.t() | nil,
  xmlns: String.t() | nil
}
```

---

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