Statifier.Machine.Content.Foreach (Statifier v2.0.0)

Copy Markdown View Source

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's item binding], 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

t()

@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()
}