-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathlazify.py
More file actions
888 lines (741 loc) · 39.2 KB
/
lazify.py
File metadata and controls
888 lines (741 loc) · 39.2 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
# -*- coding: utf-8 -*-
"""Automatic lazy evaluation of function arguments."""
__all__ = ["lazy", "lazyrec", "lazify"]
from ast import (Lambda, FunctionDef, AsyncFunctionDef, Call, Name, Attribute,
Starred, keyword, List, Tuple, Dict, Set, Subscript, Load)
from functools import partial
from mcpyrate.quotes import macros, q, u, a, h # noqa: F401
from mcpyrate.astcompat import TypeAlias
from mcpyrate.astfixers import fix_ctx
from mcpyrate.quotes import capture_as_macro, is_captured_value
from mcpyrate.unparser import unparse
from mcpyrate.walkers import ASTTransformer
from .util import (suggest_decorator_index, sort_lambda_decorators, detect_lambda,
isx, getname, is_decorator)
from .letdoutil import islet, isdo, ExpandedLetView
from .nameutil import is_unexpanded_expr_macro
from ..lazyutil import Lazy, passthrough_lazy_args, force, force1, maybe_force_args
from ..dynassign import dyn
# -----------------------------------------------------------------------------
# The `lazy` macro comes from `demo/promise.py` in `mcpyrate`.
def lazy(tree, *, syntax, **kw):
"""[syntax, expr] Delay an expression (lazy evaluation).
This macro injects a lambda to delay evaluation, and encapsulates
the result into a *promise* (an `unpythonic.lazyutil.Lazy` object).
In Racket, this operation is known as `delay`.
"""
if syntax != "expr":
raise SyntaxError("lazy is an expr macro only") # pragma: no cover
# Expand outside in. Ordering shouldn't matter here.
return _lazy(tree)
def lazyrec(tree, *, syntax, **kw):
"""[syntax, expr] Delay items in a container literal, recursively.
Essentially, this distributes ``lazy[]`` into the items inside a literal
``list``, ``tuple``, ``set``, ``frozenset``, ``unpythonic.collections.box``
or ``unpythonic.llist.cons``, and into the values of a literal ``dict`` or
``unpythonic.collections.frozendict``.
Because this is a macro and must work by names only, only this fixed set of
container types is supported.
The container itself is not lazified, only the items inside it are, to keep
the lazification from interfering with unpacking. This allows things such as
``f(*lazyrec[(1*2*3, 4*5*6)])`` to work as expected.
See also ``lazy[]`` (the effect on each item) and ``unpythonic.syntax.force``
(the inverse of ``lazyrec[]``).
For an atom, ``lazyrec[]`` has the same effect as ``lazy[]``::
lazyrec[dostuff()] --> lazy[dostuff()]
For a container literal, ``lazyrec[]`` descends into it::
lazyrec[(2*21, 1/0)] --> (lazy[2*21], lazy[1/0])
lazyrec[{'a': 2*21, 'b': 1/0}] --> {'a': lazy[2*21], 'b': lazy[1/0]}
Constructor call syntax for container literals is also supported::
lazyrec[list(2*21, 1/0)] --> [lazy[2*21], lazy[1/0]]
Nested container literals (with any combination of known types) are
processed recursively, for example::
lazyrec[((2*21, 1/0), (1+2+3, 4+5+6))] --> ((lazy[2*21], lazy[1/0]),
(lazy[1+2+3], lazy[4+5+6]))
"""
if syntax != "expr":
raise SyntaxError("lazyrec is an expr macro only") # pragma: no cover
# Expand outside in. Ordering shouldn't matter here.
return _lazyrec(tree)
def lazify(tree, *, syntax, expander, **kw):
"""[syntax, block] Call-by-need for Python.
In a ``with lazify`` block, function arguments are evaluated only when
actually used, at most once each, and in the order in which they are
actually used. Promises are automatically forced on access.
Automatic lazification applies to arguments in function calls and to
let-bindings, since they play a similar role. **No other binding forms
are auto-lazified.**
Automatic lazification uses the ``lazyrec[]`` macro, which recurses into
certain types of container literals, so that the lazification will not
interfere with unpacking. See its docstring for details.
Comboing with other block macros in ``unpythonic.syntax`` is supported,
including ``curry`` and ``continuations``.
Silly contrived example::
with lazify:
def my_if(p, a, b):
if p:
return a # b never evaluated in this code path...
else:
return b # a never evaluated in this code path...
# ...hence the divisions by zero here are never performed.
assert my_if(True, 23, 1/0) == 23
assert my_if(False, 1/0, 42) == 42
Note ``my_if`` is a run-of-the-mill runtime function, not a macro. Only the
``with lazify`` is imbued with any magic.
Like ``with continuations``, no state or context is associated with a
``with lazify`` block, so lazy functions defined in one block may call
those defined in another. Calls between lazy and strict code are also
supported (in both directions), without requiring any extra effort.
Evaluation of each lazified argument is guaranteed to occur at most once;
the value is cached. Order of evaluation of lazy arguments is determined
by the (dynamic) order in which the lazy code actually uses them.
Essentially, the above code expands into::
from unpythonic.syntax import macros, lazy
from unpythonic.syntax import force
def my_if(p, a, b):
if force(p):
return force(a)
else:
return force(b)
assert my_if(lazy[True], lazy[23], lazy[1/0]) == 23
assert my_if(lazy[False], lazy[1/0], lazy[42]) == 42
plus some clerical details to allow lazy and strict code to be mixed.
Just passing through a lazy argument to another lazy function will
not trigger evaluation, even when it appears in a computation inlined
to the argument list::
with lazify:
def g(a, b):
return a
def f(a, b):
return g(2*a, 3*b)
assert f(21, 1/0) == 42
The division by zero is never performed, because the value of ``b`` is
not needed to compute the result (worded less magically, that promise is
never forced in the code path that produces the result). Essentially,
the above code expands into::
from unpythonic.syntax import macros, lazy
from unpythonic.syntax import force
def g(a, b):
return force(a)
def f(a, b):
return g(lazy[2*force(a)], lazy[3*force(b)])
assert f(lazy[21], lazy[1/0]) == 42
This relies on the magic of closures to capture f's ``a`` and ``b`` into
the promises.
But be careful; **assignments are not auto-lazified**, so the following does
**not** work::
with lazify:
def g(a, b):
return a
def f(a, b):
c = 3*b # not in an arglist, b gets evaluated!
return g(2*a, c)
assert f(21, 1/0) == 42
To avoid that, explicitly wrap the computation into a ``lazy[]``. For why
assignment RHSs are not auto-lazified, see the section on pitfalls below.
In calls, bare references (name, subscript, attribute) are detected and for
them, re-thunking is skipped. For example::
def g(a):
return a
def f(a):
return g(a)
assert f(42) == 42
expands into::
def g(a):
return force(a)
def f(a):
return g(a) # <-- no lazy[force(a)] since "a" is just a name
assert f(lazy[42]) == 42
When resolving references, subscripts and attributes are forced just enough
to obtain the containing object from a promise, if any; for example, the
elements of a list ``lst`` will not be evaluated just because the user code
happens to use ``lst.append(...)``; this only forces the object ``lst``
itself.
A ``lst`` appearing by itself evaluates the whole list. Similarly, ``lst[0]``
by itself evaluates only the first element, and ``lst[:-1]`` by itself
evaluates all but the last element. The index expression in a subscript is
fully forced, because its value is needed to determine which elements of the
subscripted container are to be accessed.
**Mixing lazy and strict code**
Lazy code is allowed to call strict functions and vice versa, without
requiring any additional effort.
Keep in mind what this implies: when calling a strict function, any arguments
given to it will be evaluated!
In the other direction, when calling a lazy function from strict code, the
arguments are evaluated by the caller before the lazy code gets control.
The lazy code gets just the evaluated values.
If you have, in strict code, an argument expression you want to pass lazily,
use syntax like ``f(lazy[...], ...)``. If you accidentally do this in lazy
code, it shouldn't break anything; ``with lazify`` detects any argument
expressions that are already promises, and just passes them through.
**Forcing promises manually**
This is mainly useful if you ``lazy[]`` or ``lazyrec[]`` something explicitly,
and want to compute its value outside a ``with lazify`` block.
We provide the functions ``force1`` and ``force``.
Using ``force1``, if ``x`` is a ``lazy[]`` promise, it will be forced,
and the resulting value is returned. If ``x`` is not a promise,
``x`` itself is returned, à la Racket.
The function ``force``, in addition, descends into containers (recursively).
When an atom ``x`` (i.e. anything that is not a container) is encountered,
it is processed using ``force1``.
Mutable containers are updated in-place; for immutables, a new instance is
created. Any container with a compatible ``collections.abc`` is supported.
(See ``unpythonic.collections.mogrify`` for details.) In addition, as
special cases ``unpythonic.collections.box`` and ``unpythonic.llist.cons``
are supported.
**Tips, tricks and pitfalls**
You can mix and match bare data values and promises, since ``force(x)``
evaluates to ``x`` when ``x`` is not a promise.
So this is just fine::
with lazify:
def f(x):
x = 2*21 # assign a bare data value
print(x) # the implicit force(x) evaluates to x
f(17)
If you want to manually introduce a promise, use ``lazy[]``::
from unpythonic.syntax import macros, lazify, lazy
with lazify:
def f(x):
x = lazy[2*21] # assign a promise
print(x) # the implicit force(x) evaluates the promise
f(17)
If you have a container literal and want to lazify it recursively in a
position that does not auto-lazify, use ``lazyrec[]`` (see its docstring
for details)::
from unpythonic.syntax import macros, lazify, lazyrec
with lazify:
def f(x):
return x[:-1]
lst = lazyrec[[1, 2, 3/0]]
assert f(lst) == [1, 2]
For non-literal containers, use ``lazy[]`` for each item as appropriate::
def f(lst):
lst.append(lazy["I'm lazy"])
lst.append(lazy["Don't call me lazy, I'm just evaluated later!"])
Keep in mind, though, that ``lazy[]`` will introduce a lambda, so there's
the usual pitfall::
from unpythonic.syntax import macros, lazify, lazy
with lazify:
lst = []
for x in range(3): # DANGER: only one "x", mutated imperatively
lst.append(lazy[x]) # all these closures capture the same "x"
print(lst[0]) # 2
print(lst[1]) # 2
print(lst[2]) # 2
So to capture the value instead of the name, use the usual workaround,
the wrapper lambda (here written more readably as a let, which it really is)::
from unpythonic.syntax import macros, lazify, lazy, let
with lazify:
lst = []
for x in range(3):
lst.append(let[[y << x] in lazy[y]])
print(lst[0]) # 0
print(lst[1]) # 1
print(lst[2]) # 2
Be careful not to ``lazy[]`` or ``lazyrec[]`` too much::
with lazify:
a = 10
a = lazy[2*a] # 20, right?
print(a) # crash!
Why does this example crash? The expanded code is::
with lazify:
a = 10
a = lazy[2*force(a)]
print(force(a))
The ``lazy[]`` sets up a promise, which will force ``a`` *at the time when
the containing promise is forced*, but at that time the name ``a`` points
to a promise, which will force...
The fundamental issue is that ``a = 2*a`` is an imperative update; if you
need to do that, just let Python evaluate the RHS normally (i.e. use the
value the name ``a`` points to *at the time when the RHS runs*).
Assigning a lazy value to a new name evaluates it, because any read access
triggers evaluation::
with lazify:
def g(x):
y = x # the "x" on the RHS triggers the implicit force
print(y) # bare data value
f(2*21)
Inspired by Haskell, Racket's (delay) and (force), and lazy/racket.
**Combos**
Introducing the *HasThon* programming language (it has 100% more Thon than
popular brands)::
with lazify, autocurry:
def add2first(a, b, c):
return a + b
assert add2first(2)(3)(1/0) == 5
def f(a, b):
return a
assert let[[c << 42,
d << 1/0] in f(c)(d)] == 42
assert letrec[[c << 42,
d << 1/0,
e << 2*c] in f(e)(d)] == 84
assert letrec[[c << 42,
d << 1/0,
e << 2*c] in [local[x << f(e)(d)],
x/4]] == 21
Works also with continuations. Rules:
- Also continuations are transformed into lazy functions.
- ``cc`` built by chain_conts is treated as lazy, **itself**; then it's
up to the continuations chained by it to decide whether to force their
arguments.
- The default continuation ``identity`` is strict, so that return values
from a continuation-enabled computation will be forced.
If you need a lazy ``identity`` (so that you can obtain those delicious
promises), use::
from unpythonic import identity
from unpythonic.lazyutil import passthrough_lazy_args
lazy_identity = passthrough_lazy_args(identity)
and then explicitly set the kwarg `cc=lazy_identity` when invoking the
continuation-enabled computation (e.g. in the example below, we could
`ourpromises = doit(cc=lazy_identity)`).
Example::
with lazify, continuations:
k = None
def setk(*args, cc):
nonlocal k
k = cc
return args[0]
def doit():
lst = ['the call returned']
*more, = call_cc[setk('A', 1/0)]
return lst + [more[0]]
assert doit() == ['the call returned', 'A']
assert k('again') == ['the call returned', 'again']
assert k('thrice', 1/0) == ['the call returned', 'thrice']
For a version with comments, see ``unpythonic/syntax/test/test_lazify.py``.
**CAUTION**: Call-by-need is a low-level language feature that is difficult
to bolt on after the fact. Some things might not work.
**CAUTION**: The functions in ``unpythonic.fun`` are lazify-aware (so that
e.g. curry and compose work with lazy functions), as are ``call`` and
``callwith`` in ``unpythonic.misc``, but the rest of ``unpythonic`` is not.
**CAUTION**: Argument passing by function call, and let-bindings are
currently the only binding constructs to which auto-lazification is applied.
"""
if syntax != "block":
raise SyntaxError("lazify is a block macro only") # pragma: no cover
if syntax == "block" and kw['optional_vars'] is not None:
raise SyntaxError("lazify does not take an as-part") # pragma: no cover
# Two-pass macro.
with dyn.let(_macro_expander=expander):
return _lazify(body=tree)
# -----------------------------------------------------------------------------
# lazy: syntax transformer, lazify a single expression
def _lazy(tree):
return q[h[Lazy](lambda: a[tree], sourcecode=u[f"lazy[{unparse(tree, debug=True)}]"])]
# lazyrec: syntax transformer, recursively lazify elements in container literals
#
# **CAUTION**: There are some containers whose constructors appear as a Call node,
# and also ``list``, ``tuple`` and ``set`` can be created via explicit calls.
#
# To treat these cases correctly, we must know which arguments to the
# constructors refer to other containers (to be unpacked into the new one)
# and which refer to atoms (to be added as individual items).
#
# Args that represent atoms should be lazified, so that they enter the container
# as lazy items.
#
# For args that represent containers:
#
# - Args that opaquely refer to an existing container should not be lazified,
# to avoid interfering with their unpacking.
#
# - Args where the value is a literal container should be lazified by descending
# into it, to lazify its items.
#
# For example::
#
# s = {1, 2, 3}
# fs = frozenset(s) # opaque container argument, do nothing
# fs = frozenset({1, 2, 3}) # literal container argument, descend
#
# d1 = {'a': 'foo', 'b': 'bar'}
# d2 = {'c': 'baz'}
# fd = frozendict(d1, d2, d='qux') # d1, d2 opaque containers; any kws are atoms
# fd = frozendict({1: 2, 3: 4}, d2, d='qux') # literal container, opaque container, atom
#
# In any case, *args and **kwargs are lazified only if literal containers;
# whatever they are, the result must be unpackable to perform the function call.
_ctorcalls_map = ("frozendict", "dict")
_ctorcalls_seq = ("list", "tuple", "set", "frozenset", "box", "ThreadLocalBox", "Some", "cons", "llist", "ll")
# when to lazify individual (positional, keyword) args.
_ctor_handling_modes = { # constructors that take iterable(s) as positional args.
"dict": ("literal_only", "all"),
"frozendict": ("literal_only", "all"), # same ctor API as dict
"list": ("literal_only", "all"), # doesn't take kws, "all" is ok
"tuple": ("literal_only", "all"),
"set": ("literal_only", "all"),
"frozenset": ("literal_only", "all"),
"llist": ("literal_only", "all"),
# constructors that take individual items as separate positional args.
"box": ("all", "all"),
"ThreadLocalBox": ("all", "all"),
"Some": ("all", "all"),
"cons": ("all", "all"),
"ll": ("all", "all")}
_ctorcalls_all = _ctorcalls_map + _ctorcalls_seq
# Usability: warn about incorrect use to prevent mysterious errors whose cause is hard to find.
#
# Constructors for which the positional mode is "literal_only" are susceptible
# to a particular variety of human error.
#
# Without the check in `lazify_ctorcall`, the invocation `lazyrec[tuple(1/0, 2/0)]`
# will crash due to a `ZeroDivisionError`. The lazifier skips the arguments, because
# the `tuple()` call is wrong; it should be `lazyrec[tuple((1/0, 2/0))]`.
# Note the outer parentheses; `tuple` takes an iterable as **its only argument**.
#
# `frozendict` is not in this list, because it has the functional-update initialization
# variant `frozendict(mapping1, mapping2, ...)`.
_ctorcalls_that_take_exactly_one_positional_arg = {"tuple", "list", "set", "dict", "frozenset", "llist"}
_unexpanded_lazy_name = "lazy"
_expanded_lazy_name = "Lazy"
_our_lazy = capture_as_macro(lazy)
def _lazyrec(tree):
is_unexpanded_lazy = partial(is_unexpanded_expr_macro, lazy, dyn._macro_expander)
# This helper doesn't need to recurse, so we don't need `ASTTransformer` here.
def transform(tree):
if type(tree) in (Tuple, List, Set):
tree.elts = [rec(x) for x in tree.elts]
elif type(tree) is Dict:
tree.values = [rec(x) for x in tree.values]
elif type(tree) is Call and any(isx(tree.func, ctor) for ctor in _ctorcalls_all):
p, k = _ctor_handling_modes[getname(tree.func)]
lazify_ctorcall(tree, p, k)
elif is_unexpanded_lazy(tree):
pass
elif type(tree) is Call and isx(tree.func, _expanded_lazy_name):
pass
else:
# `mcpyrate` supports hygienic macro capture, so we can just splice
# hygienic `lazy` invocations here.
tree = q[a[_our_lazy][a[tree]]]
return tree
def lazify_ctorcall(tree, positionals="all", keywords="all"):
if getname(tree.func) in _ctorcalls_that_take_exactly_one_positional_arg and len(tree.args) > 1:
raise SyntaxError(f"lazyrec[]: while analyzing constructor call `{getname(tree.func)}`: there should be exactly one argument, but {len(tree.args)} were given.") # pragma: no cover
newargs = []
for arg in tree.args:
if type(arg) is Starred: # *args in Python 3.5+
if _is_literal_container(arg.value, maps_only=False):
arg.value = rec(arg.value)
# else do nothing
elif positionals == "all" or _is_literal_container(arg, maps_only=False): # single positional arg
arg = rec(arg)
newargs.append(arg)
tree.args = newargs
for kw in tree.keywords:
if kw.arg is None: # **kwargs in Python 3.5+
if _is_literal_container(kw.value, maps_only=True):
kw.value = rec(kw.value)
# else do nothing
elif keywords == "all" or _is_literal_container(kw.value, maps_only=True): # single named arg
kw.value = rec(kw.value)
rec = transform
return rec(tree)
def _is_literal_container(tree, maps_only=False):
"""Test whether tree is a container literal understood by lazyrec[]."""
if not maps_only:
if type(tree) in (List, Tuple, Set):
return True
# Not reached in case of `lazyrec`, because `lazify_ctorcall` recurses
# into the the arg using `transform`. Which in turn uses `lazify_ctorcall`,
# which (beside the constructor name) looks only at the args.
if type(tree) is Call and any(isx(tree.func, s) for s in _ctorcalls_seq):
return True
if type(tree) is Dict:
return True
# Not reached in case of `lazyrec`, similarly as above.
if type(tree) is Call and any(isx(tree.func, s) for s in _ctorcalls_map):
return True
return False
# -----------------------------------------------------------------------------
# Note we do **not** lazify the RHS of assignments. This is one place where
# explicit is better than implicit; with auto-lazification of assignment RHSs
# it is too easy to accidentally set up an infinite recursion.
#
# This is ok:
# force1(lst)[0] = (10 * (force1(lst()[0]) if isinstance(lst, Lazy) else force1(lst[0])))
#
# but this blows up (by infinite recursion) later when we eventually force lst[0]:
# force1(lst)[0] = Lazy(lambda: (10 * (force1(lst()[0]) if isinstance(lst, Lazy) else force1(lst[0]))))
#
# We **could** solve this by forcing and capturing the current value before assigning,
# instead of allowing the RHS to refer to a lazy list element. But on the other hand,
# that's a **use** of the current value, so we may as well do nothing, causing
# the RHS to be evaluated, without the need to have any extra code here. :)
# TODO: other binding constructs?
# - keep in mind that force(x) == x if x is a non-promise atom, so a wrapper is not needed
# - don't lazify "with", eager init is the whole point of a context manager
# - don't lazify "for", the loop counter changes value imperatively (and usually rather rapidly)
# full list: see unpythonic.syntax.scopeanalyzer.get_names_in_store_context (and the link therein)
def _lazify(body):
# first pass, outside-in
userlambdas = detect_lambda(body)
# Expand any inner macro invocations. Particularly, this expands away any `lazyrec[]` and `lazy[]`
# so they become easier to work with. We also know that after this, any `Subscript` is really a
# subscripting operation and not a macro invocation.
#
# We must explicitly use recursive mode to ensure we get rid of all macro invocations, because
# we may be running inside a `with step_expansion`, which uses the expand-once-only mode.
body = dyn._macro_expander.visit_recursively(body)
# `lazify`'s analyzer needs the `ctx` attributes in `tree` to be filled in correctly.
body = fix_ctx(body, copy_seen_nodes=False) # TODO: or maybe copy seen nodes?
# second pass, inside-out
class LazifyTransformer(ASTTransformer):
def transform(self, tree):
forcing_mode = self.state.forcing_mode
# Forcing references (Name, Attribute, Subscript):
# x -> f(x)
# a.x -> f(force1(a).x)
# a.b.x -> f(force1(force1(a).b).x)
# a[j] -> f((force1(a))[force(j)])
# a[j][k] -> f(force1(force1(a)[force(j)])[force(k)])
#
# where f is force, force1 or identity (optimized away) depending on
# where the term appears; j and k may be indices or slices.
#
# Whenever not in Load context, f is identity.
#
# The idea is to apply just the right level of forcing to be able to
# resolve the reference, and then decide what to do with the resolved
# reference based on where it appears.
#
# For example, when subscripting a list, force1 it to unwrap it from
# a promise if it happens to be inside one, but don't force its elements
# just for the sake of resolving the reference. Then, apply f to the
# whole subscript term (forcing the accessed slice of the list, if necessary).
def f(tree):
if type(tree.ctx) is Load:
if forcing_mode == "full":
return q[h[force](a[tree])]
elif forcing_mode == "flat":
return q[h[force1](a[tree])]
# else forcing_mode == "off"
return tree
# Hygienic captures must be treated separately:
if is_captured_value(tree):
if forcing_mode in ("full", "flat"):
return q[h[force](a[tree])]
# else forcing_mode == "off"
return tree
# Python 3.12+: leave `type` statements alone (lazifying a type declaration makes no sense)
elif type(tree) is TypeAlias:
return tree
elif type(tree) in (FunctionDef, AsyncFunctionDef, Lambda):
if type(tree) is Lambda and id(tree) not in userlambdas:
return self.generic_visit(tree) # ignore macro-introduced lambdas (but recurse inside them)
else:
# mark this definition as lazy, and insert the interface wrapper
# to allow also strict code to call this function
if type(tree) is Lambda:
lam = tree
tree = q[h[passthrough_lazy_args](a[tree])]
# TODO: This doesn't really do anything; we don't here see the chain
# TODO: of Call nodes (decorators) that surround the Lambda node.
tree = sort_lambda_decorators(tree)
lam.body = self.visit(lam.body)
else:
k = suggest_decorator_index("passthrough_lazy_args", tree.decorator_list)
# Force the decorators only after `suggest_decorator_index`
# has suggested us where to put ours.
# TODO: could make `suggest_decorator_index` ignore a `force()` wrapper.
tree.decorator_list = self.visit(tree.decorator_list)
if k is not None:
tree.decorator_list.insert(k, q[h[passthrough_lazy_args]])
else:
# passthrough_lazy_args should generally be as innermost as possible
# (so that e.g. the curry decorator will see the function as lazy)
tree.decorator_list.append(q[h[passthrough_lazy_args]])
tree.body = self.visit(tree.body)
return tree
elif type(tree) is Call:
# We don't need to expand in the output of `_lazyrec`,
# because we don't recurse further into the args of the call,
# so the `lazify` transformer never sees the confusing `Subscript`
# instances that are actually macro invocations for `lazy[]`.
def transform_arg(tree):
# add any needed force() invocations inside the tree,
# but leave the top level of simple references untouched.
isref = type(tree) in (Name, Attribute, Subscript)
self.withstate(tree, forcing_mode=("off" if isref else "full"))
tree = self.visit(tree)
if not isref: # (re-)thunkify expr; a reference can be passed as-is.
tree = _lazyrec(tree)
return tree
def transform_starred(tree, dstarred=False):
isref = type(tree) in (Name, Attribute, Subscript)
self.withstate(tree, forcing_mode=("off" if isref else "full"))
tree = self.visit(tree)
# lazify items if we have a literal container
# we must avoid lazifying any other exprs, since a Lazy cannot be unpacked.
if _is_literal_container(tree, maps_only=dstarred):
tree = _lazyrec(tree)
return tree
# let bindings have a role similar to function arguments, so auto-lazify there
# (LHSs are always new names, so no infinite loop trap for the unwary)
if islet(tree):
view = ExpandedLetView(tree)
if view.mode == "let":
for b in view.bindings.elts: # b = (name, value)
b.elts[1] = transform_arg(b.elts[1])
else: # view.mode == "letrec":
for b in view.bindings.elts: # b = (name, (lambda e: ...))
thelambda = b.elts[1]
thelambda.body = transform_arg(thelambda.body)
if view.body: # let decorators have no body inside the Call node
thelambda = view.body
thelambda.body = self.visit(thelambda.body)
return tree
# Don't lazify in calls to some specific functions we know to be strict.
# Some of these are performance optimizations; others must be left as-is
# for other macros to be able to see the original calls. (It also generates
# cleaner expanded output.)
# - `namelambda` (emitted by `let[]`, `do[]`, and `test[]`)
# - All known container constructor calls (listed in `_ctorcalls_all`).
# - `Lazy` takes a lambda, constructs a `Lazy` object; if we're calling `Lazy`,
# the expression is already lazy.
# - `_autoref_resolve` does the name lookup in `with autoref` blocks.
#
# Don't lazify in calls to return-value utilities, because return values
# are never implicitly lazy in `unpythonic`.
# - `Values` constructs a multiple-return-values and/or named return values.
# - `(chain_conts(cc1, cc2))(args)` handles a return value in `with continuations`.
elif (isdo(tree) or is_decorator(tree.func, "namelambda") or
any(isx(tree.func, s) for s in _ctorcalls_all) or
isx(tree.func, _expanded_lazy_name) or
isx(tree.func, "_autoref_resolve") or
isx(tree.func, "Values") or
(type(tree.func) is Call and isx(tree.func.func, "chain_conts"))):
# Here we know the operator (.func) to be one of specific names;
# don't transform it to avoid confusing `lazyrec[]`.
#
# This is especially important, if this is an inner call in the
# arglist of an outer, lazy call, since it must see any container
# constructor calls that appear in the args.
#
# But *do* transform in the positional and named args of the call;
# doing so generates the code to force any promises that are passed
# to the function being called.
#
# TODO: correct forcing mode for recursion? We shouldn't need to forcibly use "full",
# since maybe_force_args() already fully forces any remaining promises
# in the args when calling a strict function.
# NOTE v0.15.0: In practice, using whatever is the currently active mode seems to be fine.
tree.args = self.visit(tree.args)
tree.keywords = self.visit(tree.keywords)
return tree
else: # general case
thefunc = self.visit(tree.func)
# Lazify the arguments of the call.
adata = []
for x in tree.args:
if type(x) is Starred: # *args in Python 3.5+
v = transform_starred(x.value)
v = Starred(value=q[a[v]])
else:
v = transform_arg(x)
adata.append(v)
kwdata = []
for x in tree.keywords:
if x.arg is None: # **kwargs in Python 3.5+
v = transform_starred(x.value, dstarred=True)
else:
v = transform_arg(x.value)
kwdata.append((x.arg, v))
# Construct the call
mycall = Call(func=q[h[maybe_force_args]],
args=[q[a[thefunc]]] + [q[a[x]] for x in adata],
keywords=[keyword(arg=k, value=q[a[x]]) for k, x in kwdata])
tree = mycall
return tree
# NOTE: We must expand all inner macro invocations before we hit this, or we'll produce nonsense.
# Hence it is easiest to have `lazify` expand inside-out.
elif type(tree) is Subscript: # force only accessed part of obj[...]
# force the slice expression; it is needed to extract the relevant items.
self.withstate(tree.slice, forcing_mode="full")
tree.slice = self.visit(tree.slice)
# resolve reference to the actual container without forcing its items.
self.withstate(tree.value, forcing_mode="flat")
tree.value = self.visit(tree.value)
# using the currently active forcing mode, force the value returned
# by the subscript expression.
tree = f(tree)
return tree
elif type(tree) is Attribute:
# a.b.c --> f(force1(force1(a).b).c) (Load)
# --> force1(force1(a).b).c (Store)
# attr="c", value=a.b
# attr="b", value=a
# Note in case of assignment to a compound, only the outermost
# Attribute is in Store context.
#
# Recurse in flat mode. Consider lst = [[1, 2], 3]
# lst[0] --> f(force1(lst)[0]), but
# lst[0].append --> force1(force1(force1(lst)[0]).append)
# Hence, looking up an attribute should only force **the object**
# so that we can perform the attribute lookup on it, whereas
# looking up values should finally f() the whole slice.
# (In the above examples, we have omitted f() when it is identity;
# in reality there is always an f() around the whole expr.)
self.withstate(tree.value, forcing_mode="flat")
tree.value = self.visit(tree.value)
# using the currently active forcing mode, force the value returned
# by the attribute expression.
tree = f(tree)
return tree
elif type(tree) is Name and type(tree.ctx) is Load:
# using the currently active forcing mode, force the value.
tree = f(tree)
# must not recurse when a Name changes into a Call.
return tree
return self.generic_visit(tree)
newbody = []
for stmt in body:
newbody.append(LazifyTransformer(forcing_mode="full").visit(stmt))
# Pay-as-you-go: to avoid a drastic performance hit (~10x) in trampolines
# built by unpythonic.tco.trampolined for regular strict code, a special mode
# must be enabled to build lazify-aware trampolines.
#
# The idea is that the mode is enabled while any function definitions in the
# "with lazify" block run, so they get a lazify-aware trampoline.
# This should be determined lexically, but that's complicated to do API-wise,
# so we currently enable the mode for the dynamic extent of the "with lazify".
# Usually this is close enough; the main case where this can behave
# unexpectedly is::
#
# @trampolined # strict trampoline
# def g():
# ...
#
# def make_f():
# @trampolined # which kind of trampoline is this?
# def f():
# ...
# return f
#
# f1 = make_f() # f1 gets the strict trampoline
#
# with lazify:
# @trampolined # lazify-aware trampoline
# def h():
# ...
#
# f2 = make_f() # f2 gets the lazify-aware trampoline
#
# TCO chains with an arbitrary mix of lazy and strict functions should work
# as long as the first function in the chain has a lazify-aware trampoline,
# because the chain runs under the trampoline of the first function.
#
# Tail-calling from a strict function into a lazy function should work, because
# all arguments are evaluated at the strict side before the call is made.
#
# But tail-calling strict -> lazy -> strict will fail in some cases.
# The second strict callee may get promises instead of values, because the
# strict trampoline does not have the maybe_force_args (that usually forces the args
# when lazy code calls into strict code).
with q as quoted:
with h[dyn.let](_build_lazy_trampoline=True):
with a:
newbody
return quoted
# -----------------------------------------------------------------------------