Skip to content

Commit e551efc

Browse files
committed
porting: fix lambdatools bugs
The `quicklambda` macro needs access to the `f` macro interface, to expand that macro only; so the macro now lives in `unpythonic.syntax.__init__`. Also, `unpythonic.letdoutil.canonize_bindings` no longer needs a `locref`; this parameter has been removed. The parameter wasn't very useful in the `mcpyrate` version, because the source location is not filled in by the time the bindings are canonized in case the AST being canonized was generated by quasiquotes.
1 parent 8c26c35 commit e551efc

File tree

5 files changed

+41
-45
lines changed

5 files changed

+41
-45
lines changed

unpythonic/syntax/__init__.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"""
66

77
from mcpyrate import parametricmacro
8+
from mcpyrate.expander import MacroExpander
9+
from mcpyrate.utils import extract_bindings
810

911
from ..dynassign import make_dynvar, dyn
1012

@@ -156,7 +158,6 @@
156158
from .lambdatools import (multilambda as _multilambda,
157159
namedlambda as _namedlambda,
158160
f as _f,
159-
quicklambda as _quicklambda,
160161
envify as _envify)
161162
from .lazify import lazy as _lazy, lazify as _lazify, lazyrec as _lazyrec
162163
from .letdo import (local as _local, delete as _delete,
@@ -499,7 +500,7 @@ def letrec(tree, *, args, syntax, expander, **kw): # noqa: F811
499500
def _destructure_and_apply_let(tree, args, macro_expander, let_expander_function, allow_call_in_name_position=False):
500501
with dyn.let(_macro_expander=macro_expander): # implicit do (extra bracket notation) needs this.
501502
if args:
502-
bs = _canonize_bindings(args, locref=tree, allow_call_in_name_position=allow_call_in_name_position)
503+
bs = _canonize_bindings(args, allow_call_in_name_position=allow_call_in_name_position)
503504
return let_expander_function(bindings=bs, body=tree)
504505
# haskelly syntax, let[(...) in ...], let[..., where(...)]
505506
view = _UnexpandedLetView(tree) # note "tree" here is only the part inside the brackets
@@ -1175,9 +1176,15 @@ def quicklambda(tree, *, syntax, expander, **kw): # noqa: F811
11751176
if syntax != "block":
11761177
raise SyntaxError("quicklambda is a block macro only")
11771178

1178-
# This macro expands outside in, but needs `expander` for introspection.
1179-
with dyn.let(_macro_expander=expander):
1180-
return _quicklambda(block_body=tree)
1179+
# This macro expands outside in.
1180+
#
1181+
# In `mcpyrate`, expander instances are cheap - so we create a second expander
1182+
# to which we register only the `f` macro, under whatever names it appears in
1183+
# the original expander. Thus it leaves all other macros alone. This is the
1184+
# official `mcpyrate` way to immediately expand only some particular macros
1185+
# inside the current macro invocation.
1186+
bindings = extract_bindings(expander.bindings, f)
1187+
return MacroExpander(bindings, expander.filename).visit(tree)
11811188

11821189
def envify(tree, *, syntax, expander, **kw): # noqa: F811
11831190
"""[syntax, block] Make formal parameters live in an unpythonic env.

unpythonic/syntax/lambdatools.py

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from mcpyrate.quotes import macros, q, u, n, a, h # noqa: F401
1010

1111
from mcpyrate import gensym
12-
from mcpyrate.expander import MacroExpander
1312
from mcpyrate.quotes import is_captured_value
1413
from mcpyrate.splicing import splice_expression
1514
from mcpyrate.utils import extract_bindings
@@ -185,6 +184,7 @@ def transform(self, tree):
185184
def f(tree):
186185
# What's my name in the current expander? (There may be several names.)
187186
# https://github.com/Technologicat/mcpyrate/blob/master/doc/quasiquotes.md#hygienic-macro-recursion
187+
# TODO: doesn't currently work because this `f` is the syntax transformer, not the `f[]` macro.
188188
bindings = extract_bindings(dyn._macro_expander.bindings, f)
189189
mynames = list(bindings.keys())
190190

@@ -209,16 +209,6 @@ def transform(self, tree):
209209
tree.args.args = [arg(arg=x) for x in used_names]
210210
return tree
211211

