Skip to content

Loops

Accepted

Accepted for V1 loop source syntax and reader-facing behavior. Detailed for source selection, ownership, cleanup, and ambiguity rules are canonical in Iterator Loop Sources.

V1 loop forms are:

  • while condition { ... };
  • while const binding_pattern = optional_expr { ... };
  • for binding_pattern in source { ... };
  • for var binding_pattern in source { ... }.

All loop-introduced bindings are const in V1. for var requests mutable collection iteration; it does not make the loop binding reassignable.

Basic for iteration uses item in items:

for item in items {
  process(item)
}

V1 loop bodies always use block braces. Single-expression loop bodies are deferred:

for item in items process(item)
while ready tick()

The loop item is a binding pattern, not only an identifier. Struct and array destructuring are allowed for collection and iterator sources:

for [x, y] in point_iter {
}

for var may also use a destructuring binding pattern when mutable collection iteration is selected:

for var [x, y] in points {
  x.* = 0
  y.* = 0
}

The var still requests mutable iteration; it does not make the destructured bindings reassignable. Loop bindings are const in V1.

V1 loop result

All loop forms have success type void in V1, including while, while const, collection for, and existing-iterator for. Future break-value or value-producing loop syntax is deferred and should apply uniformly if added.

break exits the nearest enclosing loop. continue starts the next iteration of the nearest enclosing loop. V1 does not have labeled break or continue, break values, continue labels, or multi-level loop exits. Both forms trigger scope-exit cleanup, pending defers, and compiler-owned loop temporary cleanup on their exit edge.

V1 for lowering only calls infallible operations: iter, iter_mut, or next. Fallible setup, such as iter_dyn(alloc) / iter_dyn_mut(alloc), must be written before the loop.

While Loops

Plain while repeats while its condition coerces to bool:

while ready {
  tick()
}

while const binds an optional payload for each iteration and exits when the optional expression evaluates to null:

while const item = iter.next() {
  process(item)
}

while const [x, y] = point_iter.next() {
}

The binding pattern follows the same binding-pattern rules as declarations and for loop items. The payload binding is const in V1.

while value { ... } is a presence loop, not payload narrowing. Use while const value = next_optional() to bind a payload.

Deferred Loop Bindings

Reassignable optional loop payload bindings are deferred:

while var item = iter.next() {
  item = transform(item)
}

Use an explicit local when the loop body needs a mutable working value:

while const item = iter.next() {
  var current = item
  current = transform(current)
}

Future reassignable loop binding syntax is tracked by CEP-0057: Reassignable Loop Bindings.

Source Modes

Plain collection iteration yields readonly element access by default. Mutable collection iteration uses var on the loop binding:

for var item in items {
  item.field = 1
}

Collection iteration uses the iterable contracts:

  • for item in items selects Iterable(Item).
  • for var item in items selects MutableIterable(Item).
  • plain for chooses readonly iteration even when mutable iteration is available.
  • for var requests mutable iteration; it does not make the loop binding reassignable.
  • collection loops may create hidden iterator and receiver temporaries. Their ownership and cleanup rules are defined in Iterator Loop Sources.

Use move to transfer a named collection receiver into loop-owned hidden storage:

for item in move items {
  process(item)
}

for var item in move items {
  item.* = transform(item.*)
}

After the loop, items is moved and unusable according to the normal move rules. move does not introduce special loop-only move authority.

Rvalue collection receivers are allowed, including resource receivers:

for item in make_collection() {
}

The loop materializes the receiver into hidden storage, creates the iterator from that hidden receiver, and cleans up compiler-owned temporaries on every loop exit path. The loop does not automatically dispose resource items yielded by next(); yielded items are normal user-visible bindings.

for also works over existing Iterator(Item) values:

for item in iter {
  process(item)
}

Existing iterator iteration:

  • requires visible semantic conformance to Iterator(Item);
  • takes a mutable borrow of the iterator receiver and repeatedly calls next();
  • does not call iter, iter_mut, iter_dyn, or iter_dyn_mut;
  • does not move or dispose the iterator unless the source is written as move iter;
  • does not use structural detection of a method named next.

Conformance selection, ambiguity, mutable receiver, rvalue iterator, moved iterator, and borrowed dynamic pointer rules are canonical in Iterator Loop Sources.

Conceptually, direct iterator for lowers like the optional-binding while form:

while const item = Iterator(Item).next(&iter) {
  body
}

Dynamic Iteration

Dynamic iteration is split into an explicit fallible setup step and an ordinary iterator loop:

var items = make_items()
var it_box = try items.iter_dyn(alloc)
defer it_box.dispose()

for item in it_box {
  process(item)
}

Readonly iter_dyn(alloc) requires a receiver lifetime that outlives the returned boxed iterator when the iterator may borrow from the source. In V1, calling it on an rvalue receiver is rejected for the normal borrowed-iterator path:

var it_box = try make_items().iter_dyn(alloc) // error

Use a named binding so the source lifetime is explicit.

Mutable dynamic iteration uses the same explicit setup pattern:

var items = make_items()
var it_box = try items.iter_dyn_mut(alloc)
defer it_box.dispose()

for item in it_box {
  item.* = value
}

Rules:

  • iter_dyn(alloc) and iter_dyn_mut(alloc) handle allocation failure before the loop starts.
  • the loop over it_box is infallible because canonical Iterator.next is infallible.
  • iter_dyn_mut(alloc) requires a mutable source place, just like iter_mut().
  • the mutable borrow of the source lasts until the iterator box is disposed.
  • Box(dyn Iterator(Item)) forwards next() through the ordinary boxed-iterator rules; this is not general smart-pointer method forwarding and not a for special case.

Calling iter_dyn_mut(alloc) on a const binding or rvalue receiver is rejected in V1:

const items = make_items()
var it_box = try items.iter_dyn_mut(alloc) // error

var other = try make_items().iter_dyn_mut(alloc) // error

Explicit next Loops

Existing iterator values are advanced with the canonical explicit next loop:

while const item = iter.next() {
  process(item)
}

while const [x, y] = point_iter.next() {
}

This is ordinary method-call syntax plus the while const binding_pattern = optional_expr conditional declaration form. It unwraps ?Item: the body runs for each yielded item and exits when next() returns null.

The explicit while form does not require Iterator(Item) conformance. Reassignable while payload bindings are deferred; use an explicit local inside the body when a mutable working value is needed.

Dynamic iterable receivers are also handled by explicitly creating the boxed iterator first:

var it_box = try dyn_iterable_ptr.iter_dyn(alloc)
defer it_box.dispose()

for item in it_box {
}

Plain for item in dyn_iterable_ptr is rejected when the pointer is only a dynamic iterable and not itself an iterator. for does not allocate or call iter_dyn automatically.

Loop bindings are not reassignable. Element replacement is explicit, for example item.* = value.