- Status: Shipped (sync iterators); async iterators shipped in #128
- Date: 2026-05-25
- Phase: Phase 7 follow-up — iterator support
- Related: ADR-0023 (async state machine), ADR-0031 (canonical
for x in collection), ADR-0034 (imported CLR interop), ADR-0039 (by-ref pointers)
GSharp already supports consumer-side iteration via for x in collection (ADR-0031), which works on arrays, slices, dictionaries, IEnumerable[T], non-generic IEnumerable, and pattern-based GetEnumerator(). However, there is no producer-side iterator syntax — users cannot define lazy sequences that yield values one at a time. This means custom iteration logic must allocate a full collection up-front or implement the IEnumerator[T] interface manually, neither of which is ergonomic.
The async state-machine infrastructure (ADR-0023) has established patterns for synthesized types, field maps, kickoff stubs, and MoveNext bodies. The sync iterator pattern is simpler (no builder, no awaiter) and can leverage the same architectural patterns.
Async iterators (IAsyncEnumerable[T]) are aspirational but blocked until sync iterators are solid. This ADR covers sync iterators only.
By analogy with map[K,V] aliasing Dictionary[K, V] (Phase 3.A.4), sequence[T] is the GSharp-flavored spelling of System.Collections.Generic.IEnumerable<T>. Both forms are interchangeable in declarations, parameter types, return types, and assignments. At the CLR metadata level they are the same type — no wrapper, no indirection.
The sequence keyword is contextual: it is recognized only in type-annotation position (before [), and remains a valid identifier elsewhere.
yield <expr> produces the next value of the enclosing iterator function. It is legal only in functions whose return type is one of:
IEnumerable[T](equivalentlysequence[T])IEnumerator[T]IEnumerable(non-generic)IEnumerator(non-generic)
A function containing any yield statement becomes an iterator function. The <expr> must be assignable to the element type of the enclosing iterator's return type.
This slice does not implement yield break. The GSharp answer for early termination of an iterator is plain return — running off the end of the function body or executing return causes MoveNext() to return false. This is consistent with Go's approach where return is the single exit mechanism, and avoids a two-keyword statement form that has no analog in Go or Kotlin.
IAsyncEnumerable[T] with mixed yield + await shipped in #128 as part of the ADR-0023 async emitter rollout. The asyncSequence[T] alias is deferred to a future ADR; users spell the type as IAsyncEnumerable[T] directly today.
Parses identically to map[K,V]: the contextual keyword sequence followed by [, a type clause, and ]. Example:
func fib(max: int) sequence[int] {
a, b := 0, 1
for a <= max {
yield a
a, b = b, a+b
}
}
Parses as a statement. The keyword yield is contextual — it is recognized as a keyword only when it appears as the first token of a statement and is followed by an expression (not a semicolon, }, or EOF). The bare identifier yield remains legal as a variable name:
yield := 42 // legal: short variable declaration
fmt.Println(yield) // legal: yield as identifier
yield x + 1 // legal: yield statement in an iterator function
Disambiguation: at statement-start position, if the current token text is "yield" and the next token is not :=, ++, --, ,, ., (, [, =, or an assignment operator, it is parsed as a yield statement. Otherwise it falls through to expression/assignment parsing where yield is treated as an identifier.
The execution model is identical to C#'s iterator state machine:
- Lazy: calling an iterator function returns immediately with an
IEnumerable[T]instance. No user code runs until the firstMoveNext(). - Deferred side-effects: side effects in the function body execute only when
MoveNext()is called. - One-shot per enumeration: each call to
GetEnumerator()returns a fresh enumerator that starts from the beginning. The initial instance can serve as bothIEnumerable[T]andIEnumerator[T]for the first enumeration (thread-id optimization). - Composition with
for x in: because iterator functions returnIEnumerable[T], they compose naturally with the existingfor x in seqconsumption (ADR-0031).
Synthesize a sealed class implementing IEnumerable<T>, IEnumerator<T>, IDisposable:
- Fields:
<>1__state(int),<>2__current(T),<>l__initialThreadId(int), parameter proxies, hoisted locals. MoveNext(): state-dispatch switch at entry; eachyield xbecomescurrent = x; state = K; return true; resumeK: state = -1;. End of body:state = -1; return false;.get_Current: returns<>2__current.Dispose():state = -1(no try/finally support this slice).Reset(): throwsNotSupportedException.GetEnumerator(): ifstate == -2 && threadId == initialThreadId, transitions to state 0 and returnsthis; otherwise allocates a fresh instance with parameters copied.- Non-generic
IEnumerator.Current: boxes via the generic property.
The original function body is replaced with a kickoff stub: allocate the state machine with state = -2, copy parameters, return.
This reuses architectural patterns from the async state-machine pipeline (SynthesizedStateMachineType, ResumableStateAllocator, etc.) but is a separate, simpler lowering pass since there is no builder or awaiter machinery.
sequence[T] and IEnumerable[T] are the same type at the CLR metadata level. Functions declared with either spelling produce identical IL. Mixing both forms in a single program is well-defined. Values flow freely between GSharp iterators and .NET LINQ, foreach, and any other IEnumerable[T] consumer.
| ID | Message |
|---|---|
| GS9101 | yield statement is not allowed outside an iterator function |
| GS9102 | Iterator function must return IEnumerable[T], IEnumerator[T], IEnumerable, or IEnumerator |
| GS9103 | Cannot convert yielded expression of type {actual} to iterator element type {expected} |
| GS9104 | yield break is not supported; use return to end iteration |
- Async iterator alias:
IAsyncEnumerable[T]support shipped in #128 but noasyncSequence[T]alias exists yet. ADR-0041 proposes overloadingsequence[T]inasyncreturn-type position to meanIAsyncEnumerable[T], in lieu of a separate keyword. - Try/finally in iterators: this slice does not support
try/finallywithin iterator bodies (would requireDispose()to resume into finally blocks). A diagnostic is emitted if detected. - Interpreter parity: the interpreter (
Evaluator.cs) does not gainyieldsupport in this slice. Iterator functions are emit-only. This is a known gap tracked as #138 — the interpreter is authoritative perdocs/emit-pipeline.mdbut iterator state machines are inherently an emit concern.
A Go-style channel-based generator (func fib() chan int) was considered but rejected because it requires goroutine allocation per iteration, has different lifetime semantics, and doesn't compose with LINQ or foreach.
Making yield a hard keyword was rejected because it would break any existing code using yield as a variable name and is inconsistent with the contextual-keyword precedent set by in (ADR-0031) and variance markers (ADR-0021).
Rejected in favor of bare yield <expr> for brevity and Go/Kotlin flavor. The return in C#'s yield return is vestigial — it aids disambiguation in C#'s grammar but is unnecessary in GSharp where yield is unambiguous at statement start.
This ADR introduces producer-side iterator support via sequence[T] (alias for IEnumerable[T]) and yield <expr>. The feature uses a synthesized state-machine class following the well-established C# iterator pattern, reuses architectural patterns from the existing async pipeline, and composes naturally with the already-shipped for x in consumption syntax. yield break is omitted in favor of plain return. Async iterators (IAsyncEnumerable[T] with mixed yield + await) shipped in #128 as a follow-on to this ADR.