Skip to content

Commit 5cf04a1

Browse files
committed
add tail call optimization for generators
1 parent 860f5d8 commit 5cf04a1

File tree

2 files changed

+93
-0
lines changed

2 files changed

+93
-0
lines changed

unpythonic/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from .ec import *
1717
from .fun import *
1818
from .fup import *
19+
from .gtco import *
1920
from .it import *
2021
from .let import * # no guarantees on evaluation order (before Python 3.6), nice syntax
2122

unpythonic/gtco.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
"""Tail call optimization for generators."""
4+
5+
from functools import wraps
6+
from inspect import isgenerator
7+
8+
from unpythonic.dynscope import dyn
9+
10+
def gtco(generator):
11+
"""Low-level function: run a generator with TCO enabled.
12+
13+
In the generator, use ``return`` to tail-chain to the next generator.
14+
15+
Example::
16+
17+
def gen():
18+
yield 1
19+
yield 2
20+
return gen() # tail-chain to gen itself
21+
assert tuple(take(6, gtco(gen()))) == (1, 2, 1, 2, 1, 2)
22+
last(take(10000, gtco(gen()))) # no crash
23+
"""
24+
with dyn.let(_gtrampoline_active=True):
25+
while True: # trampoline
26+
x = yield from generator # yield stuff, get final result (return ...)
27+
if not isgenerator(x) and not isinstance(x, _TrampolinedGenerator):
28+
break
29+
generator = x
30+
31+
def gtrampolined(gfunc):
32+
"""Decorator for generator functions (i.e. definitions of generators).
33+
34+
Decorating the definition avoids the need to use ``gtco`` at call time.
35+
36+
Example::
37+
38+
@gtrampolined
39+
def ones():
40+
yield 1
41+
return ones()
42+
assert tuple(take(10, ones())) == (1,) * 10
43+
last(take(10000, ones())) # no crash
44+
"""
45+
@wraps(gfunc)
46+
def decorated(*args, **kwargs):
47+
generator = gfunc(*args, **kwargs)
48+
if "_gtrampoline_active" not in dyn: # start up the trampoline
49+
return _TrampolinedGenerator(generator)
50+
else: # avoid stacking when already running in the trampoline
51+
# and a generator calls a gtrampolined gfunc (incl. its own!)
52+
return generator
53+
return decorated
54+
55+
class _TrampolinedGenerator:
56+
"""Wrapper to inject the gtco() call to the generator g returned by gfunc."""
57+
def __init__(self, g):
58+
self.g = g
59+
def __iter__(self):
60+
return gtco(iter(self.g))
61+
# no __next__, because __iter__ redirects;
62+
# this wrapper is never actually iterated over.
63+
64+
def test():
65+
from unpythonic.it import last, take
66+
67+
# basic usage:
68+
def gen():
69+
yield 1
70+
yield 2
71+
return gen() # tail-chain to gen itself
72+
assert tuple(take(6, gtco(gen()))) == (1, 2, 1, 2, 1, 2)
73+
last(take(10000, gtco(gen()))) # no crash
74+
75+
def ones():
76+
yield 1
77+
return ones()
78+
assert tuple(take(10, gtco(ones()))) == (1,) * 10
79+
last(take(10000, gtco(ones()))) # no crash
80+
81+
# using decorator:
82+
@gtrampolined
83+
def ones2():
84+
yield 1
85+
return ones2()
86+
assert tuple(take(10, ones2())) == (1,) * 10
87+
last(take(10000, ones2())) # no crash
88+
89+
print("All tests PASSED")
90+
91+
if __name__ == '__main__':
92+
test()

0 commit comments

Comments
 (0)