Skip to content

Dynamic Iteration

Accepted

Accepted for V1 explicit dynamic iterator construction through iter_dyn and iter_dyn_mut.

iter_dyn and iter_dyn_mut are the standard default dynamic paths for iterable receivers. They belong to Iterable and MutableIterable, not to Iterator, because they create owning boxed iterator state from a receiver and require allocation authority.

The names iter_dyn and iter_dyn_mut are intentional: dyn describes the dispatch mode of the produced iterator. The owning storage strategy is already visible in the return type Box(dyn Iterator(Item)).

iter_dyn and iter_dyn_mut are part of the dynamic contract surface. A borrowed dynamic iterable such as *const dyn Iterable(Item) can dispatch iter_dyn dynamically to create a Box(dyn Iterator(Item))!AllocError. Static-only iter and iter_mut are guarded out of the dynamic surface with ContractSurface.current().is_static().

Because iter_dyn and iter_dyn_mut are dynamic operations, their allocator parameter is an explicit erased allocator boundary:

alloc: *dyn Allocator

The ordinary Box constructors use static allocator dispatch. Dynamic iteration is the V1 exception because the operation itself must be callable through a dynamic vtable. Box(dyn C).create_from_dyn(I, alloc, value) is the erased-allocator boxed-construction path for these defaults and for custom iter_dyn overrides.

The erased allocator authority passed to iter_dyn or iter_dyn_mut must outlive every boxed iterator allocated from it unless the concrete allocator documents a stronger free-independent policy. V1 treats this as an ownership convention and ownership/allocator-authority-escape candidate, not a hard lifetime proof.

The default iter_dyn boxes the static iterator returned by iter. Implementations inherit this default and may override it when they can build a better boxed dynamic iterator. The same rule applies to iter_dyn_mut and iter_mut.

This lets dynamic iterable objects create dynamic iterators:

fn drain(alloc: *dyn Allocator, xs: *const dyn Iterable(u8)) void!AllocError {
  var it = try xs.iter_dyn(alloc)
  defer it.dispose()

  for item in it {
  }
}

iter is static-only because it returns opaque static comptime Iterator(Item). iter_dyn is dyn-safe because it returns the concrete sized type Box(dyn Iterator(Item))!AllocError.

Likewise, iter_dyn_mut is part of the dynamic surface of dyn MutableIterable(Item) and requires a mutable dynamic receiver:

fn drain(alloc: *dyn Allocator, xs: *dyn MutableIterable(*u8)) void!AllocError {
  var it = try xs.iter_dyn_mut(alloc)
  defer it.dispose()

  for item in it {
    item.* = 0
  }
}

*const dyn MutableIterable(Item) cannot call iter_dyn_mut because the operation receiver is self: *Self.

Plain for item in xs never chooses iter_dyn or iter_dyn_mut automatically, even when xs is a dynamic iterable pointer. If xs: *const dyn Iterable(Item), the allocation boundary must be explicit:

var it = try xs.iter_dyn(alloc)
defer it.dispose()

for item in it {
}

for item in xs is rejected when xs is only a dynamic iterable pointer and not itself an iterator. This differs from *dyn Iterator(Item), which is already a borrowed iterator and may be looped directly:

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

*const dyn Iterator(Item) remains rejected because next mutates.

Dynamic Iterator Construction

Dynamic iterator construction may allocate, can fail, and creates an owning resource, so V1 keeps that boundary visible:

var xs = make_items()
var it = try xs.iter_dyn(alloc)
defer it.dispose()

for item in it {
}

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

var it = try make_items().iter_dyn(alloc) // error for borrowed iterator implementations

Use a named binding:

var xs = make_items()
var it = try xs.iter_dyn(alloc)

Owned dynamic iterator construction from moved or rvalue receivers is deferred. V1 does not include an iter_dyn_owned-style API:

var it = try (move xs).iter_dyn_owned(alloc) // deferred

Static for item in make_items() remains the ergonomic path for rvalue collection loops.

Mutable dynamic iteration uses the same pattern. It returns a boxed iterator whose item type is usually a mutable pointer:

var xs = make_items()
var it = try xs.iter_dyn_mut(alloc)
defer it.dispose()

for item in it {
  item.* = value
}

The mutable borrow of the source lasts until the iterator box is disposed.

iter_dyn_mut(alloc) requires a mutable source place, just like iter_mut():

var xs = make_items()
var it = try xs.iter_dyn_mut(alloc) // ok

Const source bindings are rejected:

const ys = make_items()
var bad = try ys.iter_dyn_mut(alloc) // error

Calling iter_dyn_mut on an rvalue receiver is rejected in V1:

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

The boxed mutable iterator may borrow from the source after the call expression. V1 does not create an implicit hidden receiver lifetime for this method-call form; use a named mutable binding instead.