-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathlambdatools.py
More file actions
308 lines (280 loc) · 14.6 KB
/
lambdatools.py
File metadata and controls
308 lines (280 loc) · 14.6 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
# -*- coding: utf-8 -*-
"""Lambdas with multiple expressions, local variables, and a name."""
from ast import (Lambda, List, Name, Assign, Subscript, Call, FunctionDef,
AsyncFunctionDef, Attribute, keyword, Dict, Constant, arg,
copy_location)
from copy import deepcopy
from mcpyrate.quotes import macros, q, u, n, a, h # noqa: F401
from mcpyrate import gensym
from mcpyrate.quotes import is_captured_value
from mcpyrate.splicing import splice_expression
from mcpyrate.utils import extract_bindings
from mcpyrate.walkers import ASTTransformer
from ..dynassign import dyn
from ..misc import namelambda
from ..fun import orf
from ..env import env
from .astcompat import getconstant, Str, NamedExpr
from .letdo import do
from .letdoutil import islet, isenvassign, UnexpandedLetView, UnexpandedEnvAssignView, ExpandedDoView
from .util import (is_decorated_lambda, isx, make_isxpred, has_deco,
destructure_decorated_lambda, detect_lambda)
def multilambda(block_body):
class MultilambdaTransformer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
if not (type(tree) is Lambda and type(tree.body) is List):
return self.generic_visit(tree)
bodys = tree.body
# bracket magic:
# - don't recurse to the implicit lambdas generated by the "do" we are inserting here
# - for each item, "do" internally inserts a lambda to delay execution,
# as well as to bind the environment
# - we must do() instead of q[h[do][...]] for pickling reasons
# - but recurse manually into each *do item*; these are explicit
# user-provided code so we should transform them
bodys = self.visit(bodys)
tree.body = do(bodys) # insert the do, with the implicit lambdas
return tree
# multilambda should expand first before any let[], do[] et al. that happen
# to be inside the block, to avoid misinterpreting implicit lambdas
# generated by those constructs.
return MultilambdaTransformer().visit(block_body)
def namedlambda(block_body):
def issingleassign(tree):
return type(tree) is Assign and len(tree.targets) == 1 and type(tree.targets[0]) is Name
# detect a manual curry
iscurry = make_isxpred("curry")
def iscurrywithfinallambda(tree):
if not (type(tree) is Call and isx(tree.func, iscurry) and tree.args):
return False
return type(tree.args[-1]) is Lambda
# Detect an autocurry from an already expanded "with autocurry".
# CAUTION: These must match what unpythonic.syntax.curry.autocurry uses in its output.
iscurrycall = make_isxpred("currycall")
iscurryf = orf(make_isxpred("curryf"), make_isxpred("curry")) # auto or manual curry in a "with autocurry"
def isautocurrywithfinallambda(tree):
if not (type(tree) is Call and isx(tree.func, iscurrycall) and tree.args and
type(tree.args[-1]) is Call and isx(tree.args[-1].func, iscurryf)):
return False
return type(tree.args[-1].args[-1]) is Lambda
def iscallwithnamedargs(tree):
return type(tree) is Call and tree.keywords
# If `tree` is a (bare or decorated) lambda, inject run-time code to name
# it as `myname` (str); else return `tree` as-is.
def nameit(myname, tree):
match, thelambda = False, None
# for decorated lambdas, match any chain of one-argument calls.
d = is_decorated_lambda(tree, mode="any") and not has_deco(tree, "namelambda")
c = iscurrywithfinallambda(tree)
# this matches only during the second pass (after "with autocurry" has expanded)
# so it can't have namelambda already applied
if isautocurrywithfinallambda(tree): # "currycall(..., curryf(lambda ...: ...))"
match = True
thelambda = tree.args[-1].args[-1]
tree.args[-1].args[-1] = q[h[namelambda](u[myname])(a[thelambda])]
elif type(tree) is Lambda or d or c:
match = True
if d:
decorator_list, thelambda = destructure_decorated_lambda(tree)
elif c:
thelambda = tree.args[-1]
else:
thelambda = tree
tree = q[h[namelambda](u[myname])(a[tree])] # plonk it as outermost and hope for the best
return tree, thelambda, match
class NamedLambdaTransformer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
if islet(tree, expanded=False): # let bindings
view = UnexpandedLetView(tree)
for b in view.bindings:
b.elts[1], thelambda, match = nameit(b.elts[0].id, b.elts[1])
if match:
thelambda.body = self.visit(thelambda.body)
else:
b.elts[1] = self.visit(b.elts[1])
view.body = self.visit(view.body)
return tree
# assumption: no one left-shifts by a literal lambda :)
elif isenvassign(tree): # f << (lambda ...: ...)
view = UnexpandedEnvAssignView(tree)
view.value, thelambda, match = nameit(view.name, view.value)
if match:
thelambda.body = self.visit(thelambda.body)
else:
view.value = self.visit(view.value)
return tree
elif issingleassign(tree): # f = lambda ...: ...
tree.value, thelambda, match = nameit(tree.targets[0].id, tree.value)
if match:
thelambda.body = self.visit(thelambda.body)
else:
tree.value = self.visit(tree.value)
return tree
elif type(tree) is NamedExpr: # f := lambda ...: ... (Python 3.8+, added in unpythonic 0.15)
tree.value, thelambda, match = nameit(tree.target.id, tree.value)
if match:
thelambda.body = self.visit(thelambda.body)
else:
tree.value = self.visit(tree.value)
return tree
elif iscallwithnamedargs(tree): # foo(f=lambda: ...)
for kw in tree.keywords:
if kw.arg is None: # **kwargs in Python 3.5+
kw.value = self.visit(kw.value)
continue
# a single named arg
kw.value, thelambda, match = nameit(kw.arg, kw.value)
if match:
thelambda.body = self.visit(thelambda.body)
else:
kw.value = self.visit(kw.value)
tree.args = self.visit(tree.args)
return tree
elif type(tree) is Dict: # {"f": lambda: ..., "g": lambda: ...}
lst = list(zip(tree.keys, tree.values))
for j in range(len(lst)):
k, v = tree.keys[j], tree.values[j]
if k is None: # {..., **d, ...}
tree.values[j] = self.visit(v)
else:
if type(k) in (Constant, Str): # Python 3.8+: ast.Constant
thename = getconstant(k)
tree.values[j], thelambda, match = nameit(thename, v)
if match:
thelambda.body = self.visit(thelambda.body)
else:
tree.values[j] = self.visit(v)
else:
tree.keys[j] = self.visit(k)
tree.values[j] = self.visit(v)
return tree
return self.generic_visit(tree)
# outside in: transform in unexpanded let[] forms
newbody = NamedLambdaTransformer().visit(block_body)
newbody = dyn._macro_expander.visit(newbody)
# inside out: transform in expanded autocurry
return NamedLambdaTransformer().visit(newbody)
# The function `f` is adapted from the `f` macro in `macropy.quick_lambda`,
# stripped into a bare syntax transformer., and then the `@Walker` inside
# converted to a `mcpyrate` `ASTTransformer`. We have also added the code
# to ignore any nested `f[]`.
#
# Used under the MIT license.
# Copyright (c) 2013-2018, Li Haoyi, Justin Holmgren, Alberto Berti and all the other contributors.
def f(tree):
# What's my name in the current expander? (There may be several names.)
# https://github.com/Technologicat/mcpyrate/blob/master/doc/quasiquotes.md#hygienic-macro-recursion
# TODO: doesn't currently work because this `f` is the syntax transformer, not the `f[]` macro.
bindings = extract_bindings(dyn._macro_expander.bindings, f)
mynames = list(bindings.keys())
class UnderscoreTransformer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
# Don't recurse into nested `f[]`.
# TODO: This would benefit from macro destructuring in the expander.
# TODO: See https://github.com/Technologicat/mcpyrate/issues/3
if type(tree) is Subscript and type(tree.value) is Name and tree.value.id in mynames:
return tree
elif type(tree) is Name and tree.id == "_":
name = gensym("_")
tree.id = name
self.collect(name)
return self.generic_visit(tree)
ut = UnderscoreTransformer()
tree = ut.visit(tree)
used_names = ut.collected
tree = q[lambda _: a[tree]] # noqa: F811, it's a placeholder overwritten at the next line.
tree.args.args = [arg(arg=x) for x in used_names]
return tree
def envify(block_body):
# first pass, outside-in
userlambdas = detect_lambda(block_body)
block_body = dyn._macro_expander.visit(block_body)
# second pass, inside-out
def getargs(tree): # tree: FunctionDef, AsyncFunctionDef, Lambda
a = tree.args
if hasattr(a, "posonlyargs"): # Python 3.8+: positional-only parameters
allargs = a.posonlyargs + a.args + a.kwonlyargs
else:
allargs = a.args + a.kwonlyargs
argnames = [x.arg for x in allargs]
if a.vararg:
argnames.append(a.vararg.arg)
if a.kwarg:
argnames.append(a.kwarg.arg)
return argnames
def isfunctionoruserlambda(tree):
return ((type(tree) in (FunctionDef, AsyncFunctionDef)) or
(type(tree) is Lambda and id(tree) in userlambdas))
# Create a renamed reference to the env() constructor to be sure the Call
# nodes added by us have a unique .func (not used by other macros or user code)
_ismakeenv = make_isxpred("_envify")
_envify = env
class EnvifyTransformer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
bindings = self.state.bindings
enames = self.state.enames
def isourupdate(thecall):
if type(thecall.func) is not Attribute:
return False
return thecall.func.attr == "update" and any(isx(thecall.func.value, x) for x in enames)
if isfunctionoruserlambda(tree):
argnames = getargs(tree)
if argnames:
# prepend env init to function body, update bindings
kws = [keyword(arg=k, value=q[n[k]]) for k in argnames] # "x" --> x
newbindings = bindings.copy()
if type(tree) in (FunctionDef, AsyncFunctionDef):
ename = gensym("e")
theenv = q[h[_envify]()]
theenv.keywords = kws
assignment = Assign(targets=[q[n[ename]]],
value=theenv)
assignment = copy_location(assignment, tree)
tree.body.insert(0, assignment)
elif type(tree) is Lambda and id(tree) in userlambdas:
# We must in general inject a new do[] even if one is already there,
# due to scoping rules. If the user code writes to the same names in
# its do[] env, this shadows the formals; if it then pops one of its names,
# the name should revert to mean the formal parameter.
#
# inject a do[] and reuse its env
tree.body = do(List(elts=[q[n["_here_"]],
tree.body]))
view = ExpandedDoView(tree.body) # view.body: [(lambda e14: ...), ...]
ename = view.body[0].args.args[0].arg # do[] environment name
theupdate = Attribute(value=q[n[ename]], attr="update")
thecall = q[a[theupdate]()]
thecall.keywords = kws
tree.body = splice_expression(thecall, tree.body, "_here_")
newbindings.update({k: Attribute(value=q[n[ename]], attr=k) for k in argnames}) # "x" --> e.x
self.generic_withstate(tree, enames=(enames + [ename]), bindings=newbindings)
else:
# leave alone the _envify() added by us
if type(tree) is Call and (isx(tree.func, _ismakeenv) or isourupdate(tree)):
# don't recurse
return tree
# transform env-assignments into our envs
elif isenvassign(tree):
view = UnexpandedEnvAssignView(tree)
if view.name in bindings.keys():
envset = Attribute(value=bindings[view.name].value, attr="set")
newvalue = self.visit(view.value)
return q[a[envset](u[view.name], a[newvalue])]
# transform references to currently active bindings
elif type(tree) is Name and tree.id in bindings.keys():
# We must be careful to preserve the Load/Store/Del context of the name.
# The default lets mcpyrate fix it later.
ctx = tree.ctx if hasattr(tree, "ctx") else None
out = deepcopy(bindings[tree.id])
out.ctx = ctx
return out
return self.generic_visit(tree)
return EnvifyTransformer(bindings={}, enames=[]).visit(block_body)