212-
def quicklambda(block_body):
213-
# In `mcpyrate`, expander instances are cheap - so we create a second expander
214-
# to which we register only the `f` macro, under whatever names it appears in
215-
# the original expander. Thus it leaves all other macros alone. This is the
216-
# official `mcpyrate` way to immediately expand only some particular macros
217-
# inside the current macro invocation.
218-
expander = dyn._macro_expander
219-
bindings = extract_bindings(expander.bindings, f)
220-
return MacroExpander(bindings, expander.filename).visit(block_body)
221-
222212
def envify(block_body):
223213
# first pass, outside-in
224214
userlambdas = detect_lambda(block_body)
@@ -228,7 +218,11 @@ def envify(block_body):
228218
# second pass, inside-out
229219
def getargs(tree): # tree: FunctionDef, AsyncFunctionDef, Lambda
230220
a = tree.args
231-
argnames = [x.arg for x in a.args + a.kwonlyargs]
221+
if hasattr(a, "posonlyargs"): # Python 3.8+: positional-only parameters
222+
allargs = a.posonlyargs + a.args + a.kwonlyargs
223+
else:
224+
allargs = a.args + a.kwonlyargs
225+
argnames = [x.arg for x in allargs]
232226
if a.vararg:
233227
argnames.append(a.vararg.arg)
234228
if a.kwarg:
@@ -298,7 +292,8 @@ def isourupdate(thecall):
298292
view = UnexpandedEnvAssignView(tree)
299293
if view.name in bindings.keys():
300294
envset = Attribute(value=bindings[view.name].value, attr="set")
301-
return q[a[envset](u[view.name], a[view.value])]
295+
newvalue = self.visit(view.value)
296+
return q[a[envset](u[view.name], a[newvalue])]
302297
# transform references to currently active bindings
303298
elif type(tree) is Name and tree.id in bindings.keys():
304299
# We must be careful to preserve the Load/Store/Del context of the name.

unpythonic/syntax/letdoutil.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ def where(*bindings):
2121
_iscurrycall = make_isxpred("currycall") # output of ``unpythonic.syntax.curry``
2222

