Skip to main content

Loops and iterators

12. Iterators and Loops​

Iterator Loops​

The fundamental loop construct in Tungsten iterates a variable over a half-open range [start, end):

i in [0, N) a[i] <- a[i] + 1;

This iterates i from 0 to N-1 (inclusive).

Decreasing Iterators​

Iterate in reverse using the (end, start] syntax, or equivalently by marking the iterator with ↓ on a [start, end) range. Both spell the same decreasing range: it visits the values of [start, end) in descending order, from end-1 down to start inclusive, and follows the same length rules as an ascending range (see below): it is empty when the bounds are equal, and runtime bounds with start > end are undefined behavior.

i in (N, 0] a[i] = a[i] - 1; // iterates N-1, N-2, ..., 0
i↓ in [0, N) a[i] = a[i] - 1; // the same range, the same order

The bracket shape sets the direction, not the order of the numbers: [lo, hi) walks up and (hi, lo] walks down. So i in (0, 4] is the downward range from -1 to 4, which is empty, and i in [4, 0) is empty too; Carbide accepts both without a warning. [0, 4] and (0, 4) are syntax errors. To walk 1 to 4 upward, write [1, 5).

A loop reads the bound its walk starts from once, as the loop begins, and tests the bound it walks toward before each step: i in [lo, hi) reads lo once, and i in (hi, lo] reads hi once. Carbide does not yet do this for every loop: a loop that compiles to a counted hardware loop reads its end bound only once, while one with a branch in its body, such as an if, reads it before each step. Do not change a loop's end bound inside its body. A multi-level loop is a nest: each level begins at every step of the level enclosing it and reads its start bound then, so ∀i in [0, n) ∀j in [s[i], m) reads s[i] once per i.

Unicode For-All (∀)​

∀ marks an iterator that was declared earlier and is being applied here. It is required in that case and may be omitted when the iterator is declared inline. ∀ (U+2200) has no ASCII spelling; ∞ may be written INF.

i in [0, N) j in [0, M) matrix[i][j] = i * M + j; // declared inline: no ∀

let i in [0, N);
let j in [0, M);
∀i ∀j matrix[i][j] = i * M + j; // already declared: ∀ required

Naming already-declared iterators without ∀ is an error:

i j matrix[i][j] = i * M + j; // error

The error does not mention ∀: an iterator name without ∀ is read as the start of a declaration. After let i in [0, 2);, i o[i] = 5; fails with duplicate array declaration when an array o exists, and with Invalid type definition when none does.

The prefix also accepts a range directly:

∀i in [0, N) a[i] = a[i] + 1;

The descending variant marks the iterator with ↓ on a [start, end) range, or uses the (end, start] form without ↓:

∀i↓ in [0, N) a[i] = a[i] - 1;
∀i in (N, 0] a[i] = a[i] - 1;

Infinite Iterators​

An iterator over [0, ∞) runs one fabric transfer without bound. Its index may subscript the transfer's memory operand, so each pass moves the next element:

i in [0, ∞) a[i] <- rx[]; // a[0], a[1], ... receive successive wavelets

Nothing stops the index at the end of a: each wavelet that arrives is stored at the next address, past the array when more arrive than it holds. Use this form only when the sender sends no more than the array holds.

The body is that one transfer, and the index appears only as such a subscript. An infinite iterator may also be the outer dimension of a transfer: i in [0, ∞) j in [0, 2) tx[] <- zz[j]; sends zz[0], zz[1], zz[0], zz[1], … without end. It may be the outer dimension of a memory iterator, which then cycles through its memory. To repeat any other body without bound, use forever.

Named Iterators (let / range)​

You can declare iterators separately and reference them later:

let i in [0, N);
let j in [0, M);

∀i ∀j matrix[i][j] = i * M + j;

range is a synonym for let: range i in [0, N);.

End each let with ;. Without it, Carbide applies the let to the next statement as a loop, and i is not declared after it: let i in [0, 4) followed by ∀i a[i] <- a[i] + 10; adds 40 to each element, not 10.

A let names the range, not its values: each use reads the bounds when it starts. After let i in [0, n); n <- 4;, ∀i walks 0 to 3.

A bare reference walks its declaration: its range, and its direction unless the reference writes ↓. The same holds for a ∑ iterator.

let k in (N, 0];
let j in [0, N);

∀k a[k] = t; // k walks N-1, ..., 0, as declared
∀j↓ b[j] = t; // j walks N-1, ..., 0, as written
c[0] <- ∑ k x[k] * y[k]; // sums in the order N-1, ..., 0

Multi-level Loops​

Nest iterators directly:

i in [0, Y) j in [0, X) {
result[i][j] = A[i][j] + B[i][j];
}

Or with named iterators:

let i in [0, Y);
let j in [0, X);

∀i ∀j result[i][j] = A[i][j] + B[i][j];

Loop with Body Block​

For multi-statement loop bodies, use braces:

i in [0, N) {
temp = a[i] * 2;
b[i] <- temp + 1;
}

Loop Control in Iterator Loops​

A serial iterator loop is a loop for break and continue: a continue in its body advances to the next iteration and a break leaves the iterator loop, exactly as in a while loop. In a multidimensional domain such as i in [0, Y) j in [0, X) { ... }, loop control binds the innermost dimension: continue advances j, and break ends the current j sweep while i continues.

i in [0, N) {
if (skip[i]) continue; // next i
total[0] += a[i];
}

An iterator loop whose body contains loop control executes as general control flow, with an explicit induction variable and branch, rather than as a counted hardware loop (one instruction over the whole range) or a vector-step loop. A parallel iterator does not bind loop control. Loop control inside a [[vectorize.*]] body is not supported.

