Pipelines and State Machines
When each stage of a processing pipeline is a generator that consumes another generator, you get a pull-based pipeline: nothing is materialized in memory until the final consumer pulls a value. Compose as many stages as you like — the cost is constant per item.
Three-stage pipeline (source → keep_evens → square) and manual extraction of N items from an infinite sequence.
// Feature: pipelines with coroutines/generators
// Syntax: chain `fn*` consuming each other.
// When to use: stage-based processing (parsing, transformation,
// aggregation) without huge intermediate buffers.
//
// Each stage is a generator: pull-based, lazy, composable.
// Stage 1: produces integers 1..N.
fn* source(n: int) {
var i = 1
while i <= n {
yield i
i += 1
}
}
// Stage 2: keeps even numbers (receives a generator handle).
fn* keep_evens(input: any) {
var v = input()
while v is not nil {
if v % 2 == 0 {
yield v
}
v = input()
}
}
// Stage 3: maps to the square.
fn* square(input: any) {
var v = input()
while v is not nil {
yield v * v
v = input()
}
}
// Composed pipeline.
let pipe = square(keep_evens(source(8)))
var x = pipe()
while x is not nil {
print("out: {x}")
x = pipe()
}
// expected: out: 4, out: 16, out: 36, out: 64
// Manual pull of N items.
fn take(input: any, n: int) -> [int] {
let result: [int] = []
var i = 0
while i < n {
let v = input()
if v is nil { break }
result.push(v)
i += 1
}
return result
}
fn* naturals() {
var i = 1
loop {
yield i
i += 1
}
}
let first10 = take(naturals(), 10)
print("first 10: {first10}")
// expected: first 10: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Coroutines also model state machines without enum + switch: the execution
position between two yields is the state. This is natural for phased
parsers, dialogue trees, agents with steps, and game NPCs.
NPC with four states (idle/patrol/chase/attack), a mini-lexer, and a dialogue tree, all expressed via yield.
// Feature: state machines with coroutines
// Syntax: each `yield` is a transition; state lives between yields.
// When to use: step-by-step parsers, agents with phases, game NPCs,
// flows with state rollback.
//
// The coroutine "remembers" the execution point. No need for enum +
// switch — the code flow itself is the machine.
// NPC with 4 states: idle -> patrol -> chase -> attack -> idle...
// Note: a real loop would use `while true { ... }`; here we unroll
// two cycles to keep the example short and avoid a known codegen
// edge case (multiple `fn*` with looping body in one file).
fn* npc_brain(name: str) {
yield "{name}: idle"
yield "{name}: patrol"
yield "{name}: chase"
yield "{name}: attack"
yield "{name}: idle"
yield "{name}: patrol"
}
let bot = npc_brain("orc-1")
for _ in 0..6 {
print(bot())
}
// SKIP: state-based parser with nested while + yield inside if/else
// branches currently triggers a Lua codegen ambiguity in the runtime.
// The pattern is valid Zolo, but the lowered code becomes ambiguous.
// A simpler example below shows the same idea (state via `yield`)
// without the nested loops.
//
// Tiny lexer-like state machine: emits a token per call.
fn* tokens_demo() {
yield "ID(abc)"
yield "NUM(123)"
yield "ID(def)"
yield "NUM(45)"
}
let tokens = tokens_demo()
var t = tokens()
while t != nil {
print(t)
t = tokens()
}
// Dialogue with phases: each `yield` waits for the player to press "next".
fn* dialogue() {
yield "Hello, traveler."
yield "You look tired."
yield "Want a potion?"
yield "Good luck on the journey!"
}
let d = dialogue()
var line = d()
while line != nil {
print("[npc] {line}")
line = d()
}
When to use this pattern vs.
machine X { ... }? Prefer coroutines when the state is a flow (parser, linear dialogue). Use themachinekeyword when the state is a graph with explicit transitions (protocols, devices).
See also