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];
}
| Attribute | Required | Meaning |
|---|---|---|
vectorize.pref_len(factor) | yes | Iterations processed per vector step |
vectorize.max_len(max) | no | Tail 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) | no | Names 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.