|
| 1 | +# Generators |
| 2 | + |
| 3 | +A generator function is any function whose body contains a `yield` statement. |
| 4 | +Instead of computing a single return value, generators produce a sequence of |
| 5 | +values lazily — each `yield` suspends the function and emits one element to the |
| 6 | +caller. |
| 7 | + |
| 8 | +```python |
| 9 | +def count_up(n: int) -> int: |
| 10 | + i = 0 |
| 11 | + while i < n: |
| 12 | + yield i |
| 13 | + i += 1 |
| 14 | + |
| 15 | +def main(): |
| 16 | + for x in count_up(3): |
| 17 | + print(x) # 0, 1, 2 |
| 18 | +``` |
| 19 | + |
| 20 | +## Syntax |
| 21 | + |
| 22 | +``` |
| 23 | +yield_stmt ::= 'yield' expression |
| 24 | + | 'yield' 'from' expression |
| 25 | +``` |
| 26 | + |
| 27 | +### `yield expression` |
| 28 | + |
| 29 | +Produces a single value to the caller and suspends the generator. |
| 30 | + |
| 31 | +```python |
| 32 | +def squares(n: int) -> int: |
| 33 | + for i in range(n): |
| 34 | + yield i * i |
| 35 | +``` |
| 36 | + |
| 37 | +### `yield from expression` |
| 38 | + |
| 39 | +Delegates to another iterable, yielding all of its values in order before |
| 40 | +continuing with the current generator. |
| 41 | + |
| 42 | +```python |
| 43 | +def inner() -> int: |
| 44 | + yield 0 |
| 45 | + yield 1 |
| 46 | + |
| 47 | +def outer() -> int: |
| 48 | + yield from inner() |
| 49 | + yield 2 |
| 50 | + yield 3 |
| 51 | + |
| 52 | +# Produces: 0, 1, 2, 3 |
| 53 | +``` |
| 54 | + |
| 55 | +## Return Type Annotation |
| 56 | + |
| 57 | +The return type annotation on a generator specifies the **element type**, not |
| 58 | +the collection type. The compiler wraps it automatically: |
| 59 | + |
| 60 | +```python |
| 61 | +# Annotate with element type T — compiler produces IEnumerable<T> |
| 62 | +def fibonacci(n: int) -> int: |
| 63 | + a = 0 |
| 64 | + b = 1 |
| 65 | + for i in range(n): |
| 66 | + yield a |
| 67 | + a, b = b, a + b |
| 68 | +``` |
| 69 | + |
| 70 | +| Context | Annotation | Emitted C# return type | |
| 71 | +|---------|-----------|------------------------| |
| 72 | +| Standalone function | `-> T` | `IEnumerable<T>` | |
| 73 | +| `__iter__` method | `-> T` | `IEnumerator<T>` | |
| 74 | +| `__reversed__` method | `-> T` | `IEnumerator<T>` | |
| 75 | + |
| 76 | +## Early Termination |
| 77 | + |
| 78 | +A bare `return` (without a value) terminates the generator early: |
| 79 | + |
| 80 | +```python |
| 81 | +def take(n: int) -> int: |
| 82 | + i = 0 |
| 83 | + while True: |
| 84 | + if i >= n: |
| 85 | + return # stops iteration |
| 86 | + yield i |
| 87 | + i += 1 |
| 88 | +``` |
| 89 | + |
| 90 | +Returning a value from a generator is forbidden (see [Restrictions](#restrictions)). |
| 91 | + |
| 92 | +## Generator `__iter__` and `__reversed__` |
| 93 | + |
| 94 | +Using `yield` inside `__iter__` makes a class iterable without writing a |
| 95 | +separate iterator class: |
| 96 | + |
| 97 | +```python |
| 98 | +class Countdown: |
| 99 | + values: list[int] |
| 100 | + |
| 101 | + def __init__(self, values: list[int]): |
| 102 | + self.values = values |
| 103 | + |
| 104 | + def __iter__(self) -> int: |
| 105 | + for v in self.values: |
| 106 | + yield v |
| 107 | +``` |
| 108 | + |
| 109 | +Similarly, `yield` inside `__reversed__` provides reverse iteration: |
| 110 | + |
| 111 | +```python |
| 112 | +class Range: |
| 113 | + start: int |
| 114 | + end: int |
| 115 | + |
| 116 | + def __init__(self, start: int, end: int): |
| 117 | + self.start = start |
| 118 | + self.end = end |
| 119 | + |
| 120 | + def __iter__(self) -> int: |
| 121 | + i = self.start |
| 122 | + while i < self.end: |
| 123 | + yield i |
| 124 | + i += 1 |
| 125 | + |
| 126 | + def __reversed__(self) -> int: |
| 127 | + i = self.end - 1 |
| 128 | + while i >= self.start: |
| 129 | + yield i |
| 130 | + i -= 1 |
| 131 | +``` |
| 132 | + |
| 133 | +When `__iter__` contains `yield`, the compiler synthesizes `IEnumerable<T>` on |
| 134 | +the class (reported via SPY1001 info diagnostic). When `__reversed__` contains |
| 135 | +`yield`, `IReverseEnumerable<T>` is synthesized. |
| 136 | + |
| 137 | +## Restrictions |
| 138 | + |
| 139 | +### No `yield` inside `__next__` |
| 140 | + |
| 141 | +The `__next__` method is part of the *explicit* iterator protocol (manual state |
| 142 | +management). Combining it with `yield` (which generates a state machine |
| 143 | +automatically) is contradictory. |
| 144 | + |
| 145 | +```python |
| 146 | +class Bad: |
| 147 | + def __next__(self) -> int: |
| 148 | + yield 1 # ERROR: SPY0268 |
| 149 | +``` |
| 150 | + |
| 151 | +### No `return` with a value |
| 152 | + |
| 153 | +Generators produce values via `yield`. A `return` with a value has no |
| 154 | +well-defined meaning and is rejected: |
| 155 | + |
| 156 | +```python |
| 157 | +def bad_gen() -> int: |
| 158 | + yield 1 |
| 159 | + return 42 # ERROR: SPY0267 |
| 160 | +``` |
| 161 | + |
| 162 | +Use a bare `return` for early termination instead. |
| 163 | + |
| 164 | +### No mixing generator `__iter__` with `__next__` |
| 165 | + |
| 166 | +A class must choose one approach: either a generator-based `__iter__` (with |
| 167 | +`yield`) or an explicit iterator (with `__next__`). Defining both is an error: |
| 168 | + |
| 169 | +```python |
| 170 | +class Bad: |
| 171 | + def __iter__(self) -> int: |
| 172 | + yield 1 # generator-based |
| 173 | + def __next__(self) -> int: |
| 174 | + return 1 # explicit — ERROR: SPY0269 |
| 175 | +``` |
| 176 | + |
| 177 | +### Nested functions do not propagate |
| 178 | + |
| 179 | +A `yield` inside a nested function definition does not make the enclosing |
| 180 | +function a generator: |
| 181 | + |
| 182 | +```python |
| 183 | +def outer() -> int: |
| 184 | + def inner() -> int: |
| 185 | + yield 1 # inner is the generator, not outer |
| 186 | + return 42 # outer is a normal function |
| 187 | +``` |
| 188 | + |
| 189 | +## Diagnostics |
| 190 | + |
| 191 | +| Code | Level | Description | |
| 192 | +|------|-------|-------------| |
| 193 | +| SPY0265 | Error | `yield` outside a function | |
| 194 | +| SPY0267 | Error | `return` with a value in a generator function | |
| 195 | +| SPY0268 | Error | `yield` inside `__next__` | |
| 196 | +| SPY0269 | Error | Class has both generator `__iter__` and `__next__` | |
| 197 | + |
| 198 | +## Implementation |
| 199 | + |
| 200 | +*`yield expr` → `yield return expr;` (C# iterator method)* |
| 201 | + |
| 202 | +*`yield from expr` → `foreach (var item in expr) { yield return item; }` (delegation via loop)* |
| 203 | + |
| 204 | +*Bare `return` in generator → `yield break;` (early termination)* |
| 205 | + |
| 206 | +*Generator detection is automatic: the compiler scans for `YieldStatement` nodes |
| 207 | +in the function body (excluding nested function/class definitions). Functions |
| 208 | +containing yield have `IsGenerator = true` on their symbol.* |
0 commit comments