Skip to content

Iterator Loop Sources

Accepted

Accepted for V1 for source selection, hidden loop ownership, cleanup, and ambiguity rules. Loops owns the shorter source-form overview; iterator laws live in Iterator Contracts.

Hidden Collection Temporaries

Static collection for owns the iterator value returned by iter or iter_mut.

Cleanup rules:

  • if the hidden iterator type implements Disposable, the loop disposes it when the hidden loop scope exits, including break and early exit.
  • cleanup is based on Disposable conformance, not on a method merely named dispose.
  • if the iterable expression is materialized into an owning hidden receiver temporary and that receiver type implements Disposable, the loop also disposes that receiver temporary.
  • this receiver cleanup applies only to hidden temporaries created by the loop.
  • named receiver bindings supplied by the user remain user-owned and are not disposed by the loop.
  • cleanup order is iterator first, receiver temporary after, because the iterator may borrow from the receiver.

Rvalue collection receivers are allowed, including resource receivers. The loop materializes the receiver into hidden storage, creates the iterator from that hidden receiver, and cleans up on every loop exit path. If both the iterator and receiver implement Disposable, the iterator is disposed first and the receiver second:

for item in make_collection() {
}

A named collection receiver can be transferred into loop-owned hidden storage with move:

for item in move xs {
}

for var item in move xs {
}

After the loop, xs is moved and unusable according to the normal move rules.

Moved receiver rules:

  • any movable receiver may be moved into loop-owned storage, including copyable and non-Disposable receivers.
  • if the moved receiver implements Disposable, the loop disposes the hidden receiver after cleaning up the iterator.
  • for for var, the moved receiver is materialized as mutable hidden storage so iter_mut can be called.
  • loop receiver move does not introduce special loop-only move authority.

Static collection for and existing-iterator for do not automatically clean up resource items yielded by next().

Yielded resource rules:

  • each yielded item is a normal loop binding owned by user code.
  • if the item type implements Disposable, the body must explicitly dispose it or move it onward.
  • missing cleanup is optional lint territory, including paths through continue, break, and early exit. See ownership/missing-cleanup and ownership/resource-loop-item-exit.

for var requires a mutable receiver place because iter_mut takes self: *Self. If the receiver is not mutable or only has readonly iteration, the loop is rejected.

Plain for uses readonly iteration even when mutable iteration is also available. Use for var to request mutable iteration.

var in for var item in xs requests mutable iteration. It does not make the loop binding itself reassignable.

Loop binding rules:

  • for loop bindings are const bindings in V1, including value-yielding iterators.
  • for var requests mutable iteration; it does not make the loop binding reassignable.
  • reassignable loop binding syntax is deferred to CEP-0057: Reassignable Loop Bindings.

The iterable expression is evaluated once at loop entry before calling iter or iter_mut.

Evaluation rules:

  • named receiver bindings remain user-owned receiver places.
  • rvalue receivers and explicit move receivers may be materialized into loop-owned hidden storage.
  • plain for materializes readonly hidden storage when needed.
  • for var materializes mutable hidden storage when needed for rvalues or moved receivers.
  • one-time evaluation defines temporary lifetimes and prevents repeated evaluation of expressions such as get_items().

For for var, an rvalue iterable expression may be materialized as a mutable hidden temporary so iter_mut(self: *Self) can be called.

Mutating such a temporary is allowed but should produce a lint when the mutation is likely discarded at loop exit.

for var over a named const place is rejected. The compiler must not silently copy a const collection into a mutable temporary for for var, because mutations would not affect the original value.

Loop Source Selection

Source syntax maps to contracts:

items[i]             // Indexable(T)
items[i] = value     // MutableIndexable(T)
for item in items     // Iterable(Item) or Iterator(Item)
for var item in items // MutableIterable(Item)

When a value supports both readonly and mutable iteration, plain for selects readonly iteration. for var is the explicit mutable-iteration request.

For example, MutableSequence(T) supports readonly iteration through its Sequence(T) dependency and mutable iteration through MutableIterable(*T):

  • for item in xs uses Iterable(*const T).
  • for var item in xs uses MutableIterable(*T).

Loop item bindings are general binding patterns and use the iterator item type directly:

for item in slice {
  // item: *const T, because slice implements Iterable(*const T)
}

for [x, y] in point_iter {
  // destructures each yielded item
}

for var [x, y] in points {
  // `var` requests MutableIterable; x and y are still const loop bindings in V1
}

for item in range {
  // item: i32, because range implements Iterable(i32)
}

for var item in slice {
  // item: *T, because slice implements MutableIterable(*T)
}

Plain loop binding inference requires exactly one visible applicable iterable conformance. If a type implements multiple iterable modes, the loop is ambiguous unless the item type is written explicitly:

for item: *const u8 in text {
}

for item: Rune in text {
}

for var item: *u8 in bytes {
}

The explicit item type selects Iterable(Item) or MutableIterable(Item) respectively.

The annotation is an exact iterable-conformance selection, not an implicit conversion request. If the selected Iterable(Item) or MutableIterable(Item) conformance is not visible, the loop is rejected.

Existing Iterator Sources

for may also operate directly on an existing Iterator(Item) value.

