Iterator Contracts¶
Accepted
Accepted for V1 iterator, iterable, mutable iterable, and dynamic setup boundaries.
This document records the standard contract families used by iteration.
Iterator and Iterable Contracts¶
The canonical iterator model is pull-based and infallible:
fn Iterator(comptime Item: Type) Type {
require_concrete_sized(Item, "Iterator item type must be concrete and sized")
return contract {
fn next(self: *Self) ?Item
}
}
next returns the next item, or null when iteration is done. End-of-iteration is not an error. Canonical iterators are fused: once next() returns null, later calls return null until the iterator is disposed or otherwise ended by its owner.
next may mutate iterator-owned state and may use resources already owned by the iterator value. It must not require hidden contextual authority. Canonical Iterator(Item) is permanently context-free: its next operation takes only the receiver. Iterators whose step operation needs additional external context require a different future contract family with ordinary parameters, possibly destructured, rather than a variant of canonical Iterator(Item).
Iterator(Item) does not provide a default into_dyn or boxing helper in V1. Turning an existing concrete iterator into an owning dynamic iterator is a Box operation, not an iterator law:
var boxed = try box_dyn(Iterator(Item), RangeIterator(Item), alloc, iter)
This keeps Iterator(Item) minimal and avoids adding allocation authority to the iterator contract.
The item type may itself be a pointer:
Iterator(i32) // value-yielding range
Iterator(*const T) // readonly borrowing collection iterator
Iterator(*T) // mutable borrowing collection iterator
This keeps the canonical iterator value-based while still supporting borrowed and mutable borrowed iteration through ordinary pointer item types.
Collections expose iteration through iterable contracts. for should use Iterable(Item), not Sequence(T), because many iterable values are not indexable:
fn Iterable(comptime Item: Type) Type {
require_concrete_sized(Item, "Iterable item type must be concrete and sized")
return contract {
fn iter(self: *const Self) comptime Iterator(Item)
if ContractSurface.current().is_static()
fn iter_dyn(self: *const Self, alloc: *dyn Allocator) Box(dyn Iterator(Item))!AllocError
}
}
fn MutableIterable(comptime Item: Type) Type {
require_concrete_sized(Item, "MutableIterable item type must be concrete and sized")
return contract {
fn iter_mut(self: *Self) comptime Iterator(Item)
if ContractSurface.current().is_static()
fn iter_dyn_mut(self: *Self, alloc: *dyn Allocator) Box(dyn Iterator(Item))!AllocError
}
}
The default implementations box the static iterator returned by iter or iter_mut through Box(dyn Iterator(Item)).create_from_dyn(I, alloc, value), where I is the concrete iterator type selected by the opaque static return. V1 generic parameter inference does not expose inferred I spelling to ordinary callers; future inference is tracked by CEP-0016: Generic Parameter Inference.
Mutable iteration is expressed as MutableIterable(*T). Readonly borrowing iteration is expressed as Iterable(*const T). The separate mutable contract is about receiver authority, not just item type: a readonly receiver must not be required or allowed to hand out mutable element pointers.
MutableIterable(*T) is not automatically a subtype or dependency of Iterable(*const T) in V1. The item types and receiver authority differ, so mutable-to-readonly iterable conversion must be modeled through explicit contract dependencies when a contract family wants that relationship, such as MutableSequence(T) depending on Sequence(T). Upcasts follow the declared contract graph; they do not infer conceptual mutable-to-readonly relationships across different applied contract types.
Plain for item in xs uses Iterable(Item).iter in static contexts. for var item in xs uses MutableIterable(Item).iter_mut in static contexts. A type supporting both readonly and mutable iteration is not ambiguous for this reason; the loop keyword selects the mode. Dynamic iterator construction is explicit: call iter_dyn or iter_dyn_mut manually with an allocator to create a Box(dyn Iterator(Item)), then iterate that existing iterator box with ordinary for.
V1 for lowering only calls infallible operations: iter, iter_mut, or next. It does not call fallible setup operations such as iter_dyn / iter_dyn_mut, and it does not model fallible per-step iteration. Fallible setup must be explicit before the loop.
Iterable Laws¶
Iterable(Item) does not require repeatability. It means the receiver can produce an iterator. One-shot streams, cursors, generators, and views over changing external state may be iterable if iter can create a fresh iterator state without mutating the readonly receiver. Fixed-state repeatability belongs to Sequence(T) or a future stronger iterable contract.
Iterable.iter must not allocate and must not mutate the receiver. It should produce a fresh independent static iterator state each time: advancing one iterator returned by iter() must not advance another iterator returned by a separate iter() call. The produced item sequence may still differ if the iterable views external changing state.
Iterable.iter_dyn should produce a fresh owned dynamic iterator with the same independence rule. MutableIterable.iter_mut and MutableIterable.iter_dyn_mut may expose mutable access to elements. Structural mutation of the collection while a mutable iterator is active is implementation-defined unless the type documents stronger guarantees. Native arrays and slices iterate over a captured range and yield stable element pointers for that range.
The static iterator return is opaque static: the implementation chooses one concrete iterator type, callers know it only as something implementing Iterator(Item), and dispatch remains static/monomorphized.