Sutra GitHub
← Sutra home Sutra

Loops

Sutra has two loop forms:

  1. loop (N) { body } — bounded compile-time unroll. The body is emitted N times in sequence; no runtime iteration; iterator inside the body is the per-copy compile-time constant. This is the preferred form for any loop with a known iteration count.

  2. Loop function declarations (do_while, while_loop, iterative_loop, foreach_loop) — first-class declared functions whose parameters are the recurrent state. The body uses pass to yield the next iteration’s values. Call sites use the loop prefix. This is the form for loops with runtime data dependence.

The retired form is the C-style imperative loopwhile(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’s x by 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, ...);
  • loop is the call prefix; the named function NAME must 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) — no slot declaration 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 String state — a String or vector accumulator 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 done flag derived from the condition (or array exhaustion, or iteration count).
  • Soft-halt sigmoid + monotone cumulative + soft-mux freeze: once done crosses 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.