Execution domains and hardware instructions​

A counted loop over regularly spaced array elements may execute as one hardware vector or tensor operation. More complex bodies can require several instructions or ordinary loop control. Use the generated listing and measured cycles to assess performance.

Important current constraints are:

  • An SDR one-dimensional hardware transfer moves at most 32766 elements; the length 0x7fff (32767) encodes infinity. Carbide does not split a longer loop: a constant length above 32767 fails with overflows 15-bit field, and a length of exactly 32767 compiles, without a warning, to a transfer that never ends. Split such a transfer into several loops.
  • Tensor hardware instructions have at most four dimensions. Only the highest selected dimension may be infinite.
  • A zero-sized dimension is zero-trip; no hardware instruction is formed.
  • Dimensions that do not vary an operand may require ordinary loop control. Extent-one dimensions are allowed.
  • Socket and FIFO stream accesses can carry vector or tensor shapes.

[[vectorize.*]] requests segmented execution, described in Vector Step Loops. Its named auxiliary iterator is specific to that loop; an unrelated iterator of the same size is not equivalent.

An ascending range [lo, hi) has hi - lo iterations and requires lo <= hi. An equal range is empty: it executes no body instructions and has no memory or other side effects. A range with runtime bounds where lo > hi is a program error with undefined behavior; the compiler emits no runtime check for it. On DSR-length loops it may program a very long or infinite vector length (the hardware treats a length of 0x7fff as infinite).

Memory Iterators​

Memory iterators bind a name to a repeating traversal over a memory region:

sp zz[2] = {0.0, 0.0};
iterator zeros = i in [0, ∞) j in [0, 2) zz[j];

This creates an infinite iterator zeros that repeatedly cycles through zz[0] and zz[1]. Read or write through it with stream syntax, as with a FIFO: k in [0, 5) o[k] = zeros[]; reads zz[0], zz[1], zz[0], zz[1], zz[0].

A memory iterator declared at module scope has module duration: its cursor is initialized once and advances across uses. After iterator it = ∀k in [0, 6) a[k]; at module scope, j in [0, 2) b[j] = it[]; followed by j in [0, 2) c[j] = it[]; leaves c holding a[2] and a[3]. A declaration inside a block has automatic duration: its cursor is initialized whenever execution reaches that declaration, including each dynamic execution of an enclosing loop body.

Memory-iterator names follow ordinary lexical binding. A declaration may shadow an outer declaration, and separate scopes may reuse the same spelling; each use resolves to the nearest declaration and its own backing memory.

Single-step, vector-step, and microthread are distinct execution modes. One instruction cannot select more than one of them across its operands.

Note: The backing expression must be an array or struct element access. A memiterator over a numeric literal (iterator zero = ∀i in [0,4) 0;) is a compile error — a literal denotes no memory, so there is nothing to traverse. For an endless source of zeros, use the zero-filled array form above; for an index or address sequence (0, 1, 2, …), use the loop iterator value directly (out[i] = i;), which needs no iterator declaration.


13. Vector Step Loops​

The vectorize pragma puts a ∀ loop into the hardware's vector step mode. Instead of one iteration per loop branch, the PE advances several consecutive iterations per step, driven by the vector step registers and a vector jump. It is available on SDR.

The attribute list goes immediately before the loop:

[[vectorize.pref_len(M)]]
∀i in [0, N) {
c[i] <- a[i] + b[i];
}
AttributeRequiredMeaning
vectorize.pref_len(factor)yesIterations processed per vector step
vectorize.max_len(max)noTail threshold: if the remaining count is at most max, the hardware consumes it as one final step; otherwise it consumes pref_len. Defaults to pref_len
vectorize.iter(name)noNames the auxiliary per-step iterator so it can be used as a subscript; generated automatically when omitted

Combine them in one bracket list. Inside the body, i is each element's own index, as in an ordinary loop. The auxiliary iterator is the element's position within its step, 0 through S - 1 for a step of length S; use it to index a scratch array that holds one step:

[[vectorize.pref_len(M), vectorize.max_len(L), vectorize.iter(j)]]
∀i in [0, N) {
tmp[j] <- a[i] + b[i];
c[i] <- tmp[j] * a[i];
}

Both length attributes must be positive compile-time constants. The hardware vector-step loop is formed only when max_len is at least pref_len and each is at most 63, or at most 32 when the body contains a predicated operation, because vector flags have 32 lanes; otherwise the loop runs as an ordinary loop, without a diagnostic. A preferred length larger than the trip count is allowed. A preferred length of 1 is valid.

The tail rule is exact: if remaining <= max_len, the next step length is remaining; otherwise it is pref_len. For N=35, pref_len=12, and max_len=23, the steps are 12 + 23; without max_len, they are 12 + 12 + 11. A named auxiliary iterator can therefore reach max_len - 1, so any scratch array indexed by it must be sized for max_len, not merely pref_len.

A static extent of 0 runs nothing, and a negative run-time extent is clamped to 0.

The body must not contain a nested ∀ or while loop. A socket or FIFO access in the body, read or write, needs uthread.enable(off) in the same bracket list; without it, the compiler reports Streams are not supported in vector step loops.

[[vectorize.pref_len(8), uthread.enable(off)]]
∀i in [0, N) {
a[i] <- rx[];
}

Placing the attribute before a multidimensional domain such as ∀i in [0, Y) j in [0, X) vectorizes only its innermost dimension; there is no compound vector-step domain. To vectorize an inner ∀ loop of a nest, put the attribute inside the outer loop's body.

The tutorial topic Vector-step loops, predication and memory iterators shows step lengths and tails, predication, a cursor that persists across calls, and how to check the listing and the cost.