VIP-21: producers: effect function* returns a Stream
Status: accepted by Will, 2026-10-28.
Decision
effect function* ticks() {
yield 1
try sleep("1 second")
yield 2
}
for try (const t of ticks()) { … } // VIP-20
// compiles to Effect's Stream.callback
function ticks() {
return Stream.callback<number>((queue) => Effect.gen(function* () {
yield* Queue.offer(queue, 1)
yield* Effect.sleep("1 second")
yield* Queue.offer(queue, 2)
}).pipe(Effect.andThen(Queue.end(queue))), { bufferSize: 1, strategy: "suspend" }) // ends the stream when the body returns
}effect function*returns a Stream, asasync function*returns an AsyncIterable.yield xemitsx, andtryruns Effects (VIP-5).- A plain
function*stays a JavaScript generator. - Amended 2026-10-30: inferred producer requirements. Infer the body requirements
RBodyas for anEffect, then exposeExclude<RBody, Scope.Scope>on theStream: the acceptedStream.callbacksupplies itsScope. Other service requirements remain. Explicit annotations keep ordinary contract checking, and values of typeScope.Scopeis unchanged. Each callback consumption owns that providedScope; this adds no new scheduling or finalizer order. See Producers and the typed lifetime witness. - It compiles to Effect's
Stream.callbackwith one element of buffer. For one sequential emitter, the producer may run up to two elements ahead of the consumer: one in the buffer and one waiting to be offered. With concurrent yields, each active item fiber can evaluate a value before waiting inQueue.offer; the one-element buffer does not bound all pending offers or upstream work. Unbounded concurrency has no constant global lead bound. A finite concurrency limit bounds active pending emitters in addition to the buffered element and work already consumed. Exact pull semantics, which would need a continuation runtime, are not provided. return valuein a producer is a compile error, because a Stream has no return value. A barereturnends the stream.- Amended 2026-10-30: the producer-return diagnostic is
EK2128.return valuein a producer isEK2128, because a Stream has no return value; a barereturnends the stream. This assigns the next free code in VIP-21's block, replacing the oldEK2101, which0.0.10reports for another rule. The oldEK2101is retired and will never be reassigned. Earlier numbers in VIP-21's block are already taken or historical, so this changes no diagnostic's meaning for a0.0.10user. Decided by the agent on 2026-10-30 under VIP-34's code-numbering amendment. - Ambiguity: none for
effect function* name(). The shorthandeffect *name()exists only for methods, asasync *gen()does in TypeScript. At top level it could parse as multiplication. - Amended 2026-10-29: no delegation yet.
yield*in a producer is a compile error until a VIP specifies delegation. Decided by the agent on 2026-10-29 under Will's rule "when I state a principle, the agent derives the concrete rule and acts; I can override later" (decided detail 50).
History
About 9 findings, on the old design (a type decided a producer's fiber; a runtime built on continuations). Neither applies to this design.
- Amended 2026-10-30: parameter yield ownership.
yieldin directly evaluated formal-parameter initialization of any Effect callable, a producer included, isEK2104. A producer ownsyieldin its body, not its parameter initialization; nested native generators and ordinary async callables keep their own contexts. The existing nonproducer Effect-body rejection remains unchanged.
Prior art
JavaScript's async function*, Kotlin's flow { emit(x) }, Python's async generators.
Will's words
"Accept"
Corrected 2026-10-29: one sequential emitter can run up to two elements ahead (one buffered, one waiting); concurrent emitters can each have a pending offer. The Effect TS needs Queue.end(queue) after the body, or the stream never completes (found by the differential fuzz suite).
- Clarified 2026-11-03: written producer contracts. Written return annotations follow TypeScript's ordinary contract checking on the
Stream.callbackcounterpart. A writtenStream.Stream<A, E, R>checks the yielded values, escaping errors and supplied requirements; otherwise infer the body channels and check the resulting Stream against the annotation. Aliases,any,unknownand compatible unions are admitted; an incompatible annotation isEK2102. This adds no annotation syntax.