Skip to content

Commit 530d2d1

Browse files
committed
print warning for unclaimed TCO jump instances (when they get GC'd)
1 parent 3797fc3 commit 530d2d1

File tree

4 files changed

+88
-1
lines changed

4 files changed

+88
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,10 @@ Here `jump` is **a noun, not a verb**. The `jump(f, ...)` part just evaluates to
248248

249249
The final result is just returned normally. Returning a normal value (anything that is not a ``jump`` instance) to a trampoline shuts down that trampoline, and returns the given value from the initial call (to a ``@trampolined`` function) that originally started that trampoline.
250250

251+
Trying to ``jump(...)`` without the ``return`` will **usually** print a warning. This is implemented by checking a flag in the ``__del__`` method; any correctly used jump instance should have been claimed by a trampoline at some point in its lifetime.
252+
253+
If you prefer the syntax without the explicit ``return``, see [`tco_exc.py`](unpythonic/tco_exc.py). **Since this is pre-1.0**, the default implementation (and the import in [`fploop.py`](unpythonic/fploop.py)) is still subject to change.
254+
251255
*Tail recursion in a lambda*:
252256

253257
```python

unpythonic/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@
3434
from .misc import *
3535
from .tco import *
3636

37-
__version__ = '0.4.2'
37+
__version__ = '0.4.3'
3838

unpythonic/tco.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ def baz():
129129
__all__ = ["SELF", "jump", "trampolined"]
130130

131131
from functools import wraps
132+
from sys import stderr
132133

133134
from unpythonic.misc import call
134135

@@ -170,6 +171,66 @@ def __init__(self, target, args, kwargs):
170171
self.target = target._entrypoint if hasattr(target, "_entrypoint") else target
171172
self.args = args
172173
self.kwargs = kwargs
174+
self._claimed = False # set when the instance is caught by a trampoline
175+
176+
def __repr__(self):
177+
return "<_jump at 0x{:x}: target={}, args={}, kwargs={}".format(id(self),
178+
self.target,
179+
self.args,
180+
self.kwargs)
181+
182+
def __del__(self):
183+
"""Warn about bugs in client code.
184+
185+
Since it's ``__del__``, we can't raise any exceptions - which includes
186+
things such as ``AssertionError`` and ``SystemExit``. So we print a
187+
warning.
188+
189+
**CAUTION**:
190+
191+
This warning mechanism should help find bugs, but it is not 100% foolproof.
192+
Since ``__del__`` is managed by Python's GC, some object instances may
193+
not get their ``__del__`` called when the Python interpreter itself exits
194+
(if those instances are still alive at that time).
195+
196+
**Typical causes**:
197+
198+
*Missing "return"*::
199+
200+
@trampolined
201+
def foo():
202+
jump(bar, 42)
203+
204+
The jump instance was never actually passed to the trampoline; it was
205+
just created and discarded. The trampoline got the ``None`` from the
206+
implicit ``return None`` at the end of the function.
207+
208+
(See ``tco_exc.py`` if you prefer this syntax, without a ``return``.)
209+
210+
*No trampoline*::
211+
212+
def foo():
213+
return jump(bar, 42)
214+
215+
Here ``foo`` is not trampolined.
216+
217+
We **have** a trampoline when the function that returns the jump
218+
instance is itself ``@trampolined``, or is running in a trampoline
219+
implicitly (due to having been entered via a tail call).
220+
221+
*Trampoline at the wrong level*::
222+
223+
@trampolined
224+
def foo():
225+
def bar():
226+
return jump(qux, 23)
227+
bar() # normal call, no TCO
228+
229+
Here ``bar`` has no trampoline; only ``foo`` does. **Only** a ``@trampolined``
230+
function, or a function entered via a tail call, may return a jump.
231+
"""
232+
if not self._claimed:
233+
print("WARNING: unclaimed {}".format(repr(self)), file=stderr)
173234

174235
# We want @wraps to preserve docstrings, so the decorator must be a function, not a class.
175236
# https://stackoverflow.com/questions/6394511/python-functools-wraps-equivalent-for-classes
@@ -191,6 +252,7 @@ def decorated(*args, **kwargs):
191252
f = v.target
192253
args = v.args
193254
kwargs = v.kwargs
255+
v._claimed = True
194256
else: # final result, exit trampoline
195257
return v
196258
# fortunately functions in Python are just objects; stash for jump constructor
@@ -258,6 +320,18 @@ def baz():
258320

259321
print("All tests PASSED")
260322

323+
print("*** These two error cases SHOULD PRINT A WARNING:", file=stderr)
324+
print("** No surrounding trampoline:", file=stderr)
325+
def bar2():
326+
pass
327+
def foo2():
328+
return jump(bar2)
329+
foo2()
330+
print("** Missing 'return' in 'return jump':", file=stderr)
331+
def foo3():
332+
jump(bar2)
333+
foo3()
334+
261335
# loop performance?
262336
n = 100000
263337
import time

unpythonic/tco_exc.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,19 @@ def __init__(self, target, args, kwargs):
6161
self.tkwargs = kwargs
6262

6363
# Error message when uncaught
64+
# (This can still be caught by the wrong trampoline, if an inner trampoline
65+
# has been forgotten when nesting TCO chains - there's nothing we can do
66+
# about that.)
6467
self.args = ("No trampoline, attempted to jump to '{}', args {}, kwargs {}".format(target,
6568
args,
6669
kwargs),)
6770

71+
def __repr__(self):
72+
return "<TrampolinedJump at 0x{:x}: target={}, args={}, kwargs={}".format(id(self),
73+
self.target,
74+
self.targs,
75+
self.tkwargs)
76+
6877
def trampolined(function):
6978
"""Decorator to make a function trampolined.
7079

0 commit comments

Comments
 (0)