Loops
Sutra has two loop forms:
-
loop (N) { body }— bounded compile-time unroll. The body is emitted N times in sequence; no runtime iteration;iteratorinside the body is the per-copy compile-time constant. This is the preferred form for any loop with a known iteration count. -
Loop function declarations (
do_while,while_loop,iterative_loop,foreach_loop) — first-class declared functions whose parameters are the recurrent state. The body usespassto yield the next iteration’s values. Call sites use theloopprefix. This is the form for loops with runtime data dependence.
The retired form is the C-style imperative loop — while(cond) { body }, do { body } while(cond), for(init; cond; step) { body }. Those compiled to host-side branches and didn’t survive the 2026-04-30 substrate-purity audit. They’re neither a counter loop nor a tail-recursive cell, which is why they didn’t fit.
The two forms above DO fit, for different reasons: loop (N) runs at compile time (no host branches needed), and the declared-function loops compile to substrate-pure RNN cells (no host counter, soft-halt mask freezes state on the substrate).
| Form | When to use |
|---|---|
loop (N) { body } |
Compile-time-known iteration count. Body is emitted N times; iterator substitutes in. |
do_while NAME(...) |
Body runs once before the condition check; condition re-evaluated each tick. |
while_loop NAME(...) |
Condition checked before each tick; body skipped if false at entry. |
iterative_loop NAME(...) |
Run N times, body sees iterator (N can be runtime). |
foreach_loop NAME(...) |
Walk a Sutra binding-array; body sees element. |
do_while
Body runs once before the first condition check. Re-evaluates after each tick.
do_while addNumber(x < 11, int x) {
pass x + 1;
}
function int main() {
slot int x = 9;
loop addNumber(x < 11, x);
return x; // 11
}
do_while addNumber(...)— declaration. First param is the condition expression; remaining params are the recurrent state vars.pass x + 1;— tail-recursive yield. Provides one value per recurrent state param, in declaration order. The condition is re-evaluated automatically against the new state, not passed.loop addNumber(x < 11, x)— call site. Mutates the caller’sxby reference on completion.
Two equivalent body forms — the compiler accepts both:
// Form A: pass an expression directly.
do_while addNumber(x < 11, int x) {
pass x + 1;
}
// Form B: mutate then pass.
do_while addNumber(x < 11, int x) {
x = x + 1;
pass x;
}
replace keeps the input value
When a state param shouldn’t update on an iteration, use replace in its pass slot — that keeps whatever value the loop was called with for that param.
while_loop
Same as do_while but the condition is checked before each tick. Body is skipped entirely if the condition is false at entry.
while_loop drainQueue(count > 0, int count) {
pass count - 1;
}
iterative_loop
Runs N times. Body sees the iterator keyword, which is 1-indexed and ranges from 1 to N.
iterative_loop sumToN(5, int n) {
pass n + iterator;
}
function int main() {
slot int n = 0;
loop sumToN(5, n);
return n; // 0 + 1 + 2 + 3 + 4 + 5 == 15
}
iterator is contextual — only meaningful inside an iterative_loop body. It is never a runtime variable in the host sense; the substrate sees it as part of the cell’s per-tick state.
foreach_loop
Walks a binding-array (Sutra’s array form: arr[0] is the length, arr[1..length] are the elements). Body sees the element keyword bound to the current item.
foreach_loop applySteps(steps, vector x) {
pass element(x);
}
The array has to be a Sutra binding-array (constructed via array_from_literal or read from another binding-array operation). See Memory for binding-array semantics.
Call-site shape
loop NAME(cond_or_count_or_array, state1, state2, ...);
loopis the call prefix; the named functionNAMEmust be a declared loop function.- The call site mutates the caller’s named variables for each state param. The state vars must be
slot-declared at the caller. - Loop functions have no outer-scope access — they’re pure functions over their declared parameters only.
Expression form — a loop call that returns its final state
A single-state loop can also be called in expression position, where it
evaluates to the loop’s final state instead of mutating a slot variable by
reference:
int x = loop addNumber(x0 < 11, x0); // x is the final state
return loop addNumber(x0 < 11, x0); // return the loop's result directly
- The state argument is an ordinary expression here (e.g.
x0,4 + 5) — noslotdeclaration and no by-reference variable are needed. - The value is the same final state the by-reference form would write back; only the plumbing differs.
- It carries vector and
Stringstate — aStringorvectoraccumulator survives tick-to-tick:
sutra
iterative_loop build(3, String acc) { pass string_concat(acc, make_string("x")); }
function string main() { return loop build(3, make_string("")); } // "xxx"
The by-reference statement form carries vector and String state too — slots hold full
vectors, so slot String acc = make_string(""); loop build(3, acc); works identically.
Pick the form by style: the expression form when you want the result as a value, the
by-reference form when you want the variable updated in place.
- Multi-state loops use the tuple-destructure form — bind each final state to a
newly-declared local:
sutra
while_loop step((n > 0) && (n != 1), int acc, int n) { acc = acc + n; pass acc, n - 1; }
function int main() {
(total, remaining) = loop step((3 > 0) && (3 != 1), 0, 3); // total = 5, remaining = 1
return total + remaining;
}
Sutra has no general tuples, so a (a, b) target on the left of = is only valid for a
loop call. A single-value int x = loop f(...) on a multi-state loop reports a diagnostic
steering you to this form.
The by-reference statement form remains valid; the value-returning forms above (single-state expression + multi-state destructure) are the idiomatic way to consume a loop’s result.
Substrate execution
Under the hood, each loop kind compiles to a fixed-T tensor-op unroll where T is the runtime compute budget. Each “tick” is one cell evaluation:
- The cell function takes the current state and emits the next state plus a
doneflag derived from the condition (or array exhaustion, or iteration count). - Soft-halt sigmoid + monotone cumulative + soft-mux freeze: once
donecrosses the threshold, subsequent ticks copy the current state forward, so the final output is the state at the moment of completion. AXIS_LOOP_DONE(a reserved synthetic axis) carries the completion flag through the unroll.
Result: the host runs the unroll once; the substrate sees T inline cell evaluations regardless of when the logical loop terminated. No counter lives on the host.
Choosing between them
| you want | use |
|---|---|
| body must run at least once, then check | do_while |
| check first, possibly skip the body | while_loop |
| run N times | iterative_loop |
| iterate a Sutra binding-array | foreach_loop |
The common theme: every Sutra loop is a substrate-resident RNN cell. Termination is on the substrate (a soft-halt mask), not a host counter.