A compiled <foreach> executable-content node (spec 4.6) - the interned
counterpart to Statifier.Document.Foreach. array is the compiled
Machine.expr() the iteration source evaluates once, before any
iteration (spec 4.6.3's shallow copy - see step 3 in execute/2's doc
below for why one evaluation is enough). item/index are the raw
attribute strings - bare variable names by 4.6.2, so neither is compiled
or resolved any earlier than execute/2. content is the dense,
document-order c_index list of <foreach>'s own body, resolved through
Statifier.Machine.content/2 at runtime rather than carried inline
(Statifier.Compiler's Decision 2).
4.6.3's three MUST sentences this node implements (the platform's own execution-failure notification - ADR-0003 - is what "place the error ... in the internal event queue" below becomes, one layer up from this file; see the "no dispatcher anywhere else" note at the end of this moduledoc for why that conversion never happens here):
If the SCXML processor does not support the type of the array or if the evaluation of the array expression fails, the SCXML processor MUST terminate execution of the
<foreach>element ... and place the error in the internal event queue.Modifications to the collection during the execution of
<foreach>MUST NOT affect the iteration behavior. ... If a shallow copy of the collection is possible, the iteration is done on the copy. Otherwise the iteration is done in such a way that iteration behavior is consistent with the use of a shallow copy.If the SCXML processor encounters an error while evaluating [the
<foreach>element'sitembinding], it MUST cease execution of the<foreach>element and the block that contains it, and it MUST place the error in the internal event queue.
Why this node's execute/2 does not recurse through the block runner,
and does not write through <assign>'s path machinery
See Statifier.Machine.Content.If's moduledoc for the four reasons a
composite node's execute/2 folds its own children directly rather than
calling back into Statifier.Interpreter.Content's block runner - they
apply here unchanged, and reason 1 is more binding for <foreach>,
since a write to item/index (or a body <assign>) in iteration n
MUST be visible in iteration n + 1 within the same <foreach>.
item/index are declared and written directly against the raw
machine_state.datamodel with Map.put_new/3 / Map.put/3 - never
through Predicator.ContextLocation.put/3, the machinery
Statifier.Machine.Content.Assign uses. <assign>'s check_root/3 would
refuse the very write 4.6.3 requires (an undeclared item/index
becoming declared is the whole point of the corpus's test150/test151),
and item/index are always bare names by Decision 1 below, so there is
no path to resolve in the first place.
Decision 5: the shallow copy is free
array is evaluated exactly once, before the first iteration
(evaluate_array/1 below), and the resulting value becomes the iteration
source for the whole loop. In an immutable language this already is
the shallow copy 4.6.3 asks for: an Elixir list is a value, so simply not
re-evaluating array per iteration satisfies "modifications to the
collection during the execution of <foreach> MUST NOT affect the
iteration behavior" with no copy step of its own. Do not move this
evaluation into the loop, and do not add a defensive Enum.to_list/1 or
similar around it - both would be redundant work solving a problem this
language does not have.
Two-element versus three-element error forms
Steps 1-4 of execute/2 (item legality, index legality, array
evaluation, iterability) run before anything is declared or written, so
a failure there returns the plain two-element {:error, reason} form -
there is nothing yet to preserve. Step 6 (the loop itself) has, by the
time any iteration's body can fail, already declared item/index and
possibly written earlier iterations' mutations into
machine_state.datamodel; 4.6.3 requires those to survive a later
iteration's failure (the corpus's test156), so a body failure returns
the three-element {:error, context, reason} form instead, carrying the
context as it stood at the moment of failure. This is the single subtlest
line in this file - do not collapse the two forms into one.
Its Statifier.ExecutableContent implementation lives right below the
struct: this file is the whole node, top to bottom, with no dispatcher
anywhere else in the tree.
Summary
Types
@type t() :: %Statifier.Machine.Content.Foreach{ array: Statifier.Machine.expr(), array_location: Statifier.Parser.Location.t() | nil, c_index: non_neg_integer(), content: [non_neg_integer()], index: String.t() | nil, index_location: Statifier.Parser.Location.t() | nil, item: String.t(), item_location: Statifier.Parser.Location.t() | nil, location: Statifier.Parser.Location.t() }