2323
# TODO: switch from call to subscript in name position for let_syntax templates.
24-
def canonize_bindings(elts, locref, allow_call_in_name_position=False): # public as of v0.14.3+
24+
def canonize_bindings(elts, allow_call_in_name_position=False): # public as of v0.14.3+
2525
"""Wrap a single binding without container into a length-1 `list`.
2626
2727
Pass through multiple bindings as-is.
@@ -35,9 +35,6 @@ def canonize_bindings(elts, locref, allow_call_in_name_position=False): # publi
3535
3636
where the ks and vs are AST nodes.
3737
38-
locref: AST node to copy location information from, in case we need to
39-
make a wrapper for a single binding.
40-
4138
allow_call_in_name_position: used by let_syntax to allow template definitions;
4239
in the call, the "function" is the template name, and the positional "parameters"
4340
are the template parameters (which may then appear in the template body).
@@ -46,7 +43,7 @@ def iskey(x):
4643
return ((type(x) is Name) or
4744
(allow_call_in_name_position and type(x) is Call and type(x.func) is Name))
4845
if len(elts) == 2 and iskey(elts[0]):
49-
return [Tuple(elts=elts, lineno=locref.lineno, col_offset=locref.col_offset)]
46+
return [Tuple(elts=elts)] # TODO: `mcpyrate`: just `q[t[elts]]`?
5047
if all((type(b) is Tuple and len(b.elts) == 2 and iskey(b.elts[0])) for b in elts):
5148
return elts
5249
raise SyntaxError("expected bindings to be ((k0, v0), ...) or a single (k, v)") # pragma: no cover
@@ -352,30 +349,31 @@ def _getbindings(self):
352349
t = self._type
353350
if t == "decorator": # bare Subscript, dlet[...], blet[...]
354351
if type(self._tree) is Call: # up to Python 3.8: parenthesis syntax for decorator macros
355-
return canonize_bindings(self._tree.args, self._tree)
352+
return canonize_bindings(self._tree.args)
356353
# Subscript as decorator (Python 3.9+)
357354
if sys.version_info >= (3, 9, 0): # Python 3.9+: the Index wrapper is gone.
358355
theargs = self._tree.slice
359356
else:
360357
theargs = self._tree.slice.value
361-
return canonize_bindings(theargs.elts, self._tree)
358+
return canonize_bindings(theargs.elts)
362359
elif t == "lispy_expr": # Subscript inside a Subscript, (let[...])[...]
363360
if sys.version_info >= (3, 9, 0): # Python 3.9+: the Index wrapper is gone.
364361
theargs = self._tree.value.slice
365362
else:
366363
theargs = self._tree.value.slice.value
367-
return canonize_bindings(theargs.elts, self._tree.value)
364+
return canonize_bindings(theargs.elts)
368365
else: # haskelly let, let[(...) in ...], let[..., where(...)]
369366
theexpr = self._theexpr_ref()
370367
if t == "in_expr":
371-
return canonize_bindings(theexpr.left.elts, theexpr.left)
368+
return canonize_bindings(theexpr.left.elts)
372369
elif t == "where_expr":
373-
return canonize_bindings(theexpr.elts[1].args, theexpr.elts[1])
370+
return canonize_bindings(theexpr.elts[1].args)
374371
def _setbindings(self, newbindings):
375372
t = self._type
376373
if t == "decorator":
377374
if type(self._tree) is Call: # up to Python 3.8: parenthesis syntax for decorator macros
378375
self._tree.args = newbindings
376+
return
379377
# Subscript as decorator (Python 3.9+)
380378
if sys.version_info >= (3, 9, 0): # Python 3.9+: the Index wrapper is gone.
381379
self._tree.slice.elts = newbindings

unpythonic/syntax/tests/test_lambdatools.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ def decorated(*args, **kwargs):
166166
test[result == 45]
167167

168168
with testset("integration: quicklambda, multilambda"):
169-
# First-pass macros, so in azazel75/macropy/HEAD, approximately the same thing as "with quicklambda, multilambda".
169+
# Outside-in macros.
170170
with quicklambda:
171171
with multilambda:
172172
func = f[[local[x << _], # noqa: F821, F823, `quicklambda` implicitly defines `f[]` to mean `lambda`.
@@ -201,9 +201,9 @@ def foo(n):
201201
with envify:
202202
def foo(n):
203203
return lambda i: n << n + i
204-
f = foo(10)
205-
test[f(1) == 11]
206-
test[f(1) == 12]
204+
g = foo(10)
205+
test[g(1) == 11]
206+
test[g(1) == 12]
207207

208208
# *starargs and **kwargs are also supported
209209
with envify:
@@ -228,16 +228,16 @@ def foo(**kwargs):
228228
with autoreturn, envify:
229229
def foo(n):
230230
lambda i: n << n + i
231-
f = foo(10)
232-
test[f(1) == 11]
233-
test[f(1) == 12]
231+
g = foo(10)
232+
test[g(1) == 11]
233+
test[g(1) == 12]
234234

235235
# or as a one-liner
236236
with autoreturn, envify:
237237
foo = lambda n: lambda i: n << n + i
238-
f = foo(10)
239-
test[f(1) == 11]
240-
test[f(1) == 12]
238+
g = foo(10)
239+
test[g(1) == 11]
240+
test[g(1) == 12]
241241

242242
# pythonic solution with optimal bytecode (doesn't need an extra location to store the accumulator)
243243
def foo(n):

unpythonic/syntax/tests/test_letdoutil.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,9 @@ def validate(lst):
3838
if type(k) is not Name:
3939
return False # pragma: no cover, only reached if the test fails.
4040
return True
41-
# known fake location information
42-
locref = q[n["here"]]
43-
locref.lineno = 9001
44-
locref.col_offset = 9
45-
test[validate(the[canonize_bindings(q[k0, v0].elts, locref)])] # noqa: F821, it's quoted.
46-
test[validate(the[canonize_bindings(q[((k0, v0),)].elts, locref)])] # noqa: F821
47-
test[validate(the[canonize_bindings(q[(k0, v0), (k1, v1)].elts, locref)])] # noqa: F821
41+
test[validate(the[canonize_bindings(q[k0, v0].elts)])] # noqa: F821, it's quoted.
42+
test[validate(the[canonize_bindings(q[((k0, v0),)].elts)])] # noqa: F821
43+
test[validate(the[canonize_bindings(q[(k0, v0), (k1, v1)].elts)])] # noqa: F821
4844

4945
# --------------------------------------------------------------------------------
5046
# AST structure matching

0 commit comments

Comments
 (0)