Coroutines

A generator function produces values one at a time. Declare the return type as iterator<T> and use yield to emit each value. The function body is suspended between yields and resumed when the next value is requested. When the body finishes the generator is exhausted. Generators are useful when you want to produce values on demand, stop early with break, or chain sequences without building intermediate collections.

Iterating with a for loop

A for loop drives the generator forward automatically. The body runs once per yielded value. Use break to stop early without consuming the rest.

The body really is suspended

Calling a generator runs none of its body; each advance runs exactly one more slice, up to the next yield. This holds on both backends for every yield that is a statement of its own.

Manual advance with next() and exhausted()

Call next(gen) yourself when you need fine-grained control.

Generators with parameters

A generator function may take any parameters, including text. Parameter values are preserved across yields so the generator can use them throughout its lifetime.

Delegation with yield from

yield from sub_gen() forwards all values from another generator inline, as if each of its yields appeared at this point directly.

Early termination with break

Stopping a for loop before the generator is exhausted is safe. The abandoned generator frame is freed automatically.

A generator runs once, and has no return

Two rules a reader coming from another language will look for.

struct Trace {
  steps: integer,
}
fn count_up(start: integer, stop: integer) -> iterator<integer> {
  for i in start..stop {
    yield i;
  }
}
fn squares(n: integer) -> iterator<integer> {
  for i in 1..=n {
    yield i * i;
  }
}

Records one step per value produced, so the consumer can see how much of the body actually ran.

fn counted(limit: integer, t: Trace) -> iterator<integer> {
  for i in 1..=limit {
    t.steps += 1;
    yield i;
  }
}

Yields the length of s twice — shows that a text parameter survives yields.

fn len_twice(s: text) -> iterator<integer> {
  yield len(s);
  yield len(s);
}
fn inner_vals() -> iterator<integer> {
  yield 10;
  yield 20;
}
fn outer_combined() -> iterator<integer> {
  yield 1;
  yield from inner_vals();
  yield 2;
}

Delegating to a generator that takes an ARGUMENT: a plain forwarding loop means the same thing as yield from scaled_by(4), and either spelling works.

fn scaled_by(factor: integer) -> iterator<integer> {
  yield factor;
  yield factor * 10;
}
fn forwards_with_argument() -> iterator<integer> {
  for v in scaled_by(4) {
    yield v;
  }
}
fn large_range(limit: integer) -> iterator<integer> {
  for i in 1..=limit {
    yield i;
  }
}

Ends early with break. A generator has no return, so break is how a body stops before its loop would.

fn first_positive(v: vector<integer>) -> iterator<integer> {
  for x in v {
    if x > 0 { yield x; break; }
  }
}
fn main() {

Iterating with a for loop

  total = 0;
  for n in count_up(1, 6) {
    total += n;
  }
  assert(total == 15, "sum 1..5: {total}");

Squares via for loop with index tracking.

  idx = 0;
  for s in squares(4) {
    idx += 1;
    if idx == 1 { assert(s == 1, "sq1: {s}"); }
    if idx == 2 { assert(s == 4, "sq2: {s}"); }
    if idx == 3 { assert(s == 9, "sq3: {s}"); }
    if idx == 4 { assert(s == 16, "sq4: {s}"); }
  }
  assert(idx == 4, "squares count: {idx}");

The body really is suspended

counted is asked for a thousand values and the loop breaks after six. Its step counter shows the body ran six times, not a thousand — the work for the values nobody asked for was never done.

⚠ That holds on BOTH backends for every yield that is a statement: in a for or while loop, under an if or match, in a nested loop, beside a continue, whatever type it yields. A yield inside an EXPRESSION, after an operand that could change while the generator waits (s = s + { yield x; "b" } with s a text), runs the whole loop eagerly on --native, so its side effects happen for values the consumer never asks for. The VALUES are the same either way. An ENDLESS eager loop never hands out a value at all: on --native it runs until memory runs out. Keep each yield a statement of its own.

  trace = Trace { steps: 0 };
  seen = 0;
  for n in counted(1000, trace) {
    seen += 1;
    if seen > 5 { break; }
  }
  assert(trace.steps == 6, "the body ran once per value asked for: {trace.steps}");

Manual advance with next() and exhausted()

exhausted is true once an advance has run off the END of the body — not once the last value has been handed out. After the third value here there are no more values, and exhausted is still false; it takes one more next to discover the end, and that call answers null.

So while !exhausted(g) runs one iteration too many. Drive the generator with for, or stop on the null that next answers.

  gen = count_up(10, 13);
  a = next(gen);
  b = next(gen);
  c = next(gen);
  assert(a == 10 && b == 11 && c == 12, "manual next: {a},{b},{c}");
  assert(!exhausted(gen), "not yet exhausted, though no values remain");
  past_end = next(gen);
  assert(past_end == null, "next past the last value answers null");
  assert(exhausted(gen), "and now it is exhausted");

Generators with parameters

len_twice takes a text parameter and yields its length twice. The text value is preserved across the yield.

  lt_total = 0;
  for n in len_twice("hello") {
    lt_total += n;
  }
  assert(lt_total == 10, "len_twice: {lt_total}");

Delegation with yield from

  yf_total = 0;
  for n in outer_combined() {
    yf_total += n;
  }
  assert(yf_total == 33, "yield from: 1+10+20+2={yf_total}");

A forwarding loop — for v in sub(arg) { yield v; } — means the same thing as yield from sub(arg); both work on both backends.

  fwd_total = 0;
  for n in forwards_with_argument() {
    fwd_total += n;
  }
  assert(fwd_total == 44, "forwarded with an argument: 4+40={fwd_total}");

Early termination with break

Stop after the first 5 values from a 1000-element generator.

  limit_total = 0;
  for n in large_range(1000) {
    if n > 5 { break; }
    limit_total += n;
  }
  assert(limit_total == 15, "early break sum 1..5: {limit_total}");

A generator runs once, and has no return

A generator is single-pass. Once it is exhausted it stays exhausted, so a second for over the same generator value runs its body zero times — quietly, because an exhausted iterator is empty rather than an error. Call the generator function again when you need the sequence again.

  once = count_up(1, 4);
  first_pass = 0;
  for n in once { first_pass += n; }
  second_pass = 0;
  for n in once { second_pass += n; }
  assert(first_pass == 6, "first pass: {first_pass}");
  assert(second_pass == 0, "the same generator a second time is empty: {second_pass}");

And a generator has no return: values leave it only through yield, so return, return value and a body ending in a value are all refused at compile time. break is how a body ends early — first_positive yields the first positive element and stops there.

  fp_seq = 0;
  for n in first_positive([-1, 5, 9]) { fp_seq = fp_seq * 10 + n; }
  assert(fp_seq == 5, "first_positive stops after its one value: {fp_seq}");
}