|
18 | 18 | https://github.com/Technologicat/python-3-scicomp-intro/blob/master/examples/beyond_python/generator.rkt
|
19 | 19 | """
|
20 | 20 |
|
21 |
| -from ...syntax import macros, test, test_raises # noqa: F401 |
| 21 | +from mcpyrate.multiphase import macros, phase |
| 22 | + |
| 23 | +from ...syntax import macros, test, test_raises # noqa: F401, F811 |
22 | 24 | from ...test.fixtures import session, testset
|
23 | 25 |
|
24 | 26 | from ...syntax import macros, continuations, call_cc, dlet, abbrev, let_syntax, block # noqa: F401, F811
|
25 | 27 |
|
26 | 28 | from ...fploop import looped
|
27 | 29 | from ...fun import identity
|
28 | 30 |
|
29 |
| -#from mcpyrate.debug import macros, step_expansion # noqa: F811, F401 |
| 31 | +from mcpyrate.debug import macros, step_expansion # noqa: F811, F401 |
| 32 | + |
| 33 | +# TODO: pretty long, move into its own module |
| 34 | +# Multishot generators can also be implemented using the pattern `k = call_cc[get_cc()]`. |
| 35 | +# |
| 36 | +# Because `with continuations` is a two-pass macro, it will first expand any |
| 37 | +# `@multishot` inside the block before performing its own processing, which is |
| 38 | +# exactly what we want. |
| 39 | +# |
| 40 | +# We could force the ordering with the metatool `mcpyrate.metatools.expand_first` |
| 41 | +# added in `mcpyrate` 3.6.0, but we don't need to do that. |
| 42 | +# |
| 43 | +# To make these multi-shot generators support the most basic parts |
| 44 | +# of the API of Python's native generators, make a wrapper object: |
| 45 | +# |
| 46 | +# - `__iter__` on the original function should create the wrapper object |
| 47 | +# and initialize it. Maybe always inject a bare `myield` at the beginning |
| 48 | +# of a multishot function before other processing, and run the function |
| 49 | +# until it returns the initial continuation? This continuation can then |
| 50 | +# be stashed just like with any resume point. |
| 51 | +# - `__next__` needs a stash for the most recent continuation |
| 52 | +# per activation of the multi-shot generator. It should run |
| 53 | +# the most recent continuation (with no arguments) until the next `myield`, |
| 54 | +# stash the new continuation, and return the yielded value, if any. |
| 55 | +# - `send` should send a value into the most recent continuation |
| 56 | +# (thus resuming). |
| 57 | +# - When the function returns normally, without returning any further continuation, |
| 58 | +# the wrapper should `raise StopIteration`, providing the return value as argument |
| 59 | +# to the exception. |
| 60 | +# |
| 61 | +# Note that a full implementation of the generator API requires much |
| 62 | +# more. We should at least support `close` and `throw`, and think hard |
| 63 | +# about how to handle exceptions. Particularly, a `yield` inside a |
| 64 | +# `finally` is a classic catch. This sketch also has no support for |
| 65 | +# `yield from`; we would likely need our own `myield_from`. |
| 66 | +with phase[1]: |
| 67 | + # TODO: relative imports |
| 68 | + # TODO: mcpyrate does not recognize current package in phases higher than 0? (parent missing) |
| 69 | + |
| 70 | + import ast |
| 71 | + from functools import partial |
| 72 | + import sys |
| 73 | + |
| 74 | + from mcpyrate.quotes import macros, q, a, h # noqa: F811 |
| 75 | + from unpythonic.syntax import macros, call_cc # noqa: F811 |
| 76 | + |
| 77 | + from mcpyrate import namemacro, gensym |
| 78 | + from mcpyrate.quotes import is_captured_value |
| 79 | + from mcpyrate.utils import extract_bindings |
| 80 | + from mcpyrate.walkers import ASTTransformer |
| 81 | + |
| 82 | + from unpythonic.syntax import get_cc, iscontinuation |
| 83 | + |
| 84 | + def myield_function(tree, syntax, **kw): |
| 85 | + if syntax not in ("name", "expr"): |
| 86 | + raise SyntaxError("myield is a name and expr macro only") |
| 87 | + |
| 88 | + # Accept `myield` in any non-load context, so that we can below define the macro `it`. |
| 89 | + # |
| 90 | + # This is only an issue, because this example uses multi-phase compilation. |
| 91 | + # The phase-1 `myield` is in the macro expander - preventing us from referring to |
| 92 | + # the name `myield` - when the lifted phase-0 definition is being run. During phase 0, |
| 93 | + # that makes the line `myield = namemacro(...)` below into a macro-expansion-time |
| 94 | + # syntax error, because that `myield` is not inside a `@multishot`. |
| 95 | + # |
| 96 | + # We hack around it, by allowing `myield` anywhere as long as the context is not a `Load`. |
| 97 | + if hasattr(tree, "ctx") and type(tree.ctx) is not ast.Load: |
| 98 | + return tree |
| 99 | + |
| 100 | + raise SyntaxError("myield may only appear inside a multishot function") |
| 101 | + myield = namemacro(myield_function) |
| 102 | + |
| 103 | + def multishot(tree, syntax, expander, **kw): |
| 104 | + """[syntax, block] Multi-shot generators based on the pattern `k = call_cc[get_cc()]`.""" |
| 105 | + if syntax != "decorator": |
| 106 | + raise SyntaxError("multishot is a decorator macro only") # pragma: no cover |
| 107 | + if type(tree) is not ast.FunctionDef: |
| 108 | + raise SyntaxError("@multishot supports `def` only") |
| 109 | + |
| 110 | + # Detect the name(s) of `myield` at the use site (this accounts for as-imports) |
| 111 | + macro_bindings = extract_bindings(expander.bindings, myield_function) |
| 112 | + if not macro_bindings: |
| 113 | + raise SyntaxError("The use site of `multishot` must macro-import `myield`, too.") |
| 114 | + names_of_myield = list(macro_bindings.keys()) |
| 115 | + |
| 116 | + def is_myield_name(node): |
| 117 | + return type(node) is ast.Name and node.id in names_of_myield |
| 118 | + def is_myield_expr(node): |
| 119 | + return type(node) is ast.Subscript and is_myield_name(node.value) |
| 120 | + def getslice(subscript_node): |
| 121 | + if sys.version_info >= (3, 9, 0): # Python 3.9+: no ast.Index wrapper |
| 122 | + return subscript_node.slice |
| 123 | + return subscript_node.slice.value |
| 124 | + # We can work with variations of the pattern |
| 125 | + # |
| 126 | + # k = call_cc[get_cc()] |
| 127 | + # if iscontinuation(k): |
| 128 | + # return k |
| 129 | + # # here `k` is the data sent in via the continuation |
| 130 | + # |
| 131 | + # to create a multi-shot resume point. The details will depend on whether our |
| 132 | + # user wants each particular resume point to return and/or take in a value. |
| 133 | + # |
| 134 | + # Note that `myield`, beside optionally yielding a value, always returns the |
| 135 | + # continuation that resumes execution just after that `myield`. The caller |
| 136 | + # is free to stash the continuations and invoke earlier ones again, as needed. |
| 137 | + class MultishotYieldTransformer(ASTTransformer): |
| 138 | + def transform(self, tree): |
| 139 | + if is_captured_value(tree): # do not recurse into hygienic captures |
| 140 | + return tree |
| 141 | + # respect scope boundaries |
| 142 | + if type(tree) in (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, |
| 143 | + ast.ListComp, ast.DictComp, ast.SetComp, ast.GeneratorExp): |
| 144 | + return tree |
| 145 | + |
| 146 | + # `k = myield[value]` |
| 147 | + if type(tree) is ast.Assign and is_myield_expr(tree.value): |
| 148 | + if len(tree.targets) != 1: |
| 149 | + raise SyntaxError("expected exactly one assignment target in k = myield[expr]") |
| 150 | + var = tree.targets[0] |
| 151 | + value = getslice(tree.value) |
| 152 | + with q as quoted: |
| 153 | + a[var] = h[call_cc][h[get_cc]()] |
| 154 | + if h[iscontinuation](a[var]): |
| 155 | + return a[var], a[value] |
| 156 | + return quoted |
| 157 | + |
| 158 | + # `k = myield` |
| 159 | + elif type(tree) is ast.Assign and is_myield_name(tree.value): |
| 160 | + if len(tree.targets) != 1: |
| 161 | + raise SyntaxError("expected exactly one assignment target in k = myield[expr]") |
| 162 | + var = tree.targets[0] |
| 163 | + with q as quoted: |
| 164 | + a[var] = h[call_cc][h[get_cc]()] |
| 165 | + if h[iscontinuation](a[var]): |
| 166 | + return a[var] |
| 167 | + return quoted |
| 168 | + |
| 169 | + # `myield[value]` |
| 170 | + elif type(tree) is ast.Expr and is_myield_expr(tree.value): |
| 171 | + var = ast.Name(id=gensym("myield_cont")) |
| 172 | + value = getslice(tree.value) |
| 173 | + with q as quoted: |
| 174 | + a[var] = h[call_cc][h[get_cc]()] |
| 175 | + if h[iscontinuation](a[var]): |
| 176 | + return h[partial](a[var], None), a[value] |
| 177 | + return quoted |
| 178 | + |
| 179 | + # `myield` |
| 180 | + elif type(tree) is ast.Expr and is_myield_name(tree.value): |
| 181 | + var = ast.Name(id=gensym("myield_cont")) |
| 182 | + with q as quoted: |
| 183 | + a[var] = h[call_cc][h[get_cc]()] |
| 184 | + if h[iscontinuation](a[var]): |
| 185 | + return h[partial](a[var], None) |
| 186 | + return quoted |
| 187 | + |
| 188 | + return self.generic_visit(tree) |
| 189 | + |
| 190 | + class ReturnToStopIterationTransformer(ASTTransformer): |
| 191 | + def transform(self, tree): |
| 192 | + if is_captured_value(tree): # do not recurse into hygienic captures |
| 193 | + return tree |
| 194 | + # respect scope boundaries |
| 195 | + if type(tree) in (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, |
| 196 | + ast.ListComp, ast.DictComp, ast.SetComp, ast.GeneratorExp): |
| 197 | + return tree |
| 198 | + |
| 199 | + if type(tree) is ast.Return: |
| 200 | + # `return` |
| 201 | + if tree.value is None: |
| 202 | + with q as quoted: |
| 203 | + raise h[StopIteration] |
| 204 | + return quoted |
| 205 | + # `return value` |
| 206 | + with q as quoted: |
| 207 | + raise h[StopIteration](a[tree.value]) |
| 208 | + return quoted |
| 209 | + |
| 210 | + return self.generic_visit(tree) |
| 211 | + |
| 212 | + # ------------------------------------------------------------ |
| 213 | + # main processing logic |
| 214 | + |
| 215 | + # Make the multishot generator raise `StopIteration` when it finishes |
| 216 | + # via any `return`. First make the implicit bare `return` explicit. |
| 217 | + # |
| 218 | + # We must do this before we transform the `myield` statements, |
| 219 | + # to avoid breaking tail-calling the continuations. |
| 220 | + if type(tree.body[-1]) is not ast.Return: |
| 221 | + with q as quoted: |
| 222 | + return |
| 223 | + tree.body.extend(quoted) |
| 224 | + tree.body = ReturnToStopIterationTransformer().visit(tree.body) |
| 225 | + |
| 226 | + # Inject a bare `myield` resume point at the beginning of the function body. |
| 227 | + # This makes the resulting function work somewhat like a Python generator. |
| 228 | + # When initially called, the arguments are bound, and you get a continuation; |
| 229 | + # then resuming that continuation starts the actual computation. |
| 230 | + tree.body.insert(0, ast.Expr(value=ast.Name(id=names_of_myield[0]))) |
| 231 | + |
| 232 | + # Transform multishot yields (`myield`) into `call_cc`. |
| 233 | + tree.body = MultishotYieldTransformer().visit(tree.body) |
| 234 | + |
| 235 | + return tree |
| 236 | + |
| 237 | +from __self__ import macros, multishot, myield # noqa: F811, F401 |
| 238 | + |
30 | 239 |
|
31 | 240 | def runtests():
|
32 | 241 | with testset("a basic generator"):
|
@@ -178,7 +387,7 @@ def result(loop, i=0):
|
178 | 387 | x = g2() # noqa: F821
|
179 | 388 | test[out == list(range(10))]
|
180 | 389 |
|
181 |
| - with testset("multi-shot generators"): |
| 390 | + with testset("multi-shot generators with call_cc[]"): |
182 | 391 | with continuations:
|
183 | 392 | with let_syntax:
|
184 | 393 | with block[value] as my_yield: # noqa: F821
|
@@ -242,6 +451,36 @@ def my_yieldf(value=None, *, cc):
|
242 | 451 | # outside any make_generator are caught at compile time. The actual template the
|
243 | 452 | # make_generator macro needs to splice in is already here in the final example.)
|
244 | 453 |
|
| 454 | + with testset("multi-shot generators with the pattern call_cc[get_cc()]"): |
| 455 | + with continuations: |
| 456 | + @multishot |
| 457 | + def g(): |
| 458 | + myield[1] |
| 459 | + myield[2] |
| 460 | + myield[3] |
| 461 | + |
| 462 | + try: |
| 463 | + out = [] |
| 464 | + k = g() # instantiate the multishot generator |
| 465 | + while True: |
| 466 | + k, x = k() |
| 467 | + out.append(x) |
| 468 | + except StopIteration: |
| 469 | + pass |
| 470 | + test[out == [1, 2, 3]] |
| 471 | + |
| 472 | + k0 = g() # instantiate the multishot generator |
| 473 | + k1, x1 = k0() |
| 474 | + k2, x2 = k1() |
| 475 | + k3, x3 = k2() |
| 476 | + k, x = k1() # multi-shot generator can resume from an earlier point |
| 477 | + test[x1 == 1] |
| 478 | + test[x2 == x == 2] |
| 479 | + test[x3 == 3] |
| 480 | + test[k.func.__qualname__ == k2.func.__qualname__] # same bookmarked position... |
| 481 | + test[k.func is not k2.func] # ...but different function object instance |
| 482 | + test_raises[StopIteration, k3()] |
| 483 | + |
245 | 484 | if __name__ == '__main__': # pragma: no cover
|
246 | 485 | with session(__file__):
|
247 | 486 | runtests()
|
0 commit comments