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()
}
Core rule: 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
_
BindingMeaning
itemBy-value binding
ref itemShared borrowed element
ref mut itemExclusive 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)
}

Shared ref Binding

for ref item in items {
    Inspect(item)
}

For element type T, the binding has type:

ref T

The source must be able to provide a stable shared reference to the current element. Shared iteration works for move-only values because no ownership transfer occurs.

Normal borrowing rules apply, and the element cannot be moved out through the shared reference.

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[] and ref 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 T is 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)
}
Important: The index is a zero-based sequential iteration index. It is not a UTF-8 byte offset.

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 owned T and runs the body;
  • None terminates iteration;
  • exactly one value binding or discard is available;
  • the yielded T is 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.

  • continue performs cleanup before the next iteration;
  • break performs cleanup for exited scopes;
  • return performs 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.

No Hidden Allocation

A for loop does not inherently require heap allocation.

Compiler-generated iteration state should use ordinary local or lowered state. Operations called by the body may allocate according to their own contracts, but iteration itself does not gain permission for hidden allocation.

Unsupported Loop Forms

Sec 0.1 does not include:

  • condition-only for;
  • C-style for;
  • loop else clauses;
  • do-while;
  • labeled break or continue;
  • break/continue values;
  • implicit consuming iteration;
  • for -> item in source;
  • for item in move source;
  • implicit mutable element iteration;
  • ref bindings to decoded string runes;
  • ref bindings to synthesized range values;
  • mutable set element iteration;
  • mutable map key iteration;
  • single-binding map iteration;
  • general pattern destructuring in for headers;
  • iterator discovery from member names;
  • implicit Iterable factory discovery;
  • runtime-dispatched iterator interface values as the for protocol.

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 while condition;
  • assignment, declaration or pattern binding used as a while condition;
  • break or continue outside 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 for for ranges and iterable sources.
  • Use while for changing boolean conditions.
  • Use for {} for intentionally unconditional repetition.
  • Use plain bindings for copyable elements when a copy is appropriate.
  • Use ref to inspect elements without copying.
  • Use ref mut only 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 while loops.
  • 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

  • for handles iteration and explicit unconditional repetition.
  • while handles 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.
  • ref creates a shared element borrow.
  • ref mut creates 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 int index plus element.
  • String iteration yields decoded rune values.
  • 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] defines mut 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 while condition must be bool.
  • while preserves normal short-circuit, try, comparison and membership semantics.
  • break exits the nearest enclosing loop.
  • continue advances the nearest enclosing loop.
  • switch does not create separate loop-control targets.
  • Sec 0.1 has no labeled loop control.
  • Cleanup runs correctly on normal completion, continue, break, return and 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 for iteration is never consuming in Sec 0.1.