Loops and Iteration
Sec separates iteration from condition-controlled repetition.
Use for for ranges, collections and explicit iterators.
Use while when repetition is controlled by a changing boolean condition.
for item in items {
Process(item)
}
while running {
Poll()
}
Unconditional repetition uses:
for {
Work()
}
for means iteration or explicit unconditional repetition.
while means condition-controlled repetition.
for vs while
Use for when there is an iterable source:
for item in items {
Process(item)
}
Use while when a boolean condition controls repetition:
while ready {
Poll()
}
Use unconditional for for intentional endless repetition:
for {
Poll()
}
A condition-only for is invalid:
for ready {
Poll()
}
C-style loop syntax is also invalid:
for i := 0; i < 10; i += 1 {
...
}
Use a range instead:
for i in 0..<10 {
...
}
for Forms
// Unconditional loop.
for {
Work()
}
// One value binding.
for item in items {
Process(item)
}
// Sequential index and value.
for index, item in items {
Process(index, item)
}
// Map key and value.
for key, value in users {
Process(key, value)
}
// Range.
for index in 0..<10 {
Process(index)
}
// Explicit descending range.
for index in 10..1 step -1 {
Process(index)
}
// Shared element borrow.
for ref item in items {
Inspect(item)
}
// Mutable element borrow.
for ref mut item in items {
item.Reset()
}
Braces are mandatory for every for body.
Loop Bindings
The supported binding modes are:
item
ref item
ref mut item
_
| Binding | Meaning |
|---|---|
item | By-value binding |
ref item | Shared borrowed element |
ref mut item | Exclusive mutable borrowed element |
_ | Discard this binding position |
The iterable category determines which modes are legal for each binding position. Sec 0.1 has no consuming loop binding.
Plain Value Binding
for item in items {
Process(item)
}
A plain binding is by value. If the yielded type T is implicitly copyable,
the current element is copied into item.
The source element remains valid.
If T is move-only, plain iteration is invalid:
for file in files {
Use(file)
}
when File is move-only.
Sec does not silently move the element out of the collection and does not silently reinterpret the loop as a borrow.
Borrow explicitly when inspection is intended:
for ref file in files {
Inspect(file)
}
Mutable ref mut Binding
for ref mut item in items {
item.Count += 1
}
For element type T, the binding has type:
ref mut T
The source must provide mutable borrowing authority. Element mutation is permitted according to ordinary exclusive-borrow rules.
This does not grant permission to structurally mutate the source collection:
for ref mut item in items {
try items.Append(newItem)
}
is invalid when Append may change collection structure or invalidate
iteration state.
The binding itself remains immutable; the referenced element is mutable.
Discard Binding
for _, item in items {
Process(item)
}
for _, ref mut value in users {
Update(value)
}
_ creates no local symbol.
Discarding a position does not force the compiler to copy or move the yielded
value merely to discard it.
Binding Scope
Every loop body is a lexical scope.
for item in items {
Process(item)
}
Process(item) // invalid
Loop bindings follow normal no-shadowing rules:
let item := 10
for item in items {
...
}
is invalid because the loop binding would shadow the visible item.
A plain loop binding cannot be reassigned:
for item in items {
item = replacement // invalid
}
Source Evaluation
The iterable expression is evaluated exactly once before iteration begins.
for item in LoadItems() {
Process(item)
}
LoadItems() runs once, not once per iteration.
An owned temporary used as the iterable source remains alive as long as required by the loop and is cleaned up through ordinary ownership rules.
Iterable Categories
Sec 0.1 defines iteration for compiler-known categories including:
- fixed arrays
T[N]; - dynamic arrays
T[]; - slices
ref T[]andref mut T[]; list[T];set[T];map[K, V];string;- finite numeric ranges;
- supported sequential shaped types such as
vector[T, N]; - concrete types explicitly conforming to compiler-known
Iterator[T].
A type does not become iterable merely because it has members named:
Len
Get
Next
HasNext
Contains
User-defined pull-based iteration uses explicit Iterator[T] conformance.
Sequential Collections
Sequential sources support one element binding:
for item in items {
...
}
and, where defined, index plus element:
for index, item in items {
...
}
The sequential index is zero-based and has type int.
It is a value binding, not an element reference.
for index, ref item in items {
Inspect(index, item)
}
for index, ref mut item in items {
item.Sequence = index
}
Arrays and Lists
Fixed arrays
A fixed array T[N] iterates from index 0 through
N - 1.
Plain element iteration copies T and therefore requires
T to be copyable.
for ref item in values {
Inspect(item)
}
for ref mut item in values {
Update(item)
}
A for loop never partially moves elements out of a fixed array.
Dynamic arrays and lists
T[] and list[T] visit live logical elements in
collection order.
They follow the same plain-copy, shared-borrow and mutable-borrow model where the source provides the required authority.
Slices
A shared slice:
ref T[]
supports:
- plain value when
Tis copyable; ref T;_.
A shared slice does not provide mutable element authority.
A mutable slice:
ref mut T[]
may additionally provide:
ref mut T
Iteration does not extend the backing owner's lifetime beyond normal reference rules.
Strings
String iteration yields Unicode code points of type:
rune
for character in text {
Process(character)
}
Two-binding string iteration is also supported:
for index, character in text {
Process(index, character)
}
String iteration does not expose raw UTF-8 bytes. Use an explicit encoded byte representation when bytes are required.
Decoded runes are yielded values rather than mutable stored rune elements, so
ordinary string iteration does not support ref or
ref mut rune bindings.
Sets
A set has exactly one logical value binding:
for value in activeUsers {
Process(value)
}
Shared borrowing may be supported:
for ref value in activeUsers {
Inspect(value)
}
Mutable set element iteration is invalid:
for ref mut value in activeUsers {
...
}
Stored set values participate in equality and hashing identity and must not be mutated in place.
Ordinary set[T] iteration order is unspecified.
Maps
A map requires exactly two logical binding positions:
for key, value in users {
Process(key, value)
}
Single-binding map iteration is not supported:
for entry in users {
...
}
Shared borrowing:
for ref key, ref value in users {
Inspect(key, value)
}
Mutable value borrowing:
for ref key, ref mut value in users {
Update(value)
}
Mutable key borrowing is invalid:
for ref mut key, value in users {
...
}
A stored key participates in hash and equality identity and cannot be modified in place.
Discard positions independently when needed:
for _, ref mut value in users {
Update(value)
}
Ordinary map[K, V] iteration order is unspecified.
Ranges
The directly iterable finite range forms are:
start..end
start..<end
.. includes the endpoint when reached:
for value in 1..5 {
Process(value)
}
yields 1 through 5.
..< excludes it:
for value in 1..<5 {
Process(value)
}
yields 1 through 4.
Open-ended ranges
for value in 0.. {
...
}
is invalid because an open-ended range has no finite iteration boundary.
Use for {} or while instead.
Bound evaluation
for value in Start()..<End() step Increment() {
Process(value)
}
Start(), End() and Increment() are each
evaluated exactly once before progression begins.
Named numeric types
let minimum: Score := Score(0)
let maximum: Score := Score(10)
for score in minimum..maximum {
Process(score)
}
The binding has type Score. Range construction does not erase
named-type identity.
Range step
Integer-like ranges use an implicit ascending +1 step when
step is omitted.
The compiler does not infer descending iteration from the bounds:
for value in 10..<5 {
...
}
performs zero iterations.
Descending iteration is explicit:
for value in 10..1 step -1 {
Process(value)
}
An explicit step:
- is evaluated once;
- must be non-zero;
- must be valid for the range's numeric type;
- determines direction and delta.
These are invalid when statically known:
for value in 1..10 step -1 {
...
}
for value in 10..1 step 1 {
...
}
Descending unsigned ranges are not silently converted to a signed domain.
Use a suitable signed type or while.
Range termination never requires an overflowing increment after the final yielded value.
Float and Decimal Ranges
float and decimal range iteration requires an explicit step.
for value in 0.001..<0.002 step 0.00001 {
Process(value)
}
for value in 0.002..0.001 step -0.00001 {
Process(value)
}
This is invalid:
for value in 0.001..<0.002 {
...
}
Progression follows the arithmetic semantics of the resolved numeric type. Float lowering must preserve source-visible coverage rather than introduce avoidable repeated-addition drift. Decimal progression preserves Sec decimal semantics.
Compiler-Known Iterator[T]
Sec defines this compiler-known pull iterator contract:
interface Iterator[T] {
mut fn Next() Option[T]
}
A concrete source participates through explicit conformance:
type NumberStream struct {
current: int,
end: int,
}
impl NumberStream implements Iterator[int] {
fn Next() Option[int] {
if self.current >= self.end {
return None
}
let value := self.current
self.current += 1
return Some(value)
}
}
Then:
let mut stream := NumberStream {
current: 0,
end: 10,
}
for value in stream {
Process(value)
}
The protocol means:
- the source expression is evaluated once;
- the concrete statically resolved
Next()is called repeatedly; Some(value)yields one ownedTand runs the body;Noneterminates iteration;- exactly one value binding or discard is available;
- the yielded
Tis not an implicit reference into iterator storage.
Reusable iterator storage must provide the mutable authority required by the iterator operation.
A fresh owned iterator may be used directly:
for part in text.Split(";") {
Process(part)
}
The compiler retains the fresh iterator as loop-local state for as long as needed.
The Iterator[T] protocol does not define ref or
ref mut bindings for yielded results.
It also requires no:
- runtime interface object;
- virtual dispatch;
- boxing;
- runtime type inspection;
- runtime borrow tracking;
- hidden heap allocation.
A method named Next is not sufficient. The type must explicitly
conform to Iterator[T].
Structural Stability During Iteration
Active collection iteration must preserve the structural assumptions used by the iterator.
Structural mutation includes operations that may:
- change collection length;
- insert or remove elements;
- move or replace backing storage;
- reorder elements so iteration state becomes invalid;
- invalidate current or future element references.
Such operations are invalid while the active iteration depends on that structure.
Element mutation through a valid ref mut loop binding is not structural
mutation when it preserves the collection's structural invariants.
while
while condition {
statements
}
The condition is evaluated before every possible iteration.
The body runs only when it evaluates to true, so a normal
while loop may execute zero times.
Parentheses around the condition are allowed but normally unnecessary.
while Conditions
The condition must have type bool. Sec has no implicit truthiness.
Valid:
while running {
Poll()
}
while count < limit {
count += 1
}
Invalid:
while 1 {
...
}
while items {
...
}
Short-circuit evaluation
while ready && CanContinue() {
Work()
}
&& and || retain guaranteed left-to-right
short-circuit semantics.
Side effects
while ReadNext() {
Process()
}
A legal side-effecting condition is evaluated exactly once before each attempted iteration.
try
while try ReadStatus() {
Poll()
}
This is valid when the successful value has type bool.
A Result value itself is not implicitly boolean.
State tests
while state is Active {
Poll()
}
Non-binding is expressions may be used when another language rule
defines them as producing bool.
while does not introduce payload-binding patterns.
No declaration header
while let value := Read(); value > 0 {
...
}
is invalid.
Assignment is not an expression
while running = true {
...
}
is invalid.
Comparisons and membership
Comparison chaining is not supported:
while 0 <= value < 100 {
...
}
Use:
while value >= 0 && value < 100 {
...
}
or:
while value in 0..<100 {
...
}
while Progression
while provides no automatic progression clause.
let mut index: int := 0
while index < limit {
Process(index)
index += 1
}
The body is responsible for changing any state required for eventual termination.
Infinite Loops
Both forms are legal:
for {
Poll()
}
while true {
Poll()
}
Use for {} when unconditional repetition is the direct intent.
A loop with no reachable exit path is non-continuing for control-flow analysis.
break
break exits the nearest enclosing loop.
for item in items {
if Stop(item) {
break
}
Process(item)
}
It is invalid outside a loop and does not carry a value. Required cleanup runs before control reaches the loop exit.
Labeled break is not part of Sec 0.1.
continue
continue ends the current iteration and advances the nearest
enclosing loop.
for item in items {
if Skip(item) {
continue
}
Process(item)
}
In a while, it transfers to the next condition evaluation:
while running {
if ShouldSkip() {
continue
}
Work()
}
Required per-iteration cleanup runs before the next iteration.
continue carries no value and has no labeled form in Sec 0.1.
Nested Loops
break and continue always target the nearest enclosing loop.
while rowsRemain {
while columnsRemain {
if DoneWithRow() {
break
}
ProcessCell()
}
}
The inner break exits only the inner loop.
Exiting multiple levels uses explicit state, return, error propagation,
or refactoring rather than labels.
Loops and switch
switch does not own a break or continue target.
while running {
switch status {
case Status.Stop:
break
default:
Work()
}
}
The break exits the surrounding loop.
while running {
switch status {
case Status.Skip:
continue
default:
Work()
}
}
The continue advances the surrounding loop.
Ownership and Cleanup
Each executed iteration creates a new loop-body scope.
Owned locals created during an iteration are destroyed before that iteration finishes unless ownership is transferred elsewhere.
continueperforms cleanup before the next iteration;breakperforms cleanup for exited scopes;returnperforms function-exit cleanup;- error propagation performs cleanup required by its exit path.
Loop-provided references do not create ownership of source elements. Plain by-value copies are ordinary locals with ordinary destruction semantics.
Definite Assignment
Normal finite for and while loops may execute zero times.
An assignment performed only inside such a loop therefore does not automatically establish definite assignment after the loop.
let mut result: int
for value in values {
result = value
}
Use(result)
is invalid unless some other flow fact proves result initialized on
every path that reaches Use.
Non-continuing paths are excluded from post-loop state merging.
Return-Path Analysis
A return inside a finite loop does not prove that a non-void function returns, because the loop may execute zero times.
An unconditional loop with no reachable exit is non-continuing:
fn RunForever() int {
for {
Poll()
}
}
A reachable break introduces a continuing path:
fn MayStop(stop: bool) int {
for {
if stop {
break
}
}
return 0
}
A break that exits only a nested inner loop does not make an outer infinite loop continuing.
No Consuming for
Ordinary iteration never implicitly extracts ownership from the source collection.
Sec 0.1 does not define:
for -> item in items {
...
}
for item in move items {
...
}
Plain iteration also does not turn into a move when an element happens to be move-only.
When stored ownership must be removed, use the explicit owning extraction operation provided by the collection type.
Unsupported Loop Forms
Sec 0.1 does not include:
- condition-only
for; - C-style
for; - loop
elseclauses; do-while;- labeled
breakorcontinue; - break/continue values;
- implicit consuming iteration;
for -> item in source;for item in move source;- implicit mutable element iteration;
refbindings to decoded string runes;refbindings to synthesized range values;- mutable set element iteration;
- mutable map key iteration;
- single-binding map iteration;
- general pattern destructuring in
forheaders; - iterator discovery from member names;
- implicit
Iterablefactory discovery; - runtime-dispatched iterator interface values as the
forprotocol.
Loops are statements and do not produce values.
Diagnostics
Important loop diagnostics include:
- non-iterable source;
- invalid binding count;
- invalid binding mode;
- plain by-value iteration of a move-only element;
- mutable iteration without mutable authority;
- mutable map key or set element iteration;
- structural mutation during active iteration;
- open-ended range used in
for; - incompatible range types;
- zero or invalid range step;
- invalid statically known step direction;
- missing explicit step for float or decimal range;
Iterator[T]use without explicit conformance;- iterator source without required mutable authority;
- invalid iterator
Next()contract; - non-boolean
whilecondition; - assignment, declaration or pattern binding used as a
whilecondition; breakorcontinueoutside a loop;- unsupported labeled loop control;
- loop binding shadowing or reassignment.
A move-related diagnostic should explain the intended alternatives:
cannot iterate File by value
File is move-only and ordinary for iteration does not consume collection elements
help: use `for ref file in files` to borrow each element
help: use an explicit owning collection operation to extract ownership
Best Practices
- Use
forfor ranges and iterable sources. - Use
whilefor changing boolean conditions. - Use
for {}for intentionally unconditional repetition. - Use plain bindings for copyable elements when a copy is appropriate.
- Use
refto inspect elements without copying. - Use
ref mutonly for supported in-place element mutation. - Do not structurally mutate a collection while iteration depends on its structure.
- Do not depend on ordinary map or set iteration order.
- Prefer finite ranges over manually maintained counted
whileloops. - Make descending iteration explicit with a negative
step. - Use explicit
Iterator[T]conformance for custom pull iterators. - Use collection-specific extraction operations when ownership must leave the collection.
Summary
forhandles iteration and explicit unconditional repetition.whilehandles condition-controlled repetition.for {}is the canonical unconditional loop.- Sec has no condition-only or C-style
for. - Plain loop bindings copy and require copyable yielded values.
- Plain iteration never silently moves or borrows move-only elements.
refcreates a shared element borrow.ref mutcreates an exclusive mutable element borrow where supported._discards a binding without forcing a copy or move.- Loop bindings are immutable and body-scoped.
- The iterable source is evaluated once.
- Sequential two-binding iteration yields zero-based
intindex plus element. - String iteration yields decoded
runevalues. - String iteration indices are logical iteration indices, not UTF-8 byte offsets.
- Sets do not permit mutable element iteration.
- Maps require key and value bindings and do not permit mutable key iteration.
- Ordinary map and set iteration order is unspecified.
- Finite
..and..<ranges are iterable. - Open-ended ranges are not directly iterable.
- Integer-like ranges default to ascending
+1. - Descending iteration requires an explicit negative
step. - Float and decimal ranges require an explicit step.
- Custom pull iteration uses explicit
Iterator[T]conformance. Iterator[T]definesmut fn Next() Option[T].- Iterator results are owned yielded values, not implicit element references.
- Iteration does not inherently allocate.
- Active iteration requires structural stability of its source.
- A
whilecondition must bebool. whilepreserves normal short-circuit,try, comparison and membership semantics.breakexits the nearest enclosing loop.continueadvances the nearest enclosing loop.switchdoes not create separate loop-control targets.- Sec 0.1 has no labeled loop control.
- Cleanup runs correctly on normal completion,
continue,break,returnand error propagation. - Finite loops may execute zero times and therefore do not automatically establish definite assignment.
- An infinite loop without a reachable exit is non-continuing.
- Ordinary
foriteration is never consuming in Sec 0.1.