Coroutines and Generators
The coroutine is the building block of concurrency in Zolo. With
coroutine.create, coroutine.resume, and coroutine.yield you can suspend
and resume functions arbitrarily — no OS threads, no data locks.
The lifecycle is simple: suspended → running → suspended (at each
yield) → dead (when the body returns).
Creation, resume with and without arguments, and status inspection with coroutine.status.
// Feature: Coroutines (create / resume / yield)
// Syntax: `coroutine.create(fn)`, `coroutine.resume(co)`, `coroutine.yield(v)`
// When to use: lightweight state machines, pause and resume work,
// pull-based pipelines, simulating threads without OS threads.
//
// A coroutine is a suspended function. Calling `resume` runs it
// until the next `yield` (or return). Each `yield` "pushes"
// a value back to whoever called `resume`.
use std::coroutine
// Simple coroutine — produces three values and ends.
let co = coroutine.create(|| {
coroutine.yield("first")
coroutine.yield("second")
coroutine.yield("third")
})
print(coroutine.resume(co)) // first
print(coroutine.resume(co)) // second
print(coroutine.resume(co)) // third
print(coroutine.status(co)) // dead
// Coroutine with arguments — `resume` passes, `yield` returns.
let echo = coroutine.create(|x: int| {
var n = x
while n > 0 {
coroutine.yield(n)
n = n - 1
}
})
print(coroutine.resume(echo, 3)) // 3
print(coroutine.resume(echo)) // 2
print(coroutine.resume(echo)) // 1
// Status: "suspended" before the first resume, "dead" after it returns.
let lazy = coroutine.create(|| {
coroutine.yield(42)
})
print(coroutine.status(lazy)) // suspended
coroutine.resume(lazy)
print(coroutine.status(lazy)) // suspended (still has a pending yield)
coroutine.resume(lazy) // finishes the body
print(coroutine.status(lazy)) // dead
Generators (fn*) are a syntactic sugar layer on top of coroutines aimed
at lazy sequences. The function returns a handle that, when called, advances to
the next yield. When the body finishes, it returns nil.
This allows infinite sequences: work only happens when the caller asks for the next value.
Finite generator, infinite Fibonacci, string generator, and a generator+filter pipeline.
// Feature: Generators (`fn*`) with yield
// Syntax: `fn* name(params) { yield expr }`
// When to use: lazy/infinite sequences, streams, pipelines where
// we do not want to materialize everything in memory.
//
// A generator is a function that produces values on demand. Each
// call to the returned handle advances to the next `yield`. When
// the function ends, it returns `nil`.
// Finite generator.
fn* counter(limit: int) {
var i = 0
while i < limit {
yield i
i += 1
}
}
let gen = counter(4)
print(gen()) // 0
print(gen()) // 1
print(gen()) // 2
print(gen()) // 3
print(gen()) // nil (exhausted)
// INFINITE generator — possible because it is lazy.
fn* fibonacci() {
var a = 0
var b = 1
loop {
yield a
let temp = a + b
a = b
b = temp
}
}
let fib = fibonacci()
for i in 0..8 {
print("fib({i}) = {fib()}")
}
// expected: fib(0) = 0 ... fib(7) = 13
// String generator — any type works.
fn* statuses() {
yield "loading"
yield "ready"
yield "done"
}
let st = statuses()
var s = st()
while s != nil {
print("status: {s}")
s = st()
}
// Pipeline: generator + filter.
fn* range_step(start: int, stop: int, step: int) {
var i = start
while i < stop {
yield i
i += step
}
}
let evens = range_step(0, 10, 2)
var v = evens()
while v != nil {
print("even: {v}")
v = evens()
}
Challenge
Write a generator fn* primes() that yields prime numbers indefinitely.
Use a manual take to print the first 10.