StatifierBlocks.Compiler.Interrupts (StatifierBlocks v0.30.0)

Copy Markdown View Source

The group-scoped interrupt pair: a <raise> of the interrupt protocol emitted inside a group's rail is rewritten to that group's own salted event name (ADR-0010 decision 8).

The defect this closes

The protocol is two strings - StatifierBlocks.Core.Emit's interrupt_events/0 - and before decision 8 every interruptible group emitted the same two transitions matching them. One name at every nesting depth means the rails of two nested groups listen for the same event, and only the raise's position in the active configuration decides which of them takes it. That is right for a raise from the inner group's own rail and wrong for a raise from the outer group's rail while a second railed group is active in the outer group's body: the inner rail's source sits deeper, so it takes the outer group's resume and the outer group's history re-entry - the whole point of core.resumable_group - never fires.

StatifierBlocks.Core.Emit.interruptible/2 emits the group's own two transitions already salted with its state id. This pass is the other half: the raises inside the rail, which the group cannot reach when it emits, because a parent never receives its children's SCXML (ADR-0004 decision 4) - what it emits in a handler's place is a {:child, block id} placeholder. By the time the compiler has the children's compiled emissions in hand, it also has the parent's own emission saying where each of them sits, and that is exactly what this pass reads.

Which raises are a rail's, structurally

Nothing here names a block type, in the same spirit as StatifierBlocks.Compiler.Cancels. The parent's own emission is walked for a <state id="X"> that carries a transition on Emit.interrupt_events(X) - a state matching its own salted pair is a group's rail-bearing state, and X is the salt. Its rail children are the {:child, _} placeholders sitting directly in the <parallel> under it: the body is a <state> region the group emitted, so a body child's placeholder is nested inside that state and is never one of these. A host group type that builds its rail through Emit.interruptible/2 gets the scoping for free, and one that arranges regions of its own gets it by placing its placeholders the same way.

Every <raise> of the bare pair anywhere under a rail child - the handler's own state, and any depth within it - is rewritten. That is decision 8b's "anywhere inside that group's rail subtree", and it is what makes a host block type on a rail scoped on exactly the same rule as core.on_event (8d): the rewrite is a property of the position in the emitted chart, not of which type emitted the raise.

Why it is idempotent, and why nesting composes

A child's own pass has already run by the time its parent sees it, so an inner group's rail raises arrive here already salted with the inner group's id. Those names are not the bare pair, so this pass does not touch them: an inner rail keeps its own salt and the outer salt is applied only to what is still bare. The two halves - the transitions in Emit and the raises here - therefore move together at every depth.

What is not rewritten

A raise of the bare pair that is not inside a rail (decision 8c). It reaches no rail today and none after, and leaving it alone is what keeps the compiled bytes of a document with no interruptible group identical to what they were before this pass existed - a document with no rail has no rail child, so scope/2 answers with the children it was handed.

Summary

Functions

Salts the interrupt raises inside emission's rail children.

Functions

scope(emission, compiled_children)

Salts the interrupt raises inside emission's rail children.

emission is one block's own emission, before its children are spliced in, and compiled_children is the {block id, emission} list the compiler already holds after emitting them. Returns compiled_children with each rail child's subtree rewritten, and returns it unchanged when emission carries no rail - which is every block in every document that holds no interruptible group, so no such chart moves a byte.