# `StatifierBlocks.Core.Foreach`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/core/foreach.ex#L1)

`core.foreach`: a container whose body runs **once per item** of a
datamodel list (ADR-0004's 2026-08-29 amendment, F1 through F6).

It is the first core type whose emission is not a fixed subtree, and the
amendment exists because of that: nothing in the record said where a
loop counter lives or whether the list is re-read between passes, and a
block type cannot decide either on its own.

## The compile is a plain Appendix D loop (F1)

Nothing here reaches outside the interpreter's ordinary macrostep
semantics. There is no engine loop construct, no executable-content
`<foreach>` (which cannot hold a body that waits for an event), and no
unrolling: the body's states exist once however long the list is, so its
provenance entries do too.

    <state id="s_blk_F" initial="s_blk_F__head">
      <onentry>
        <assign location="s_blk_F__items" expr="signup.invitees"/>
        <assign location="s_blk_F__i" expr="0"/>
      </onentry>
      <transition event="done.state.s_blk_F__body" type="internal" target="s_blk_F__head">
        <assign location="s_blk_F__i" expr="s_blk_F__i + 1"/>
      </transition>
      <state id="s_blk_F__head">
        <onentry>
          <assign location="invitee" expr="s_blk_F__items[s_blk_F__i]"/>
          <assign location="invitee_index" expr="s_blk_F__i"/>
        </onentry>
        <transition cond="s_blk_F__items[s_blk_F__i] === undefined" target="s_blk_F__o_done"/>
        <transition target="s_blk_F__body"/>
      </state>
      <state id="s_blk_F__body" initial="s_blk_INVITE">
        ...the body slot's children, sequenced...
        <final id="s_blk_F__body_done"/>
      </state>
      <final id="s_blk_F__o_done">...</final>
    </state>

## The loop-back transition is `type="internal"`, and must be

A transition is external by default, and an external transition **exits
and re-enters its own source** even when its target is inside it
(Appendix D's `getTransitionDomain`/`findLCCA`). The loop-back
transition's source is this block's own state, so the external form
would re-run the `<onentry>` above on every pass - re-snapshotting the
list and resetting the cursor to `0` - and the loop would never end.
F4's "re-targets the head" is only true of the internal form. This is
pinned upstream in statifier-ex and by a runtime test here.

## The cursor and the snapshot are the compiler's roots (F2)

`s_blk_<id>__i` holds the cursor and `s_blk_<id>__items` holds a
snapshot of the list, both declared as `<data>` roots through
`StatifierBlocks.Compiler.DeclaredRoots` and both minted through
`StatifierBlocks.Compiler.Context.role_id/2`, so decision 3's
uniqueness keeps them out of any name an author can write.

The snapshot is assigned **once, at the block's `<onentry>`**, which is
what gives the loop SCXML 4.6.3's shallow-copy behaviour: the body walks
the list as it stood when the loop began, and a step inside the body
that writes the source path does not change what is left to iterate.
The cursor is reset in the same `<onentry>` as well as declared with
`expr="0"`, which is what lets a foreach sit inside another foreach's
body and start from the top on each of the outer loop's passes.

## The bindings are declared roots too (F3)

`item_as` and `index_as` are declared `<data>` roots rather than
anything scoped: early binding makes a root global, so they exist for
the whole session, and predicator refuses to read a root nothing
declared. They are re-assigned in the head state's `<onentry>` on each
pass, from `snapshot[cursor]`.

A consequence worth stating: after the loop ends the bindings still hold
their last values - `item_as` holds the out-of-bounds read, `undefined`.
A step after the loop that reads them is reading whatever the loop left,
which is what "global" means and not something this type can narrow.

## Termination, and the limit it carries (F5)

Termination is `snapshot[cursor] === undefined`: predicator indexes
lists and reads out of bounds as `undefined`, and it has no list-length
function, so there is no `i < len(items)` to test instead.

The operator is `===`, strictly. Predicator's loose `==` against
`undefined` evaluates to `:undefined` rather than to a boolean, so the
loose form does not express a termination test at all.

The limit the ruling documents is that a list holding a legitimate
`undefined`/`null` item stops the loop early at that item. **In the
resolved predicator the limit is narrower than that wording**, and this
type is written to the narrower one: `===` is strict, so a `nil` item
does *not* trip `=== undefined` - only an actual `:undefined` does,
which for a list read means only an out-of-bounds index. A list holding
`nil` items therefore iterates to its end, and a runtime test here pins
it.

## Colliding bound names are refused (F6)

A nested foreach re-using its enclosing loop's `item_as`, or an
`index_as` equal to a root an enclosing block declares, would silently
overwrite that binding - early binding makes these roots global, so the
inner loop's writes are visible to the outer body after the inner loop
ends. The compiler refuses the document with a `:duplicate_binding`
Emit-stage finding against the inner block, carrying the config key the
name was typed into. The check is
`StatifierBlocks.Compiler.DeclaredRoots`', not this module's, and it is
a narrow carve-out from decision 9's delegation rather than a general
id-uniqueness check.

### Sibling loops, and what F6 does not cover

F6's carve-out is **nesting**, and it is exactly the case where the
overwrite would otherwise be silent. Two *sibling* loops that both call
their item `invitee` are not that case - neither is inside the other -
but they are still refused, one stage later: both declare a `<data>`
root of the same name, and statifier's id-uniqueness check over the
whole document reports `{:duplicate_id, "invitee"}` against the second
one, mapped to that block and its `item_as` field. That is decision 9's
delegation working as written rather than a second check here, and F6
says as much when it describes itself as pre-empting exactly that
finding for the nesting case.

So a document may not have two loops binding one name anywhere, and an
author renames one of them. **Whether sibling loops should instead
share one declared root is not a question this type may answer**: a
shared root is safe for loops that run one after another and is a race
between the lanes of a `core.parallel`, and picking between them is an
ADR-level decision about the block vocabulary, not an emitter's.

# `emit`

The loop, emitted (F1-F5).

Everything in it is this block's except the transitions leaving the body
slot's children, which `StatifierBlocks.Core.Emit.chain/2` attributes to
the child each one leaves (decision 5). The `items` expression, the
`item_as` location and the `index_as` location are each stamped with the
config field they came from, so an upstream finding inside one is the
author's typo rather than a bug in this type.

# `io`

`core.group`'s `io/1`: a step containing steps.

`produces` is absent rather than `:unknown` - a foreach has one outcome,
so there is no join to refuse, which is the distinction `core.branch`
and `core.invoke` declare `:unknown` for. `consumes` is absent too, and
it is the one place a reader might expect otherwise: a foreach does read
the datamodel through `items`, but that is a config path rather than a
value arriving through the type flow, exactly as `core.invoke` reads its
inputs through `params`.

# `sentence`

This block as one line of prose (ADR-0002's 2026-09-07 amendment).

One constant line. The list this block iterates is a datamodel path
rather than a value, so naming it here would put an expression in a
sentence - the thing ADR-0005's `10n` keeps out of a chip for the same
reason. What a reader needs from a list view is that the body below runs
once per item, and the body is right there under it.

    iex> StatifierBlocks.Core.Foreach.sentence(%{"items" => "cards.batch"})
    "Run once per item"

    iex> StatifierBlocks.Core.Foreach.sentence(%{})
    "Run once per item"

# `slots`

One `body` slot, labelled for what makes this container different from
`core.sequence`: the steps inside it run more than once.

---

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