-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathtailtools.py
More file actions
1646 lines (1381 loc) · 77 KB
/
tailtools.py
File metadata and controls
1646 lines (1381 loc) · 77 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
"""Automatic TCO, continuations, implicit return statements.
The common factor is tail-position analysis."""
__all__ = ["autoreturn",
"tco",
"continuations", "call_cc", "get_cc", "iscontinuation"]
from functools import partial
from ast import (Lambda, FunctionDef, AsyncFunctionDef, ClassDef,
arguments, arg, keyword,
List, Tuple,
Call, Name, Starred, Constant,
BoolOp, And, Or,
With, AsyncWith, If, IfExp, Try, Match, Assign, Return, Expr,
Await,
copy_location)
from mcpyrate.quotes import macros, q, u, n, a, h # noqa: F401
from mcpyrate import gensym
from mcpyrate.astcompat import TryStar
from mcpyrate.quotes import capture_as_macro, is_captured_value
from mcpyrate.utils import NestingLevelTracker
from mcpyrate.walkers import ASTTransformer, ASTVisitor
from .ifexprs import aif, it
from .letdoutil import isdo, islet, ExpandedLetView, ExpandedDoView
from .util import (isx, isec,
detect_callec, detect_lambda,
has_tco, sort_lambda_decorators,
suggest_decorator_index,
UnpythonicASTMarker, ExpandedContinuationsMarker)
from ..dynassign import dyn
from ..fun import identity
from ..funutil import Values
from ..it import uniqify
from ..tco import trampolined, jump
# In `continuations`, we use `aif` and `it` as hygienically captured macros.
# Note the difference between `aif[..., it, ...]` and `q[a[_our_aif][..., a[_our_it], ...]]`.
#
# If `it` is bound in the current expander, even *mentioning* it outside an `aif` is a syntax error, by design.
#
# When constructing a quasiquoted tree that invokes `aif[]`, we can splice in a hygienic reference to `it`
# as `a[_our_it]` without even having the macro bound in the expander that expands *this* module.
_our_aif = capture_as_macro(aif)
_our_it = capture_as_macro(it)
# --------------------------------------------------------------------------------
# Macro interface
def autoreturn(tree, *, syntax, **kw):
"""[syntax, block] Implicit "return" in tail position, like in Lisps.
Each ``def`` function definition lexically within the ``with autoreturn``
block is examined, and if the last item within the body is an expression
``expr``, it is transformed into ``return expr``.
If the last item is an if/elif/else block, the transformation is applied
to the last item in each of its branches.
If the last item is a ``with`` or ``async with`` block, the transformation
is applied to the last item in its body.
If the last item is a try/except/else/finally block, the rules are as follows.
If an ``else`` clause is present, the transformation is applied to the last
item in it; otherwise, to the last item in the ``try`` clause. Additionally,
in both cases, the transformation is applied to the last item in each of the
``except`` clauses. The ``finally`` clause is not transformed; the intention
is it is usually a finalizer (e.g. to release resources) that runs after the
interesting value is already being returned by ``try``, ``else`` or ``except``.
Example::
with autoreturn:
def f():
"I'll just return this"
assert f() == "I'll just return this"
def g(x):
if x == 1:
"one"
elif x == 2:
"two"
else:
"something else"
assert g(1) == "one"
assert g(2) == "two"
assert g(42) == "something else"
**CAUTION**: If the final ``else`` is omitted, as often in Python, then
only the ``else`` item is in tail position with respect to the function
definition - likely not what you want.
So with ``autoreturn``, the final ``else`` should be written out explicitly,
to make the ``else`` branch part of the same if/elif/else block.
**CAUTION**: ``for``, ``async for``, ``while`` are currently not analyzed;
effectively, these are defined as always returning ``None``. If the last item
in your function body is a loop, use an explicit return.
**CAUTION**: With ``autoreturn`` enabled, functions no longer return ``None``
by default; the whole point of this macro is to change the default return
value.
The default return value is ``None`` only if the tail position contains
a statement (because in a sense, a statement always returns ``None``).
"""
if syntax != "block":
raise SyntaxError("autoreturn is a block macro only") # pragma: no cover
if syntax == "block" and kw['optional_vars'] is not None:
raise SyntaxError("autoreturn does not take an as-part") # pragma: no cover
# Expand outside in. Any nested macros should get clean standard Python,
# not having to worry about implicit "return" statements.
return _autoreturn(block_body=tree)
def tco(tree, *, syntax, expander, **kw):
"""[syntax, block] Implicit tail-call optimization (TCO).
Examples::
with tco:
evenp = lambda x: (x == 0) or oddp(x - 1)
oddp = lambda x: (x != 0) and evenp(x - 1)
assert evenp(10000) is True
with tco:
def evenp(x):
if x == 0:
return True
return oddp(x - 1)
def oddp(x):
if x != 0:
return evenp(x - 1)
return False
assert evenp(10000) is True
This is based on a strategy similar to MacroPy's tco macro, but using
the TCO machinery from ``unpythonic.tco``.
This recursively handles also builtins ``a if p else b``, ``and``, ``or``;
and from ``unpythonic.syntax``, ``do[]``, ``let[]``, ``letseq[]``, ``letrec[]``,
when used in computing a return value. (``aif[]`` and ``cond[]`` also work.)
Note only calls **in tail position** will be TCO'd. Any other calls
are left as-is. Tail positions are:
- The whole return value, if it is just a single call.
- Both ``a`` and ``b`` branches of ``a if p else b`` (but not ``p``).
- The last item in an ``and``/``or``. If these are nested, only the
last item in the whole expression involving ``and``/``or``. E.g. in::
(a and b) or c
a and (b or c)
in either case, only ``c`` is in tail position, regardless of the
values of ``a``, ``b``.
- The last item in a ``do[]``.
- In a ``do0[]``, this is the implicit item that just returns the
stored return value.
- The argument of a call to an escape continuation. The ``ec(...)`` call
itself does not need to be in tail position; escaping early is the
whole point of an ec.
All function definitions (``def`` and ``lambda``) lexically inside the block
undergo TCO transformation. The functions are automatically ``@trampolined``,
and any tail calls in their return values are converted to ``jump(...)``
for the TCO machinery.
Note in a ``def`` you still need the ``return``; it marks a return value.
But see ``autoreturn``::
with autoreturn, tco:
def evenp(x):
if x == 0:
True
else:
oddp(x - 1)
def oddp(x):
if x != 0:
evenp(x - 1)
else:
False
assert evenp(10000) is True
**CAUTION**: regarding escape continuations, only basic uses of ecs created
via ``call_ec`` are currently detected as being in tail position. Any other
custom escape mechanisms are not supported. (This is mainly of interest for
lambdas, which have no ``return``, and for "multi-return" from a nested
function.)
*Basic use* is defined as either of these two cases::
# use as decorator
@call_ec
def result(ec):
...
# use directly on a literal lambda (effectively, as a decorator)
result = call_ec(lambda ec: ...)
When macro expansion of the ``with tco`` block starts, names of escape
continuations created **anywhere lexically within** the ``with tco`` block
are captured. Lexically within the block, any call to a function having
any of the captured names, or as a fallback, one of the literal names
``ec``, ``brk``, ``throw`` is interpreted as invoking an escape
continuation.
"""
if syntax != "block":
raise SyntaxError("tco is a block macro only") # pragma: no cover
if syntax == "block" and kw['optional_vars'] is not None:
raise SyntaxError("tco does not take an as-part") # pragma: no cover
# Two-pass macro.
with dyn.let(_macro_expander=expander):
return _tco(block_body=tree)
def continuations(tree, *, syntax, expander, **kw):
"""[syntax, block] call/cc for Python.
This allows saving the control state and then jumping back later
(in principle, any time later). Some possible use cases:
- Tree traversal (possibly a cartesian product of multiple trees, with the
current position in each tracked automatically).
- McCarthy's amb operator.
- Generators. (Python already has those, so only for teaching.)
This is a very loose pythonification of Paul Graham's continuation-passing
macros, which implement continuations by chaining closures and passing the
continuation semi-implicitly. For details, see chapter 20 in On Lisp:
http://paulgraham.com/onlisp.html
Continuations are most readily implemented when the program is written in
continuation-passing style (CPS), but that is unreadable for humans.
The purpose of this macro is to partly automate the CPS transformation, so
that at the use site, we can write CPS code in a much more readable fashion.
A ``with continuations`` block implies TCO; the same rules apply as in a
``with tco`` block. Furthermore, ``with continuations`` introduces the
following additional rules:
- Functions which make use of continuations, or call other functions that do,
must be defined within a ``with continuations`` block, using the usual
``def`` or ``lambda`` forms.
- All function definitions in a ``with continuations`` block, including
any nested definitions, have an implicit formal parameter ``cc``,
**even if not explicitly declared** in the formal parameter list.
If declared explicitly, ``cc`` must be in a position that can accept a
default value.
This means ``cc`` must be declared either as by-name-only::
with continuations:
def myfunc(a, b, *, cc):
...
f = lambda *, cc: ...
or as the last parameter that has no default::
with continuations:
def myfunc(a, b, cc):
...
f = lambda cc: ...
Then the continuation machinery will automatically set the default value
of ``cc`` to the default continuation (``identity``), which just returns
its arguments.
The most common use case for explicitly declaring ``cc`` is that the
function is the target of a ``call_cc[]``; then it helps readability
to make the ``cc`` parameter explicit.
- A ``with continuations`` block will automatically transform all
function definitions and ``return`` statements lexically contained
within the block to use the continuation machinery.
- ``return somevalue`` actually means a tail-call to ``cc`` with the
given ``somevalue``.
Multiple values can be returned as a ``Values``. Multiple-valueness
is tested at run time.
Any ``Values`` return value is automatically unpacked to the args
and kwargs of ``cc``.
- An explicit ``return somefunc(arg0, ..., k0=v0, ...)`` actually means
a tail-call to ``somefunc``, with its ``cc`` automatically set to our
``cc``. Hence this inserts a call to ``somefunc`` before proceeding
with our current continuation. (This is most often what we want when
making a tail-call from a continuation-enabled function.)
Here ``somefunc`` **must** be a continuation-enabled function;
otherwise the TCO chain will break and the result is immediately
returned to the top-level caller.
(If the call succeeds at all; the ``cc`` argument is implicitly
filled in and passed by name. Regular functions usually do not
accept a named parameter ``cc``, let alone know what to do with it.)
- Just like in ``with tco``, a lambda body is analyzed as one big
return-value expression. This uses the exact same analyzer; for example,
``do[]`` (including any implicit ``do[]``) and the ``let[]`` expression
family are supported.
- Calls from functions defined in one ``with continuations`` block to those
defined in another are ok; there is no state or context associated with
the block.
- Much of the language works as usual.
Any non-tail calls can be made normally. Regular functions can be called
normally in any non-tail position.
Continuation-enabled functions behave as regular functions when
called normally; only tail calls implicitly set ``cc``. A normal call
uses ``identity`` as the default ``cc``.
- For technical reasons, the ``return`` statement is not allowed at the
top level of the ``with continuations:`` block. (Because a continuation
is essentially a function, ``return`` would behave differently based on
whether it is placed lexically before or after a ``call_cc[]``.)
If you absolutely need to terminate the function surrounding the
``with continuations:`` block from inside the block, use an exception
to escape; see ``call_ec``, ``catch``, ``throw``.
**Capturing the continuation**:
Inside a ``with continuations:`` block, the ``call_cc[]`` statement
captures a continuation. (It is actually a macro, for technical reasons.)
Capturing a continuation introduces a scope boundary. The continuation
captured by `call_cc` (i.e. the rest of the function body after the
`call_cc` statement) is a new scope, and the assignment part of the
`call_cc` statement takes effect in that new scope. Under the hood,
the assignment from the `call_cc` is implemented as function parameters;
the continuation is a function.
For various possible program topologies that continuations may introduce, see
the clarifying pictures under ``doc/`` in the source distribution.
Syntax::
x = call_cc[func(...)]
*xs = call_cc[func(...)]
x0, ... = call_cc[func(...)]
x0, ..., *xs = call_cc[func(...)]
call_cc[func(...)]
Conditional variant::
x = call_cc[f(...) if p else g(...)]
*xs = call_cc[f(...) if p else g(...)]
x0, ... = call_cc[f(...) if p else g(...)]
x0, ..., *xs = call_cc[f(...) if p else g(...)]
call_cc[f(...) if p else g(...)]
Assignment targets:
- To destructure positional multiple-values (from a `Values` return value),
use a tuple assignment target (comma-separated names, as usual).
Destructuring *named* return values from a `call_cc` is currently not supported.
- The last assignment target may be starred. It is transformed into
the vararg (a.k.a. ``*args``) of the continuation function.
(It will capture a whole tuple, or any excess items, as usual.)
- To ignore the return value (useful if ``func`` was called only to
perform its side-effects), just omit the assignment part.
Conditional variant:
- ``p`` is any expression. If truthy, ``f(...)`` is called, and if falsey,
``g(...)`` is called.
- Each of ``f(...)``, ``g(...)`` may be ``None``. A ``None`` skips the
function call, proceeding directly to the continuation. Upon skipping,
all assignment targets (if any are present) are set to ``None``.
The starred assignment target (if present) gets the empty tuple.
- The main use case of the conditional variant is for things like::
with continuations:
k = None
def setk(cc):
global k
k = cc
def dostuff(x):
call_cc[setk() if x > 10 else None] # capture only if x > 10
...
To keep things relatively straightforward, a ``call_cc[]`` is only
allowed to appear **at the top level** of:
- the ``with continuations:`` block itself
- a ``def`` or ``async def``
Nested defs are ok; here *top level* only means the top level of the
*currently innermost* ``def``.
If you need to place ``call_cc[]`` inside a loop, use ``@looped`` et al.
from ``unpythonic.fploop``; this has the loop body represented as the
top level of a ``def``.
Multiple ``call_cc[]`` statements in the same function body are allowed.
These essentially create nested closures.
**Main differences to Scheme and Racket**:
Compared to Scheme/Racket, where ``call/cc`` will capture also expressions
occurring further up in the call stack, our ``call_cc`` may be need to be
placed differently (further out, depending on what needs to be captured)
due to the delimited nature of the continuations implemented here.
Scheme and Racket implicitly capture the continuation at every position,
whereas we do it explicitly, only at the use sites of the ``call_cc`` macro.
Also, since there are limitations to where a ``call_cc[]`` may appear, some
code may need to be structured differently to do some particular thing, if
porting code examples originally written in Scheme or Racket.
Unlike ``call/cc`` in Scheme/Racket, ``call_cc`` takes **a function call**
as its argument, not just a function reference. Also, there's no need for
it to be a one-argument function; any other args can be passed in the call.
The ``cc`` argument is filled implicitly and passed by name; any others are
passed exactly as written in the client code.
**Technical notes**:
The ``call_cc[]`` statement essentially splits its use site into *before*
and *after* parts, where the *after* part (the continuation) can be run
a second and further times, by later calling the callable that represents
the continuation. This makes a computation resumable from a desired point.
The return value of the continuation is whatever the original function
returns, for any ``return`` statement that appears lexically after the
``call_cc[]``.
The effect of ``call_cc[]`` is that the function call ``func(...)`` in
the brackets is performed, with its ``cc`` argument set to the lexically
remaining statements of the current ``def`` (at the top level, the rest
of the ``with continuations`` block), represented as a callable.
The continuation itself ends there (it is *delimited* in this particular
sense), but it will chain to the ``cc`` of the function it appears in.
This is termed the *parent continuation* (**pcc**), stored in the internal
variable ``_pcc`` (which defaults to ``None``).
Via the use of the pcc, here ``f`` will maintain the illusion of being
just one function, even though a ``call_cc`` appears there::
def f(*, cc):
...
call_cc[g(1, 2, 3)]
...
The continuation is a closure. For its pcc, it will use the value the
original function's ``cc`` had when the definition of the continuation
was executed (for that particular instance of the closure). Hence, calling
the original function again with its ``cc`` set to something else will
produce a new continuation instance that chains into that new ``cc``.
The continuation's own ``cc`` will be ``identity``, to allow its use just
like any other function (also as argument of a ``call_cc`` or target of a
tail call).
When the pcc is set (not ``None``), the effect is to run the pcc first,
and ``cc`` only after that. This preserves the whole captured tail of a
computation also in the presence of nested ``call_cc`` invocations (in the
above example, this would occur if also ``g`` used ``call_cc``).
Continuations are not accessible by name (their definitions are named by
gensym). To get a reference to a continuation instance, stash the value
of the ``cc`` argument somewhere while inside the ``call_cc``.
The function ``func`` called by a ``call_cc[func(...)]`` is (almost) the
only place where the ``cc`` argument is actually set. There it is the
captured continuation. Roughly everywhere else, ``cc`` is just ``identity``.
Tail calls are an exception to this rule; a tail call passes along the current
value of ``cc``, unless overridden manually (by setting the ``cc=...`` kwarg
in the tail call).
When the pcc is set (not ``None``) at the site of the tail call, the
machinery will create a composed continuation that runs the pcc first,
and ``cc`` (whether current or manually overridden) after that. This
composed continuation is then passed to the tail call as its ``cc``.
**Tips**:
- Once you have a captured continuation, one way to use it is to set
``cc=...`` manually in a tail call, as was mentioned. Example::
def main():
call_cc[myfunc()] # call myfunc, capturing the current cont...
... # ...which is the rest of "main"
def myfunc(cc):
ourcc = cc # save the captured continuation (sent by call_cc[])
def somefunc():
return dostuff(..., cc=ourcc) # and use it here
somestack.append(somefunc)
In this example, when ``somefunc`` is eventually called, it will tail-call
``dostuff`` and then proceed with the continuation ``myfunc`` had
at the time when that instance of the ``somefunc`` closure was created.
(This pattern is essentially how to build the ``amb`` operator.)
- Instead of setting ``cc``, you can also overwrite ``cc`` with a captured
continuation inside a function body. That overrides the continuation
for the rest of the dynamic extent of the function, not only for a
particular tail call::
def myfunc(cc):
ourcc = cc
def somefunc():
cc = ourcc
return dostuff(...)
somestack.append(somefunc)
- A captured continuation can also be called manually; it's just a callable.
The assignment targets, at the ``call_cc[]`` use site that spawned this
particular continuation, specify its call signature. All args are
positional, except the implicit ``cc``, which is by-name-only.
- Just like in Scheme/Racket's ``call/cc``, the values that get bound
to the ``call_cc[]`` assignment targets on second and further calls
(when the continuation runs) are the arguments given to the continuation
when it is called (whether implicitly or manually).
- Setting ``cc`` to ``unpythonic.fun.identity``, while inside a ``call_cc``,
will short-circuit the rest of the computation. In such a case, the
continuation will not be invoked automatically. A useful pattern for
suspend/resume.
- However, it is currently not possible to prevent the rest of the tail
of a captured continuation (the pcc) from running, apart from manually
setting ``_pcc`` to ``None`` before executing a ``return``. Note that
doing that is not strictly speaking supported (and may be subject to
change in a future version).
- When ``call_cc[]`` appears inside a function definition:
- It tail-calls ``func``, with its ``cc`` set to the captured
continuation.
- The return value of the function containing one or more ``call_cc[]``
statements is the return value of the continuation.
- When ``call_cc[]`` appears at the top level of ``with continuations``:
- A normal call to ``func`` is made, with its ``cc`` set to the captured
continuation.
- In this case, if the continuation is called later, it always
returns ``None``, because the use site of ``call_cc[]`` is not
inside a function definition.
- If you need to insert just a tail call (no further statements) before
proceeding with the current continuation, no need for ``call_cc[]``;
use ``return func(...)`` instead.
The purpose of ``call_cc[func(...)]`` is to capture the current
continuation (the remaining statements), and hand it to ``func``
as a first-class value.
- To combo with ``multilambda``, use this ordering::
with multilambda, continuations:
...
- Some very limited comboability with ``call_ec``. May be better to plan
ahead, using ``call_cc[]`` at the appropriate outer level, and then
short-circuit (when needed) by setting ``cc`` to ``identity``.
This avoids the need to have both ``call_cc`` and ``call_ec`` at the
same time.
- ``unpythonic.ec.call_ec`` can be used normally **lexically before any**
``call_cc[]``, but (in a given function) after at least one ``call_cc[]``
has run, the ``ec`` ceases to be valid. This is because our ``call_cc[]``
actually splits the function into *before* and *after* parts, and
**tail-calls** the *after* part.
(Wrapping the ``def`` in another ``def``, and placing the ``call_ec``
on the outer ``def``, does not help either, because even the outer
function has exited by the time *the continuation* is later called
the second and further times.)
Usage of ``call_ec`` while inside a ``with continuations`` block is::
with continuations:
@call_ec
def result(ec):
print("hi")
ec(42)
print("not reached")
assert result == 42
result = call_ec(lambda ec: do[print("hi"),
ec(42),
print("not reached")])
Note the signature of ``result``. Essentially, ``ec`` is a function
that raises an exception (to escape to a dynamically outer context),
whereas the implicit ``cc`` is the closure-based continuation handled
by the continuation machinery.
See the ``tco`` macro for details on the ``call_ec`` combo.
"""
if syntax != "block":
raise SyntaxError("continuations is a block macro only") # pragma: no cover
if syntax == "block" and kw['optional_vars'] is not None:
raise SyntaxError("continuations does not take an as-part") # pragma: no cover
# Two-pass macro.
with dyn.let(_macro_expander=expander):
return _continuations(block_body=tree)
def call_cc(tree, **kw):
"""[syntax] Only meaningful in a "with continuations" block.
Syntax cheat sheet::
x = call_cc[func(...)]
*xs = call_cc[func(...)]
x0, ... = call_cc[func(...)]
x0, ..., *xs = call_cc[func(...)]
call_cc[func(...)]
Conditional variant::
x = call_cc[f(...) if p else g(...)]
*xs = call_cc[f(...) if p else g(...)]
x0, ... = call_cc[f(...) if p else g(...)]
x0, ..., *xs = call_cc[f(...) if p else g(...)]
call_cc[f(...) if p else g(...)]
where ``f()`` or ``g()`` may be ``None`` instead of a function call.
For more, see the docstring of ``continuations``.
"""
if _continuations_level.value < 1:
raise SyntaxError("call_cc[] is only meaningful in a `with continuations` block.") # pragma: no cover, not meant to hit the expander (expanded away by `with continuations`)
return CallCcMarker(body=tree)
# --------------------------------------------------------------------------------
# Syntax transformers
# Implicit return statement. This performs a tail-position analysis of function bodies.
def _autoreturn(block_body):
class AutoreturnTransformer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
if type(tree) in (FunctionDef, AsyncFunctionDef):
newtail = TailStatementTransformer().visit(tree.body[-1])
if isinstance(newtail, list): # replaced by more than one statement?
tree.body = tree.body[:-1] + newtail
else:
tree.body[-1] = newtail
return self.generic_visit(tree)
class TailStatementTransformer(ASTTransformer):
def transform(self, tree):
# TODO: For/AsyncFor/While?
if type(tree) is If:
tree.body[-1] = self.visit(tree.body[-1])
if tree.orelse:
tree.orelse[-1] = self.visit(tree.orelse[-1])
elif type(tree) in (With, AsyncWith):
tree.body[-1] = self.visit(tree.body[-1])
elif type(tree) in (Try, TryStar): # Python 3.11+: `try`/`except*`
# We don't care about finalbody; typically used for unwinding only.
if tree.orelse: # tail position is in else clause if present
tree.orelse[-1] = self.visit(tree.orelse[-1])
else: # tail position is in the body of the "try"
tree.body[-1] = self.visit(tree.body[-1])
# additionally, tail position is in each "except" handler
for handler in tree.handlers:
handler.body[-1] = self.visit(handler.body[-1])
elif type(tree) is Match: # Python 3.10+: `match`/`case`
for case in tree.cases:
if case.body:
case.body[-1] = self.visit(case.body[-1])
elif type(tree) in (FunctionDef, AsyncFunctionDef, ClassDef): # v0.15.0+
# If the item in tail position is a named function definition
# or a class definition, it binds a name - that of the function/class.
# Return that object.
with q as quoted:
with a:
tree
return n[tree.name]
tree = quoted
elif type(tree) is Expr: # expr -> return expr
with q as quoted:
return a[tree.value]
tree = quoted[0]
return tree
# This macro expands outside-in. Any nested macros should get clean standard Python,
# not having to worry about implicit "return" statements.
return AutoreturnTransformer().visit(block_body)
# Automatic TCO. This is the same framework as in "continuations", in its simplest form.
def _tco(block_body):
# first pass, outside-in
userlambdas = detect_lambda(block_body)
known_ecs = list(uniqify(detect_callec(block_body)))
block_body = dyn._macro_expander.visit_recursively(block_body)
# second pass, inside-out
transform_retexpr = partial(_transform_retexpr)
new_block_body = []
for stmt in block_body:
# skip nested, already expanded "with continuations" blocks
# (needed to support continuations in the Lispython dialect, which applies tco globally)
if isinstance(stmt, ExpandedContinuationsMarker):
new_block_body.append(stmt)
continue
stmt = _tco_transform_return(stmt, known_ecs=known_ecs,
transform_retexpr=transform_retexpr)
stmt = _tco_transform_def(stmt, preproc_cb=None)
stmt = _tco_transform_lambda(stmt, preproc_cb=None,
userlambdas=userlambdas,
known_ecs=known_ecs,
transform_retexpr=transform_retexpr)
stmt = sort_lambda_decorators(stmt)
new_block_body.append(stmt)
return new_block_body
# -----------------------------------------------------------------------------
# True multi-shot continuations for Python, based on a CPS transformation.
# _pcc/cc chaining handler, to be exported to client code via q[h[]].
#
# We handle multiple-return-values like the rest of unpythonic does:
# returning a `Values` means returning multiple values. Unpack them
# to cc's args/kwargs.
#
def chain_conts(cc1, cc2, with_star=False): # cc1=_pcc, cc2=cc
"""Internal function, used in code generated by the continuations macro."""
if with_star: # to be chainable from a tail call, accept a multiple-values arglist
if cc1 is not None:
def cc(*rets, **kwrets):
return jump(cc1, cc=cc2, *rets, **kwrets)
else:
# Beside a small optimization, it is important to preserve
# "identity" as "identity", so that the call_cc logic that
# defines the continuation functions will detect it and
# know when to set _pcc (and importantly, when not to).
cc = cc2
else: # for inert data value returns (this produces the multiple-values arglist)
if cc1 is not None:
def cc(return_value):
if isinstance(return_value, Values):
return jump(cc1, cc=cc2, *return_value.rets, **return_value.kwrets)
else:
return jump(cc1, return_value, cc=cc2)
else:
def cc(return_value):
if isinstance(return_value, Values):
return jump(cc2, *return_value.rets, **return_value.kwrets)
else:
return jump(cc2, return_value)
return cc
_continuations_level = NestingLevelTracker() # for checking validity of call_cc[]
class ContinuationsMarker(UnpythonicASTMarker):
"""AST marker related to the unpythonic's continuations (call_cc) subsystem."""
class CallCcMarker(ContinuationsMarker):
"""AST marker denoting a `call_cc[]` invocation."""
def _continuations(block_body): # here be dragons.
# This is a very loose pythonification of Paul Graham's continuation-passing
# macros in On Lisp, chapter 20.
#
# We don't have an analog of PG's "=apply", since Python doesn't need "apply"
# to pass in varargs.
# first pass, outside-in
userlambdas = detect_lambda(block_body)
known_ecs = list(uniqify(detect_callec(block_body)))
with _continuations_level.changed_by(+1):
block_body = dyn._macro_expander.visit_recursively(block_body)
# second pass, inside-out
# _tco_transform_def and _tco_transform_lambda correspond to PG's
# "=defun" and "=lambda", but we don't need to generate a macro.
#
# Here we define only the callback to perform the additional transformations
# we need for the continuation machinery.
def transform_args(tree):
assert type(tree) in (FunctionDef, AsyncFunctionDef, Lambda)
# Add a cc kwarg if the function has no cc arg.
posnames = [arg.arg for arg in tree.args.args] # positional-or-keyword
kwonlynames = [kw.arg for kw in tree.args.kwonlyargs]
if "cc" not in posnames + kwonlynames:
tree.args.kwonlyargs = tree.args.kwonlyargs + [arg(arg="cc")]
tree.args.kw_defaults = tree.args.kw_defaults + [None] # not set
kwonlynames.append("cc")
# Patch in the default (if possible), i.e. the identity continuation,
# to allow regular (non-tail) calls without explicitly passing a continuation.
if "cc" in posnames:
j = posnames.index("cc")
na = len(posnames)
nd = len(tree.args.defaults) # defaults apply to n last args
if j == na - nd - 1: # last one that has no default
tree.args.defaults.insert(0, q[h[identity]])
else: # "cc" in kwonlynames:
j = kwonlynames.index("cc")
if tree.args.kw_defaults[j] is None: # not already set
tree.args.kw_defaults[j] = q[h[identity]]
# implicitly add "parent cc" arg for treating the tail of a computation
# as one entity (only actually used in continuation definitions created by
# call_cc; everywhere else, it's None). See doc/callcc_topology.pdf for clarifying pictures.
if "_pcc" not in kwonlynames:
non = q[None]
non = copy_location(non, tree)
tree.args.kwonlyargs = tree.args.kwonlyargs + [arg(arg="_pcc")]
tree.args.kw_defaults = tree.args.kw_defaults + [non] # has the value None **at runtime**
return tree
# _tco_transform_return corresponds to PG's "=values".
# It uses _transform_retexpr to transform return-value expressions
# and arguments of calls to escape continuations.
#
# Ours is applied automatically to all return statements (and calls to
# escape continuations) in the block, and there's some extra complexity
# to support IfExp, BoolOp, and the do and let macros in return-value expressions.
#
# Already performed by the TCO machinery:
# return f(...) --> return jump(f, ...)
#
# Additional transformations needed for `continuations`.
# Function calls, after the TCO transform:
# return jump(f, ...) --> return jump(f, cc=cc, ...) # customize the transform to add the cc kwarg
# Bare data:
# return value --> return jump(cc, value)
# return Values(a0, ..., k0=v0, ...) --> return jump(cc, a0, ..., k0=v0, ...)
#
# Here we only customize the transform_retexpr callback to pass our
# current continuation (if no continuation already specified by user).
def call_cb(tree): # add the cc kwarg (this plugs into the TCO transformation)
# we're a postproc; our input is "jump(some_target_func, *args, **kwargs)"
hascc = any(kw.arg == "cc" for kw in tree.keywords)
if hascc:
# chain our _pcc and the cc=... manually provided by the user
thekw = [kw for kw in tree.keywords if kw.arg == "cc"][0] # exactly one
usercc = thekw.value
thekw.value = q[h[chain_conts](n["_pcc"], a[usercc], with_star=True)]
else:
# chain our _pcc and the current value of cc
tree.keywords = [keyword(arg="cc", value=q[h[chain_conts](n["_pcc"], n["cc"], with_star=True)])] + tree.keywords
return tree
# The `data_cb` handles also `Values`; `_transform_retexpr` detects those and treats them as bare data.
def data_cb(tree): # transform an inert-data return value into a tail-call to cc.
tree = q[h[chain_conts](n["_pcc"], n["cc"])(a[tree])]
return tree
transform_retexpr = partial(_transform_retexpr, call_cb=call_cb, data_cb=data_cb)
# CPS conversion, essentially the call/cc. Corresponds to PG's "=bind".
#
# But we have a code walker, so we don't need to require the body to be
# specified inside the body of the macro invocation like PG's solution does.
# Instead, we capture as the continuation all remaining statements (i.e.
# those that lexically appear after the ``call_cc[]``) in the current block.
def iscallccstatement(tree):
if type(tree) not in (Assign, Expr):
return False
return isinstance(tree.value, CallCcMarker)
# owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
def split_at_callcc(owner, body):
if not body:
return [], None, []
before, after = [], body
while True:
stmt, *after = after
if iscallccstatement(stmt):
# after is always non-empty here (has at least the explicitified "return")
# ...unless we're at the top level of the "with continuations" block
if not after:
raise SyntaxError("call_cc[] cannot appear as the last statement of a 'with continuations' block (no continuation to capture)") # pragma: no cover
# after = patch_scoping(owner, before, stmt, after) # bad idea, DON'T DO THIS
return before, stmt, after
before.append(stmt)
if not after:
return before, None, []
# Try to maintain an illusion of Python's standard scoping rules across the split
# into the parent context (`before`) and continuation closure (`after`).
# See Politz et al 2013 (the "full monty" paper), section 4.2.
#
# TODO: On second thought, this is a bad idea, DON'T DO THIS.
#
# The function `patch_scoping` is an experiment that implements propagation
# of the scope of variable definitions from the parent scope into the continuation,
# recursively. But:
#
# - Due to how the continuation machinery works, the continuation's
# parameters (assignment targets of the `call_cc`) **must** shadow
# the same names from the parent scope, if they happen to exist there.
#
# - There is no propagation from the continuation up the parent scope
# chain. That is, if a continuation declares a new local variable, the
# name won't become available to any of the parent contexts, even if
# those are part of the same original function (to which the
# continuation splitting was applied). Implementing this would require
# a second pass.
#
# - Without looking at the source code of the full module, it is not even
# possible to determine whether the top level of the with continuations
# block is inside a function or not. This has implications to `call_cc`
# invoked from the top level of the block: should the variables from
# the parent scope be declared `nonlocal` or `global`?
#
# It is much simpler and much more robust to just document that introducing a
# continuation introduces a scope boundary - that is a simple, transparent rule
# that is easy to work with. The behavior is no worse than how, in standard Python,
# comprehensions and generator expressions introduce a scope boundary.
#
# owner: FunctionDef node, or `None` if the use site of the `call_cc` is not inside a function
# def patch_scoping(owner, before, callcc, after):
# # Determine the names of all variables that should be made local to the continuation function.
# # In the unexpanded code, the continuation doesn't look like a new scope, so by appearances,
# # these will effectively break the usual scoping rules. Thus this set should be kept minimal.
# # To allow the machinery to actually work, at least the parameters of the continuation function
# # *must* be allowed to shadow names from the parent scope.
# targets, starget, ignored_condition, ignored_thecall, ignored_altcall = analyze_callcc(callcc)
# if not targets and not starget:
# targets = ["_ignored_arg"] # this must match what `make_continuation` does, below
# # The assignment targets of the `call_cc` become parameters of the continuation function.
# # Furthermore, a continuation function generated by `make_continuation` always takes
# # the `cc` and `_pcc` parameters.
# afterargs = targets + ([starget] or []) + ["cc", "_pcc"]
# afterlocals = afterargs
#
# if owner:
# # When `call_cc` is used inside a function, local variables of the
# # parent function (including parameters) become nonlocals in the
# # continuation.
# #
# # But only those that are not also locals of the continuation!
# # In that case, the local variable of the continuation overrides.
# # Locals of the continuation include its arguments, and any names in store context.
# beforelocals = set(extract_args(owner) + get_names_in_store_context(before))
# afternonlocals = list(beforelocals.difference(afterlocals))
# if afternonlocals: # TODO: Python 3.8: walrus assignment
# after.insert(0, Nonlocal(names=afternonlocals))
# else:
# # When `call_cc` is used at the top level of `with continuations` block,
# # the variables at that level become globals in the continuation.
# #
# # TODO: This **CANNOT** always work correctly, because we would need to know
# # TODO: whether the `with continuations` block itself is inside a function or not.
# # TODO: So we just assume it's outside any function.
# beforelocals = set(get_names_in_store_context(before))
# afternonlocals = list(beforelocals.difference(afterlocals))
# if afternonlocals: # TODO: Python 3.8: walrus assignment
# after.insert(0, Global(names=afternonlocals))
#
# # Nonlocals of the parent function remain nonlocals in the continuation.
# # When `owner is None`, `beforenonlocals` will be empty.
# beforenonlocals = collect_nonlocals(before)
# if beforenonlocals: # TODO: Python 3.8: walrus assignment
# after.insert(0, Nonlocal(names=beforenonlocals))
#
# # Globals of parent are also globals in the continuation.
# beforeglobals = collect_globals(before)
# if beforeglobals: # TODO: Python 3.8: walrus assignment
# after.insert(0, Global(names=beforeglobals))
#
# return after # we mutate; return it just for convenience