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

The scope-shaped cancel: a delayed send armed by a child block is
cancelled in the `<onexit>` of the state that armed it (ADR-0004's
2026-08-29 amendment, "a delayed send's cancel, emitted in the arming
state's `<onexit>`").

## Which state is "the state that armed the send"

The **nearest enclosing scope state**: the innermost `<state>` in the
parent's own emission that contains the child, whichever state that
turns out to be.

For most block types that is the parent block's own state, because
ADR-0004 decision 2 gives one block exactly one state and a child's
emission is spliced directly inside it: a `core.send` in a
`core.sequence` body is cancelled by the sequence, and one nested a
sequence deep inside a `core.group` is cancelled by the sequence rather
than the group, because the sequence is nearer.

A block type whose emission puts its children inside a **region** is the
case that makes "nearest" do work of its own, and there are two of them
in this vocabulary. An interruptible `core.group` puts its body in a
region, and abandoning the group takes an *internal* transition to the
group's own final - which exits the body region without exiting the
group, so a cancel on the group's `<onexit>` never fired for it. That one
is a fix as much as a move. `core.parallel`'s lanes are
regions of its `<parallel>`, and a lane is a scope: leaving the lane -
which under `complete: first` is exactly what the losing lanes do - has
to reach the sends that lane armed. So a lane child's cancel is emitted
in the *region's* `<onexit>`, not the wrapper's, and that is ADR-0004's
`complete: first` amendment, P3, applied to both completion rules at
once. A delayed send in a lane of an `all` parallel is cancelled when its
region is exited for exactly the same reason.

Only a `<state>` claims. A `<parallel>` between the child and its nearest
state does not, which is what keeps an interrupt rail's cancel on the
enclosing group's state where the record puts it ("the enclosing group
for a rail"): a rail child is a region of the group's `<parallel>`, and
the nearest *state* above it is the group's own. The one exception is a
block whose emission is itself rooted at a `<parallel>` - not something
this package emits, since decision 2 makes every block's emission a
`<state>` - which is still offered the cancels rather than dropping them
on the floor, in the same spirit as decision 1's "a finding, never a
raise".

A send block that is the document's root has no enclosing scope and
therefore no cancel. Nothing above it is a state this package emits.

## How a scope learns what its children armed

Not through the child's SCXML, which ADR-0004 decision 4 keeps out of a
parent's reach, and not through a new `emit/2` callback either. Through
the **send id**, which is a state id in disguise: ADR-0002's amendment
of the same date fixes it as `<the send block's state id>__send`, and
`StatifierBlocks.Compiler.StateId.unstate_id/1` inverts that back to
`{block id, "send"}` exactly. So this pass reads each direct child's
compiled emission, keeps every `<send>` whose `id` inverts to *that
child's own block id* under the reserved role `"send"`, and
emits one `<cancel sendid="...">` per hit in the scope's `<onexit>`.

Three properties follow from reading the id rather than the emitter:

  * **A grandchild's send is not this scope's.** Its id names the
    grandchild, so it fails the direct-child test here and was already
    cancelled by the nearer scope on its own pass. No send is cancelled
    twice and none is missed.
  * **`core.wait`'s timer is one of these sends.** It mints its own
    delayed `<send>` under this same reserved role rather than a role of
    its own, so a wait left before its delay elapses is cancelled by its
    scope exactly as a `core.send` is. The wait's own state bounds only
    what the interpreter holds: a delayed send a durable host has already
    scheduled outlives that state, and would fire into a run that no
    longer wants it.
  * **A host block type opts in by minting the same role.** Nothing here
    names `StatifierBlocks.Core.Send`, so a host type that arms a
    cancellable delayed send gets the scope cancel by using
    `role_id/2` the way `core.send` does.

Only a send carrying a `delay` is cancelled. An undelayed `<send>` is on
the external queue before the enclosing state can be exited, so a
`<cancel>` for it would be bytes that can never match a pending timer.

## Which scope a child belongs to, and how it is found

The parent's emission carries a `{:child, block_id}` placeholder wherever
a child's subtree will be spliced, so the parent has already said where
each child sits. The walk below reads that: one post-order pass over the
emission, in which a placeholder reports its own block id upward and each
`<state>` claims the armed ids reported from inside it before the rest
travel further up. Nothing here reads a slot name, and nothing names a
block type - a host type that arranges its children into regions of its
own gets per-region cancels by placing its placeholders, exactly as
`core.parallel` does.

## Determinism

Children are visited in slot-declaration order and then document order -
the order `StatifierBlocks.Compiler` already hands them over - and within
one scope the cancels are emitted in that order, so ADR-0004 decision 6
holds. The `<onexit>` is prepended to its scope state's children, ahead
of the transitions the type wrote, which is one fixed position rather
than a position that depends on what the type emitted. A block type that
wrote an `<onexit>` of its own keeps it; SCXML runs both.

The pass runs on the parent's own emission *before*
`StatifierBlocks.Compiler.Attribution` stamps it, so the `<onexit>` and
its cancels are owned by the scope block and carry the scope state's
role, which is the block an author would recognise: the cancel is a
consequence of where the send sits in that scope, not of the send block
itself. That stays true when the scope is a region: the region is a state
the same block emitted.

# `arm`

```elixir
@spec arm(StatifierBlocks.Emission.t(), [
  {StatifierBlocks.Block.id(), StatifierBlocks.Emission.t()}
]) ::
  StatifierBlocks.Emission.t()
```

Adds the scope `<onexit>` cancels to `emission`, the emission of the
block that `compiled_children` are the direct children of.

`compiled_children` is the `{block id, emission}` list the compiler
already holds after emitting the children. Returns `emission` unchanged
when no direct child armed a delayed send - which is every scope in
every document that contains no `core.send`, so no existing chart moves
a byte.

# `armed_role`

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

The role a cancellable armed send's id carries: `"send"`.

`StatifierBlocks.Core.Send` mints its send id with
`StatifierBlocks.Compiler.Context.role_id(context, armed_role())`, and
this module reads it back, so the two halves of the convention name one
string in one place.

---

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