-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathautoref.py
More file actions
223 lines (205 loc) · 11.5 KB
/
autoref.py
File metadata and controls
223 lines (205 loc) · 11.5 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
# -*- coding: utf-8 -*-
"""Implicitly reference attributes of an object."""
from ast import (Name, Assign, Load, Call, Lambda, With, Constant, arg,
Attribute, Subscript, Store, Del)
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.walkers import ASTTransformer
from .astcompat import getconstant, Str
from .nameutil import isx
from .util import wrapwith, AutorefMarker
from .letdoutil import isdo, islet, ExpandedDoView, ExpandedLetView
from ..lazyutil import force1, passthrough_lazy_args
# with autoref[o]:
# with autoref[scipy.loadmat("mydata.mat")]: # evaluate once, assign to a gensym
# with autoref[scipy.loadmat("mydata.mat")] as o: # evaluate once, assign to given name
#
# We need something like::
#
# with autoref[o]:
# x # --> (o.x if hasattr(o, "x") else x)
# x.a # --> (o.x.a if hasattr(o, "x") else x.a)
# x[s] # --> (o.x[s] if hasattr(o, "x") else x[s])
# o # --> o
# with autoref[p]:
# x # --> (p.x if hasattr(p, "x") else (o.x if hasattr(o, "x") else x))
# x.a # --> (p.x.a if hasattr(p, "x") else (o.x.a if hasattr(o, "x") else x.a))
# x[s] # --> (p.x[s] if hasattr(p, "x") else (o.x[s] if hasattr(o, "x") else x[s]))
# o # --> (p.o if hasattr(p, "o") else o)
# o.x # --> (p.o.x if hasattr(p, "o") else o.x)
# o[s] # --> (p.o[s] if hasattr(p, "o") else o[s])
#
# One possible clean-ish implementation is::
#
# with AutorefMarker("o"): # no-op at runtime
# x # --> (lambda _ar271: _ar271[1] if _ar271[0] else x)(_autoref_resolve((o, "x")))
# x.a # --> ((lambda _ar271: _ar271[1] if _ar271[0] else x)(_autoref_resolve((o, "x")))).a
# x[s] # --> ((lambda _ar271: _ar271[1] if _ar271[0] else x)(_autoref_resolve((o, "x"))))[s]
# o # --> o (can only occur if an asname is supplied)
# with AutorefMarker("p"):
# x # --> (lambda _ar314: _ar314[1] if _ar314[0] else x)(_autoref_resolve((p, o, "x")))
# x.a # --> ((lambda _ar314: _ar314[1] if _ar314[0] else x)(_autoref_resolve((p, o, "x"))).a
# x[s] # --> ((lambda _ar314: _ar314[1] if _ar314[0] else x)(_autoref_resolve((p, o, "x")))[s]
# # when the inner autoref expands, it doesn't know about the outer one, so we will get this:
# o # --> (lambda _ar314: _ar314[1] if _ar314[0] else o)(_autoref_resolve((p, "o")))
# o.x # --> ((lambda _ar314: _ar314[1] if _ar314[0] else o)(_autoref_resolve((p, "o")))).x
# o[s] # --> ((lambda _ar314: _ar314[1] if _ar314[0] else o)(_autoref_resolve((p, "o"))))[s]
# # the outer autoref needs the marker to know to skip this (instead of looking up o.p):
# p # --> p
#
# The lambda is needed, because the lexical-variable lookup for ``x`` must occur at the use site,
# and it can only be performed by Python itself. We could modify ``_autoref_resolve`` to take
# ``locals()`` and ``globals()`` as arguments and look also in the ``builtins`` module,
# but that way we get no access to the enclosing scopes (the "E" in LEGB).
#
# Recall the blocks expand from inside out.
#
# We must leave an AST marker in place of the each autoref block, so that any outer autoref block (when it expands)
# understands that within that block, any read access to the name "p" is to be left alone.
#
# In ``_autoref_resolve``, we use a single args parameter to avoid dealing with ``*args``
# when analyzing the Call node. This used to be to avoid much special-case code for the
# AST differences between Python 3.4 and 3.5+. Now this doesn't matter any more, but
# there's no reason to change the design, either.
#
# In reality, we also capture-and-assign the autoref'd expr into a gensym'd variable (instead of referring
# to ``o`` and ``p`` directly), so that arbitrary expressions can be autoref'd without giving them
# a name in user code.
@passthrough_lazy_args
def _autoref_resolve(args):
*objs, s = [force1(x) for x in args]
for o in objs:
if hasattr(o, s):
return True, force1(getattr(o, s))
return False, None
def autoref(block_body, args, asname):
if len(args) != 1:
raise SyntaxError("expected exactly one argument, the expr to implicitly reference") # pragma: no cover
if not block_body:
raise SyntaxError("expected at least one statement inside the 'with autoref' block") # pragma: no cover
o = asname.id if asname else gensym("_o") # Python itself guarantees asname to be a bare Name.
# TODO: We can't use `unpythonic.syntax.util.isexpandedmacromarker` here, because it
# TODO: doesn't currently understand markers with arguments. Extend it?
#
# with AutorefMarker("_o42"):
def isexpandedautorefblock(tree):
if not (type(tree) is With and len(tree.items) == 1):
return False
ctxmanager = tree.items[0].context_expr
return (type(ctxmanager) is Call and
isx(ctxmanager.func, "AutorefMarker") and
len(ctxmanager.args) == 1 and type(ctxmanager.args[0]) in (Constant, Str)) # Python 3.8+: ast.Constant
def getreferent(tree):
return getconstant(tree.items[0].context_expr.args[0])
# (lambda _ar314: _ar314[1] if _ar314[0] else x)(_autoref_resolve((p, o, "x")))
def isautoreference(tree):
return (type(tree) is Call and
len(tree.args) == 1 and type(tree.args[0]) is Call and
isx(tree.args[0].func, "_autoref_resolve") and
type(tree.func) is Lambda and len(tree.func.args.args) == 1 and
tree.func.args.args[0].arg.startswith("_ar"))
def get_resolver_list(tree): # (p, o, "x")
return tree.args[0].args[0].elts
def add_to_resolver_list(tree, objnode):
lst = get_resolver_list(tree)
lst.insert(-1, objnode)
# x --> the autoref code above.
def makeautoreference(tree):
assert type(tree) is Name and (type(tree.ctx) is Load or not tree.ctx)
newtree = q[(lambda __ar_: __ar_[1] if __ar_[0] else a[tree])(h[_autoref_resolve]((n[o], u[tree.id])))]
our_lambda_argname = gensym("_ar")
# TODO: could we use `mcpyrate.utils.rename` here?
class PlaceholderRenamer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
if type(tree) is Name and tree.id == "__ar_":
tree.id = our_lambda_argname
elif type(tree) is arg and tree.arg == "__ar_":
tree.arg = our_lambda_argname
return self.generic_visit(tree)
return PlaceholderRenamer().visit(newtree)
class AutorefTransformer(ASTTransformer):
def transform(self, tree):
if is_captured_value(tree):
return tree # don't recurse!
referents = self.state.referents
if type(tree) in (Attribute, Subscript, Name) and type(tree.ctx) in (Store, Del):
return tree
# skip autoref lookup for let/do envs
elif islet(tree):
view = ExpandedLetView(tree)
self.generic_withstate(tree, referents=referents + [view.body.args.args[0].arg]) # lambda e14: ...
elif isdo(tree):
view = ExpandedDoView(tree)
self.generic_withstate(tree, referents=referents + [view.body[0].args.args[0].arg]) # lambda e14: ...
elif isexpandedautorefblock(tree):
self.generic_withstate(tree, referents=referents + [getreferent(tree)])
elif isautoreference(tree): # generated by an inner already expanded autoref block
thename = getconstant(get_resolver_list(tree)[-1])
if thename in referents:
# This case is tricky to trigger, so let's document it here. This code:
#
# with autoref[e]:
# with autoref[e2]:
# e
#
# expands to:
#
# with AutorefMarker('_o5'):
# _o5 = e
# with AutorefMarker('_o4'):
# _o4 = (lambda _ar13: (_ar13[1] if _ar13[0] else e2))(_autoref_resolve((_o5, 'e2')))
# (lambda _ar9: (_ar9[1] if _ar9[0] else e))(_autoref_resolve((_o4, _o5, 'e')))
#
# so there's no "e" as referent; the actual referent has a gensymmed name.
# Inside the body of the inner autoref, looking up "e" in e2 before falling
# back to the outer "e" is exactly what `autoref` is expected to do.
#
# Where is this used, then? The named variant `with autoref[...] as ...`:
#
# with step_expansion:
# with autoref[e] as outer:
# with autoref[e2] as inner:
# outer
#
# expands to:
#
# with AutorefMarker('outer'):
# outer = e
# with AutorefMarker('inner'):
# inner = (lambda _ar17: (_ar17[1] if _ar17[0] else e2))(_autoref_resolve((outer, 'e2')))
# outer # <-- !!!
#
# Now this case is triggered; we get a bare `outer` inside the inner body.
# TODO: Whether this wart is a good idea is another question...
# remove autoref lookup for an outer referent, inserted early by an inner autoref block
# (that doesn't know that any outer block exists)
tree = q[n[thename]] # (lambda ...)(_autoref_resolve((p, "o"))) --> o
else:
add_to_resolver_list(tree, q[n[o]]) # _autoref_resolve((p, "x")) --> _autoref_resolve((p, o, "x"))
return tree
elif type(tree) is Call and isx(tree.func, "AutorefMarker"): # nested autorefs
return tree
elif type(tree) is Name and (type(tree.ctx) is Load or not tree.ctx) and tree.id not in referents:
tree = makeautoreference(tree)
return tree
# Attribute works as-is, because a.b.c --> Attribute(Attribute(a, "b"), "c"), so Name "a" gets transformed.
# Subscript similarly, a[1][2] --> Subscript(Subscript(a, 1), 2), so Name "a" gets transformed.
return self.generic_visit(tree)
# Skip (by name) some common references inserted by other macros.
#
# We are a second-pass macro (inside out), so any first-pass macro invocations,
# as well as any second-pass macro invocations inside the `with autoref` block,
# have already expanded by the time we run our transformer.
always_skip = ['letter', 'dof', 'namelambda', 'curry', 'currycall', 'lazy', 'lazyrec', 'maybe_force_args',
# test framework stuff
'unpythonic_assert', 'unpythonic_assert_signals', 'unpythonic_assert_raises',
'callsite_filename', 'returns_normally']
newbody = [Assign(targets=[q[n[o]]], value=args[0])]
for stmt in block_body:
newbody.append(AutorefTransformer(referents=always_skip + [o]).visit(stmt))
return wrapwith(item=q[h[AutorefMarker](u[o])],
body=newbody,
locref=block_body[0])