Parallel Loops
parallel_for, parallel_transform and parallel_reduce run a loop over an array on every core. The engine checks the body when the statement is reached, before any element runs. A body it cannot prove safe is refused with a catchable error that names the construct. It is never run serially in silence.
The model
- An iteration owns its element. The loop variables are the elements:
auto&changes one in place, a by-value variable is the body's copy. Every element is a value, or a handle to an object no other element names. - Everything outside the loop is captured and read-only. The body reads enclosing state as it was when the statement was reached. A write to it is refused.
- Results leave through writes that cannot collide: the element, the same position of a zipped array,
out[i]whereiis a loop variable and every element carries a different index, the arrayparallel_transformreturns, or aparallel_reduceaccumulator. - The rules follow the calls. They cover the functions the body calls, the methods it runs and the constructors and destructors of the objects it makes. An object the body makes belongs to its iteration.
- The worker count never changes the result.
thread_count()reports it: the count the host set, else the machine's performance cores, at most one fewer than its hardware threads.
With deterministic inputs, the elements, the index writes and the error (the first failing element in iteration order) are the serial loop's, on any number of workers. Four things are not the serial loop's:
- A captured
randomgives the same numbers at every worker count, but not the numbers a serial loop would draw (Random numbers). crypto::calls, and arandomthe body makes without a seed, differ on every run, as they do anywhere.parallel_reducefolds by chunk. The result is bit-identical at every worker count, but a float fold is grouped differently from a left-to-right loop, and a non-identityinitis counted once per chunk (Reductions).- After an error, index writes hold up to the failing element, as in the serial loop, but an element changed in place past it may or may not have been written.
A read that would depend on scheduling, such as time::now(), is refused.
parallel_for and parallel_transform
parallel_for runs its body once per element. parallel_transform(arr, fn) returns a new array with out[i] = fn(arr[i]).
var nums = [1, 2, 3, 4, 5, 6, 7, 8];
parallel_for (auto& x : nums) { x = x * x; } // auto& is the element itself
print(to_json(nums));
int inc(int v) { return v + 1; }
print(to_json(parallel_transform(nums, inc))); // a new array; nums is not touched[1,4,9,16,25,36,49,64]
[2,5,10,17,26,37,50,65]An element may be an instance of a class whose fields hold values. Math vectors are values (Math): an array of them, a class holding them and a captured one all qualify.
using math::vec2;
class Particle { vec2 pos; vec2 vel = vec2(0.0, 2.0); }
vec2 gravity = vec2(0.0, -0.5);
array<Particle> sparks;
for (int i = 0; i < 4; ++i) {
Particle p;
p.vel.x = i;
sparks.push(p);
}
parallel_for (auto& p : sparks) {
p.vel += gravity; // gravity is captured: read, never written
p.pos += p.vel; // p is this iteration's element
}
print(sparks[0].pos, " ", sparks[3].pos);(0.0, 1.5) (3.0, 1.5)Counted loops
parallel_for (int i = a; i < b; i++) runs its body once for each value the serial for gives i, with no array made and no memory per element. The form is kept to what provably visits each value once:
- one
int(orauto) variable, never written by the body - compared with a bound by
<,<=,>or>= - stepped toward the bound by
i++,i--,i += kori -= k, withka constant or a name.
a, b and the step are evaluated once, in that order, before any element runs. Any other shape is refused when the script loads. A step that would never end the serial loop, or a last step that would overflow, is refused when the statement is reached.
int n = 8;
array<int> squares = [0, 0, 0, 0, 0, 0, 0, 0];
parallel_for (int i = 0; i < n; i++) { squares[i] = i * i; }
array<int> marks = [0, 0, 0, 0, 0, 0, 0, 0];
parallel_for (int i = n - 2; i >= 0; i -= 2) { marks[i] = 1; }
print(`${to_json(squares)} ${to_json(marks)}`);
int none = 0;
try { parallel_for (int i = 0; i < n; i += none) { } } catch (e) { print(e); }[0,1,4,9,16,25,36,49] [1,0,1,0,1,0,1,0]
parallel_for: a counted loop's step of 0 never reaches its bound (the serial loop would not end)The zip form
The zip form walks several arrays of one length in lockstep, one loop variable per array. To produce several values per element, allocate the outputs before the loop and write them in place. Every zipped array is a separate array of the range's length.
class Body { float x = 0.0; float vx = 0.0; Body() {} Body(float px, float v) { x = px; vx = v; } }
class Sample { float pos = 0.0; bool moving = false; }
array<Body> bodies = [];
array<Sample> samples = [];
for (int i = 0; i < 4; i++) { bodies.push(Body(i * 10.0, i % 3 - 1.0)); samples.push(Sample()); }
parallel_for (auto& b, auto& s : bodies, samples) {
b.x += b.vx * 0.5; // the input, changed in place
s.pos = b.x; // the output slot at the same position
s.moving = b.vx != 0.0;
}
for (auto& s : samples) { print(`${s.pos:.1f} ${s.moving}`); }-0.5 true
10.0 false
20.5 true
29.5 trueparallel_transform is that zip
parallel_transform(xs, fn) is parallel_for (auto& x, auto& o : xs, out) { o = fn(x); } over a fresh out of xs.size() slots.
fnis a script function or lambda that takes its element by value. One that only reads its parameter costs no call and no copy per element. One that changes its parameter changes a private copy, so the source array is never touched.- Any result type works, objects included. The returned array holds copies that alias nothing.
- An optional third argument,
weight_fn, balances the chunks by effort per element. It takes one parameter by value and returns a finite, non-negative number.
class Body { float x = 0.0; float vx = 0.0; Body() {} Body(float px, float v) { x = px; vx = v; } }
array<Body> bodies = [];
for (int i = 0; i < 4; i++) { bodies.push(Body(i * 10.0, 1.0)); }
float next_x(Body b) { return b.x + b.vx; }
Body stepped(Body b) { b.x += b.vx; return b; } // changes its own copy
var xs = parallel_transform(bodies, next_x);
var moved = parallel_transform(bodies, stepped);
print(`${to_json(xs)} moved ${moved[3].x:.1f} source ${bodies[3].x:.1f}`);
var texts = ["7", "42", "1999"];
int digits(string s) { return s.size(); }
float cost(string s) { return s.size(); } // longer strings weigh more
print(to_json(parallel_transform(texts, digits, cost)));[1.0,11.0,21.0,31.0] moved 31.0 source 30.0
[1,2,4]Control flow in the body
continue, and a return at the body's top level, move to the next element. A loop-level break is a parse error, since iterations are independent. Inside a nested loop or switch, break and continue keep their usual meaning, and try/catch works. An element that throws fails the whole call with the first error in iteration order.
var words = ["keep", "", "skip", "keep", "", "keep"];
array<int> kept = [0, 0, 0, 0, 0, 0];
parallel_for (auto& w, auto& k : words, kept) {
if (w == "") { continue; }
if (w == "skip") { return; } // the next element, like continue
k = w.size();
}
print(to_json(kept));[4,0,0,4,0,4]Reductions: parallel_reduce
parallel_reduce(xs, init, step, combine) folds an array on every core:
xssplits into chunks whose size depends onxs.size()alone, never on the worker count.- Each chunk folds its elements in order with
step(acc, x), starting from its own copy ofinit. - The chunk results combine left to right with
combine(a, b). An empty array returnsinit.
init must be combine's identity (0 for a sum, 1 for a product, an empty container for a merge). Every chunk starts from it, so any other value is counted once per chunk: the last line of the sample sums 100 ones from 100.0 and prints 800.0, not 200.0.
stepis a script function or lambda of two parameters and follows the body rules. It takes its element by value. The input may hold class instances, each its own object.- A scalar accumulator suits the by-value form
T step(T acc, x). A container or object accumulator is taken by reference (void step(T& acc, x), or-> T&returningacc) and folds in place with no copy in or out. combineis ordinary code of two parameters. It runs on the calling thread after the chunks join, so it may print or write enclosing state.
array<float> prices = [4.5, 12.0, 7.25, 3.0, 9.5];
float add(float a, float b) { return a + b; }
print(`total ${parallel_reduce(prices, 0.0, add, add):.2f}`);
class Span { float lo = 1000000.0; float hi = -1000000.0; }
void widen(Span& r, float x) { if (x < r.lo) { r.lo = x; } if (x > r.hi) { r.hi = x; } } // folds in place
Span join(Span a, Span b) { if (b.lo < a.lo) { a.lo = b.lo; } if (b.hi > a.hi) { a.hi = b.hi; } return a; }
Span r = parallel_reduce(prices, Span(), widen, join);
float big = parallel_reduce(prices, 0.0, [](float a, float x) { return x > 5.0 ? a + x : a; }, add);
print(`span ${r.lo:.2f}..${r.hi:.2f} big ${big:.2f}`);
array<float> ones = [];
for (int i = 0; i < 100; i++) { ones.push(1.0); }
print(parallel_reduce(ones, 0.0, add, add), " ", parallel_reduce(ones, 100.0, add, add)); // 100.0 counted once per chunktotal 36.25
span 3.00..12.00 big 28.75
100.0 800.0Captured reads
The body reads enclosing scalars, strings, math vectors, arrays and maps, and runs the read-only builtins on them. Every worker sees their content as of the statement.
- A container of primitives or strings that the body only subscripts, or asks
size(),length()orempty(), is read in place with no copy. Anything else captured is copied once per worker, and the copies count toward the memory cap. - A function or method the body calls takes enclosing state through a
const T¶meter as a captured read. A plainT¶meter could write it and is refused; when the function never writes through it, the error names theconst T&to declare. - The lambda given to
parallel_transformorparallel_reducereads its captures the same way.
Writes to enclosing state
Every write to captured state is refused: assignment, compound assignment, ++/--, a mutating or unrecognized method (g.push(...)), a non-const reference that aliases it (var& r = g), an argument to a non-const reference parameter (directly or as an arm of c ? a : b), and for (auto& v : g). A const T& declaration or a const auto& loop reads it.
Index writes
The one exception is out[i] = v (and op=, ++, --) into an enclosing array:
iis a loop variable the body never writes, directly or through aT¶meter.- Every element carries a different index, which the statement checks when it is reached. An index past the array raises the serial loop's error.
- The body touches
outonly asout[i]. Reading another slot would race the writes.
array<int> heights = [3, 9, 4, 7, 1, 8];
int peak(const array<int>& h, int i) { // const T&: a captured read, no copy
int best = h[i];
if (i > 0 && h[i - 1] > best) { best = h[i - 1]; }
if (i + 1 < h.size() && h[i + 1] > best) { best = h[i + 1]; }
return best;
}
int tally(array<int>& h) { return h.size(); } // never writes h, but does not say so
array<int> peaks = [0, 0, 0, 0, 0, 0];
parallel_for (int i = 0; i < 6; i++) { peaks[i] = peak(heights, i); }
print(to_json(peaks));
try { parallel_for (int i = 0; i < 6; i++) { peaks[i] = tally(heights); } } catch (e) { print(e); }
int total = 0;
try { parallel_for (int h : heights) { total += h; } } catch (e) { print(e); }
array<int> order = [3, 3, 1, 0];
try { parallel_for (int slot : order) { peaks[slot] = 1; } } catch (e) { print(e); }[9,9,9,7,8,8]
parallel_for: argument passes enclosing state 'heights' to reference parameter 'h' of 'tally' (captured reads are read-only; 'tally' only reads it: declare it const array<int>& h) at 12:63
parallel_for: body writes enclosing state 'total' at 14:40
parallel_for: elements 0 and 1 write 'peaks' at the same index 3Captured objects
A captured object is read through its fields and methods and written by no iteration.
| Captured | What the body may do |
|---|---|
A value-class instance (fields of primitives, strings, containers of them and nested value-class instances), or a shared_ptr to one | Read its fields; call a script method that writes no part of the object, which the statement checks before any element runs. Each worker reads its own copy. |
| An array or map of value-class instances | The same, per entry: ITEMS[i].cost, for (auto it : ITEMS), ITEMS[i].price(). |
| A C++ class's instance | Every worker reaches the one C++ object, so its binding decides: const methods run unless the host registered them serial (C++ calls). |
| A handle | Used through its members and methods only. |
| Refused | An object holding a handle, a function, a var field or a C++ object; one whose class runs a destructor; a script class over a C++ class; a weak_ptr; a shared_ptr, weak_ptr or C++ object inside a captured container; a range whose elements reach a captured object. |
class Vec3 {
float x = 0.0; float y = 0.0; float z = 0.0;
Vec3(float a, float b, float c) { x = a; y = b; z = c; }
float dot(Vec3 o) { return x * o.x + y * o.y + z * o.z; } // writes nothing of its object
void scale(float k) { x *= k; y *= k; z *= k; }
}
Vec3 sun = Vec3(0.0, 1.0, 0.0);
array<float> light = [0.0, 0.0, 0.0, 0.0];
parallel_for (int i = 0; i < 4; i++) { light[i] = sun.dot(Vec3(i * 1.0, i * 0.5, 0.0)); }
print(to_json(light));
try { parallel_for (int i = 0; i < 4; i++) { sun.scale(2.0); } } catch (e) { print(e); }
try { parallel_for (int i = 0; i < 4; i++) { sun.x = 1.0; } } catch (e) { print(e); }[0.0,0.5,1.0,1.5]
parallel_for: method 'scale' of 'Vec3' writes its object, and captured 'sun' is read-only (write a local copy, or the element) at 11:50
parallel_for: member store reaches captured state 'sun' (captured reads are read-only) at 12:50Calling functions
A body calls named script functions (recursion included), the builtin container and string methods on its own values, and host functions the host did not register serial. The target of a call must be known when the statement is reached:
- A global name that holds a function or a lambda is called directly.
- A call through a local of the enclosing function, a local of the body or a parameter is refused.
- A function read as a value, to store it or to pass it, is refused. A body writes no lambda of its own.
int twice(int v) { return v * 2; }
var bump = [](int v) { return v + 1; }; // a global that holds a lambda
array<int> xs = [1, 2, 3, 4];
parallel_for (auto& x : xs) { x = bump(twice(x)); }
print(to_json(xs));
int apply(var f, int v) { return f(v); }
void local_target() {
var pick = twice;
try { parallel_for (auto& x : xs) { x = pick(x); } } catch (e) { print(e); }
}
local_target();
try { parallel_for (auto& x : xs) { x = apply(twice, x); } } catch (e) { print(e); }
try { parallel_for (auto& x : xs) { var keep = twice; } } catch (e) { print(e); }
try { parallel_for (auto& x : xs) { print(x); } } catch (e) { print(e); }[3,5,7,9]
parallel_for: call target 'pick' is not a function known at admission time at 10:45
parallel_for: call target 'f' is not statically resolvable in a parallel body at 7:34
parallel_for: captured name 'twice' is a function value (only direct calls to statically-known functions are allowed in a parallel body)
parallel_for: host function 'print' is marked serial_function() and cannot be called in a parallel body at 15:37C++ calls
Bound C++ functions, static methods and methods run on the worker unless the host registered them serial_function, serial_static_method or serial_method (the binder).
- A serial function or static method refuses the statement before any element runs. A serial method is refused when it is called, since its receiver's class is known only then. The mark covers the method's non-const overloads, so a const getter of the same name stays parallel.
- A non-const C++ method, or a store to a C++ property, runs only on an object the body owns: its element, a
parallel_reduceaccumulator, or an object it made. On any other object only const methods run. - The library's builtins follow the same rule. The table lists the serial ones;
printis among them.
Constructing objects
A body constructs script classes in every spelling: Hit(r, d), Hit h(r, d);, Hit h;, new Hit(r, d), and the converting constructor a typed slot runs (Box b = x;, an argument, a return, a store). The new object belongs to the worker that made it, so its constructor writes its fields freely. Everything else the construction runs follows the body rules, and the statement checks it before any element runs:
- every constructor the call's argument count admits
- the base constructors its initializer list names (an unnamed base is default-constructed)
- each level's field initializers
- the destructor of every class whose instance can die in the body: an object the body makes, an element it replaces, and whatever their fields hold.
An error from this code names where it sits: a constructor, a field initializer or a destructor.
string PREFIX = "hit-";
class Vec { float x = 0.0; float y = 0.0; Vec() {} Vec(float a, float b) { x = a; y = b; } }
class Hit {
int ray = -1;
Vec at;
string tag = PREFIX;
Hit() {}
Hit(int r, float d) { ray = r; at = Vec(d, d * 2.0); tag = tag + to_string(r); }
}
array<Hit> hits = [Hit(), Hit(), Hit(), Hit()];
parallel_for (int r = 0; r < 4; r++) { hits[r] = Hit(r, r * 0.5); }
print("{} {} {}", hits[3].tag, hits[3].at.y, hits[0].ray);hit-3 3.0 0C++ classes and hybrids
A class the host binds, and a script class over one, construct in a body too: the host's constructor and destructor are taken to be safe on any thread. A host class whose constructor or destructor touches shared state (a registry, a cache, a service) is bound with .serial_only_constructor() (the binder), and a body that constructs it, or a script class over it, is refused before any element runs. An object of such a class that dies on a worker anyway is released on the calling thread when the workers are done, in element order.
Random numbers
| The generator is | What a draw gives |
|---|---|
| Captured from outside the loop | The statement takes one draw from it to seed a stream per element (per chunk for parallel_reduce). A stream depends only on that draw and the element's index: the same numbers at every worker count, not the numbers a serial loop would draw. After the loop the generator has moved one draw. Reseeding it in the body is refused. |
Owned by the element (an array<random> zipped beside the data, or a field of the element's class) | Its own sequence, as anywhere. Two elements holding one generator are refused. |
| Made in the body | Its own sequence: reproducible when the body seeds it, different on every run when it does not. |
crypto:: runs in a body too, apart from crypto::sha256_file, and is never reproducible. Random Numbers & Crypto has the generator's side of these rules.
random loot(5);
array<int> drops = [0, 0, 0, 0, 0, 0, 0, 0];
parallel_for (auto& d : drops) { d = loot.next_int(1, 100); } // captured: one stream per element
print(drops.join(" "));
print(loot.next_int(1, 100)); // loot moved one draw for the loop
array<random> brains = [random(1), random(2), random(3)]; // owned: one generator per element
array<int> hp = [10, 10, 10];
parallel_for (auto& h, auto& brain : hp, brains) { h -= brain.next_int(1, 6); }
print(hp.join(" "));
try { parallel_for (auto& d : drops) { loot.seed(d); } } catch (e) { print(e); }51 17 31 91 93 79 47 67
61
5 9 5
parallel_for: random::seed cannot be called on captured 'loot' in a parallel body (seed before the loop: each element draws its own stream from it) at 12:45Handles as elements
An array of shared_ptr<T> or weak_ptr<T> elements runs when no object appears at two elements of the range and its zipped arrays, which the statement checks when it is reached. Its class must still be a flat value class. Another holder of the object elsewhere in the program is fine, since nothing else runs during the loop.
Choosing a shape
Use captured reads for shared inputs, a zipped output array or out[i] for per-element results, and parallel_reduce for a total. Where the output shape is not known up front, elements shaped [input..., tag] come back from parallel_transform as [tag, outputs...], collected from the result in iteration order.
What a parallel body may do
One row per construct. The reference lists every refusal text with its cause (Parallel bodies); the last column links each row to its texts there.
- A text that starts with
parallel_for:is raised when the statement is reached, before any element runs.parallel_transformandparallel_reduceraise the same texts under their own names. - A text without the prefix is raised when the body runs the construct, because only then is the receiver known.
- Most texts end with the position of the construct,
at LINE:COL.
| In a body | Rule | Refused with |
|---|---|---|
| Elements | A primitive, a string, a math vector, a value container, an instance of a flat value class (typed fields holding values, vectors included, no handles), or a handle to an object no other element names. A bound C++ value such as a random counts when no other element holds the same object. A reference to a member of the element or of a local (auto& p = b.pos;), and a by-reference loop over a map they hold (for (auto& [k, v] : b.m)), write it as the serial loop does. | not value-semantic |
| Handle elements | No object at two elements of the range and its zipped arrays (Handles as elements). | reference |
| The range and zipped arrays | The range is an array. Every zipped array is a separate array of the range's length. | range not an array, zipped not an array, zipped length, zipped range |
| Counted loops | One int variable the body never writes, compared with a bound, stepped toward it by a fixed amount (Counted loops). | reference |
| Index writes | out[i] into an enclosing array, i an unwritten loop variable, a different index per element, out touched no other way (Index writes). | reference |
| Reductions | step and combine are script functions or lambdas of two parameters; step follows the body rules and takes its element by value; each input element is its own object (Reductions). | reference |
parallel_transform's functions | fn takes its element by value; weight_fn takes one parameter by value and returns a finite, non-negative number (above). | fn not a script function, fn by reference, weight parameter, weight not finite |
| Captured reads | Allowed (Captured reads). A captured vector's components read by name (gravity.y, table[1].x). The read-only builtins run: size, empty, front, back, index_of, has, contains, first, last, length, slice, join, keys, values, get, to_array, at, substr and the find family. A subscript read of the range's own array would race the element writes, and so would a const T& of it or of an index-written array. | mutating method, read of the range |
| Writes to enclosing state | Refused in every shape but the index write (Writes to enclosing state). A member or element of enclosing state is that state, and so is a default a call leaves to bind to a reference parameter. | enclosing write, by-reference loop |
| Captured objects | Read only; no element may reach one (Captured objects). Every other object an iteration writes is its element or one it made. this exists only inside a method the body runs. | mutating method, captured weak_ptr |
| Constructing script classes | Allowed in every spelling; all the code a construction can run follows the body rules (Constructing objects). A conversion the statement could not see is refused when it runs, never run unchecked. | reference |
| Constructing C++ classes | Allowed, a script class over one included, unless the host bound it with serial_only_constructor() (C++ classes and hybrids). | reference |
| Script class methods | A method runs when it is the only overload of its name on the element's class or the base it inherits it from, the class has no private members and the method body follows the body rules. Inside it a sibling method is called as this.name() or by its bare name; a C++ base method keeps the this. spelling. A method of an object reached through a field is not admitted. | private members, bare base call |
| Operators | A class's operators run on the worker: arithmetic, comparison and <=> (reversed forms included), unary - + ~, compound assignment, [], operator=, and the conversions into a typed slot (to_string, to_bool, to_int, to_float, to_char). A script operator's overloads and what they construct follow the body rules; one that breaks a rule is refused when it runs. A C++ operator follows the host-call rules, so += and the other non-const ones run only on an object the body owns. | reference |
| Enums and class constants | E::v reads an enum value and C::K a static const of a number, bool or char, in the body or a method it runs. | reference |
| Accessors, class changes | A user getter or setter, and an assignment that changes an object's class, are refused when they run. | reference |
| Script function calls | A direct call to a named function (recursion included) or to a global that holds a function or lambda, and the builtin container and string methods on the body's own values. Refused: a call through a local or a parameter, and a function read as a value (Calling functions). | enclosing local, parameter or body local, function value |
| Host calls | Parallel unless registered serial; non-const methods and property stores only on an object the body owns (C++ calls). The library's serial builtins: print, sleep, extend, a nested parallel_transform or parallel_reduce, the archives (to_json, from_json, to_binary, from_binary, to_base64, from_base64), the file and folder lookups (resolve_path, file_data, app_data_dir, app_cache_dir, import_recheck_interval), the namespace forms that call back into script (array::map, array::filter, array::sort, map::filter), crypto::sha256_file, HTML template rendering, and every reflect::, time:: and spider:: entry. A namespaced host function follows the same rule (string::repeat runs). A static method of a script class is refused. | serial function, script static method |
| Objects from host calls | An object a host call returns that the program already holds (a C++ object's wrapper or script layer) is shared. The body reads its fields, each read a copy; an object a field holds is shared too, and its C++ class's const methods run. A store into it and a script method on it are refused when they run. ==, a conversion into a typed slot, a condition and its text read the worker's own copy, on which its class's operators and conversions run and which never runs a destructor. Kept in the element, in out[i] or in the result, it is the object itself after the loop. | reference |
| Signals | connect and disconnect on a Signal are serial methods. A C++ call that fires a signal runs its receivers on the worker that made the call, so a host marks such a method serial_method when its receivers reach shared state (Signals). | reference |
| Lambdas | A body writes no lambda. fn, step and combine may be lambdas. | lambda |
yield | A body neither yields nor calls a coroutine method. | yield, coroutine method |
time:: | The clock is engine-owned, and a read would depend on scheduling (Time and parallel loops). | reference |
| Random numbers | Draws are allowed; reseeding a captured random is not (Random numbers). | reference |
| Nested parallelism | A body, and every function it calls, runs no parallel loop of its own. | reference |
| Declarations and modules | A body declares no function, class, namespace or enum, and includes or imports nothing. | declaration, include/import |
| Control flow | continue and a top-level return move to the next element; a loop-level break is a parse error (Control flow). | loop-level break |
| Reference casts | cast<T&> would bind a reference the proof cannot follow. | reference cast |
| Errors and limits | The first error in iteration order fails the call. The execution budget and the memory cap hold inside a body (Safety Rails): a budget overrun is terminal as anywhere, and copies made for the workers count toward the memory cap. | the element's own error |