Existing-iterator rules:

  • the loop takes a mutable borrow of the iterator receiver and repeatedly calls next().
  • the loop does not move, create, or dispose that iterator unless the source uses explicit move.
  • the caller owns the iterator.
  • after the loop, the iterator binding still exists and is usually exhausted.
  • the explicit while const item = iter.next() form remains canonical when code needs to make the next() calls visible.
while const item = iter.next() {
}

Existing-iterator for requires visible semantic conformance to Iterator(Item).

Conformance rules:

  • a method named next is not enough by itself.
  • if multiple visible Iterator(...) conformances apply, item type annotation may select one exact item type.
  • otherwise the loop is ambiguous.

Conceptually, direct iterator for lowers like the optional-binding while form, but with contract dispatch through the resolved Iterator(Item) conformance:

for item in iter {
  body
}

is shaped like:

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

The condition calls next(), continues when the result is present, and exits when the result is null.

This is ordinary method-call syntax plus the general while const name = optional_expr conditional declaration form; it 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.

The for and explicit while forms do not dispose an existing iterator receiver. The caller owns any existing iterator value and must explicitly clean it up or move it onward:

var iter = make_iter()
for item in iter {
}
iter.dispose()

An existing iterator can be transferred into loop-owned hidden storage with explicit move:

var iter = make_iter()
for item in move iter {
}

After the loop, iter is moved and unusable.

Moved iterator rules:

  • moving into the loop is valid for copyable and non-copyable iterator values.
  • if the moved iterator implements Disposable, the loop disposes the hidden moved iterator when the loop exits, including break, return, and error propagation from the loop body.
  • if the moved iterator does not implement Disposable, no cleanup call is made.

Existing iterator for requires a mutable receiver place because Iterator.next takes self: *Self:

var iter = make_iter()
for item in iter {
} // ok

Const iterator bindings are rejected because the loop needs a mutable receiver:

const iter2 = make_iter()
for item in iter2 {
} // error: iterator receiver is not mutable

A mutable borrowed dynamic iterator pointer may be iterated directly:

fn drain(it: *dyn Iterator(u8)) void {
  for item in it {
  }
}

This performs dynamic dispatch through the borrowed pointer and does not allocate or take ownership. *const dyn Iterator(Item) is rejected because next requires self: *Self.

Concrete iterator pointers

V1 does not add a general rule that *ConcreteIterator is a loop source.

Prefer for item in iter for a mutable iterator place, or use the explicit while const item = ptr.next() form if pointer receiver behavior is needed.

The dynamic pointer case is allowed because *dyn Iterator(Item) is the ordinary borrowed dynamic receiver form for the erased iterator.

This applies to Box(dyn Iterator(Item)) too. Its Iterator(Item) implementation must borrow the erased payload mutably through ptr(), so the box binding must be mutable:

var it_box = try xs.iter_dyn(alloc)
for item in it_box {
} // ok
it_box.dispose()

var owned_box = try xs.iter_dyn(alloc)
for item in move owned_box {
} // loop disposes the moved box at exit

Const boxed iterator bindings are rejected:

const it_box2 = try xs.iter_dyn(alloc)
for item in it_box2 {
} // error: box receiver is not mutable

A fresh iterator rvalue may be iterated directly.

The loop materializes it into hidden mutable storage, owns that temporary, and disposes it on every loop exit path if the iterator type implements Disposable:

for item in make_iter() {
}

for item in xs.iter() {
}

Conceptually:

{
  var iter = make_iter()
  defer iter.dispose() // present only when the iterator type implements Disposable

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

Ambiguous loop source

If a type implements both Iterable(Item) and Iterator(Item), for item in x is ambiguous and rejected.

The compiler must not prefer one by guessing whether the type is more "collection-like" or "iterator-like". The two meanings have different state semantics:

  • Iterable.iter() creates a fresh iterator without advancing x.
  • Iterator.next() advances x itself.

Use an explicit call to disambiguate:

for item in x.iter() {
} // iterator rvalue path

while const item = x.next() {
} // advance x itself

Implementing both Iterable(Item) and Iterator(Item) for the same concrete type should produce a lint unless the type documents a strong reason.

It is usually clearer to separate collection/view values from iterator-state values. This lint is specifically about Iterable plus Iterator; resource-owning iterators commonly implement both Iterator(Item) and Disposable.

move does not resolve this ambiguity. for item in move x transfers ownership of the selected source into loop-owned hidden storage, but source-mode selection still happens first. If x implements both Iterable(Item) and Iterator(Item), for item in move x is ambiguous too.

An explicit item type may disambiguate when only one source mode yields that exact item type:

for item: Rune in x {
} // selects Iterable(Rune) if iterator mode yields a different item type

for item: Token in x {
} // selects Iterator(Token) if iterable mode yields a different item type

If both source modes yield the annotated item type, the loop remains ambiguous.

for var item in iter is not valid for an existing Iterator(Item) in V1.

Invalid-form rules:

  • for var requests mutable collection iteration through MutableIterable(Item).iter_mut.
  • it does not mean "make the loop binding reassignable."
  • even with an item type annotation, for var never selects iterator mode.

If an iterator yields mutable pointers, ordinary for item in iter is enough:

for item in iter {
  item.* = value
}