Skip to content

Commit 2de12fd

Browse files
committed
add rackety let_syntax
1 parent a3eed96 commit 2de12fd

File tree

3 files changed

+149
-0
lines changed

3 files changed

+149
-0
lines changed

macro_extras/main.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
let, letseq, letrec, \
1414
dlet, dletseq, dletrec, \
1515
blet, bletseq, bletrec, \
16+
let_syntax, \
1617
do, do0, local, \
1718
forall, insist, deny, \
1819
aif, it, \
@@ -417,6 +418,23 @@ def test14():
417418
result.append(lst)]]
418419
assert result == [[], [1]]
419420

421+
# local **syntactic** bindings (code splicing at macro-expansion time)
422+
evaluations = 0
423+
def verylongfunctionname(x=1):
424+
nonlocal evaluations
425+
evaluations += 1
426+
return x
427+
y = let_syntax((f, verylongfunctionname))[[ # extra brackets: implicit do
428+
f(),
429+
f(5)]]
430+
assert evaluations == 2
431+
assert y == 5
432+
y = let_syntax((f(a), verylongfunctionname(2*a)))[[ # templating
433+
f(2),
434+
f(3)]]
435+
assert evaluations == 4
436+
assert y == 6
437+
420438
# multilambda: multi-expression lambdas with implicit do
421439
with multilambda:
422440
# use brackets around the body of a lambda to denote a multi-expr body

unpythonic/syntax/__init__.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
let as _let, letseq as _letseq, letrec as _letrec, \
2020
dlet as _dlet, dletseq as _dletseq, dletrec as _dletrec, \
2121
blet as _blet, bletseq as _bletseq, bletrec as _bletrec
22+
from unpythonic.syntax.letsyntax import let_syntax_expr
2223
from unpythonic.syntax.prefix import prefix as _prefix
2324
from unpythonic.syntax.tailtools import autoreturn as _autoreturn, tco as _tco, \
2425
continuations as _continuations, bind
@@ -477,6 +478,61 @@ def do0(tree, gen_sym, **kw):
477478

478479
# -----------------------------------------------------------------------------
479480

481+
@macros.expr
482+
def let_syntax(tree, args, gen_sym, **kw):
483+
"""Introduce local **syntactic** bindings.
484+
485+
Usage::
486+
487+
let_syntax((lhs, rhs), ...)[body]
488+
489+
let_syntax((lhs, rhs), ...)[[body0, ...]]
490+
491+
The bindings are applied **at macro expansion time**, substituting
492+
the expression on the RHS for each instance of the corresponding LHS.
493+
This macro runs in the first pass (outside in).
494+
495+
This is useful to e.g. locally abbreviate long macro names,
496+
or to splice in several (parametric) instances of a common pattern.
497+
498+
The LHS may be:
499+
500+
- A bare name (e.g. ``x``), or
501+
502+
- A simple template of the form ``f(x, ...)``. The names inside the
503+
parentheses declare the formal parameters of the template.
504+
505+
Templates support only positional arguments, with no default values.
506+
507+
In the body, a template is used like a function call. Just like in an
508+
actual function call, when the template is substituted, the formal
509+
parameters on its RHS get replaced by the argument values from the
510+
"call" site; but ``let_syntax`` performs this at macro-expansion time.
511+
512+
This is a two-step process. In the first step, we apply template substitutions.
513+
In the second step, we apply bare name substitutions to the result of the
514+
first step. (So RHSs of templates may use any of the bare-name definitions.)
515+
516+
Within each step, the substitutions are applied **in the order specified**.
517+
So if the bindings are ``((x, y), (y, z))``, then ``x`` transforms to ``z``.
518+
But if the bindings are ``((y, z), (x, y))``, then ``x`` transforms to ``y``,
519+
and only an explicit ``y`` at the use site transforms to ``z``.
520+
521+
Inspired by Racket's ``let-syntax``, see:
522+
https://docs.racket-lang.org/reference/let.html
523+
524+
**CAUTION**: This is essentially a toy macro system inside a macro system.
525+
The usual caveats of macro systems apply. Especially, we support absolutely
526+
no form of hygiene. Be very, very careful to avoid name conflicts.
527+
528+
``let_syntax`` is meant only for simple local substitutions. If you need to
529+
do something complex, prefer writing a real macro directly in MacroPy.
530+
"""
531+
with dyn.let(gen_sym=gen_sym): # gen_sym is only needed by the implicit do.
532+
return (yield from let_syntax_expr(bindings=args, body=tree))
533+
534+
# -----------------------------------------------------------------------------
535+
480536
@macros.expr
481537
def forall(tree, **kw):
482538
"""[syntax, expr] Nondeterministic evaluation.

unpythonic/syntax/letsyntax.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
# This is for introducing **syntactic** local bindings, i.e. simple code splicing
5+
# at macro expansion time. If you're looking for regular run-time let et al. macros,
6+
# see letdo.py.
7+
8+
from ast import Name, Call, Starred
9+
from copy import deepcopy
10+
11+
from macropy.core.walkers import Walker
12+
13+
from unpythonic.syntax.letdo import implicit_do
14+
15+
def let_syntax_expr(bindings, body):
16+
body = implicit_do(body) # support the extra bracket syntax
17+
if not bindings:
18+
return body
19+
templates, barenames = _split_bindings(bindings)
20+
def substitute_barename(name, value, tree):
21+
@Walker
22+
def splice(tree, **kw):
23+
if type(tree) is Name and tree.id == name:
24+
tree = value
25+
return tree
26+
return splice.recurse(tree)
27+
for name, formalparams, value in templates:
28+
@Walker
29+
def splice(tree, **kw):
30+
if type(tree) is Call and type(tree.func) is Name and tree.func.id == name:
31+
theargs = tree.args
32+
if len(theargs) != len(formalparams):
33+
assert False, "let_syntax template '{}' expected {} arguments, got {}".format(name,
34+
len(formalparams),
35+
len(theargs))
36+
# make a fresh deep copy of the RHS to avoid destroying the template.
37+
tree = deepcopy(value) # expand the f itself in f(x, ...)
38+
for k, v in zip(formalparams, theargs): # expand the x, ... in the expanded form of f
39+
tree = substitute_barename(k, v, tree)
40+
return tree
41+
body = splice.recurse(body)
42+
for name, _, value in barenames:
43+
body = substitute_barename(name, value, body)
44+
yield body # first-pass macro (outside in) so that we can e.g. let_syntax((a, ast_literal))[...]
45+
46+
# TODO: implement a block version; should probably support some form of "with" to allow substituting statements.
47+
#def let_syntax_block(bindings, block_body):
48+
# pass
49+
50+
# -----------------------------------------------------------------------------
51+
52+
def _split_bindings(bindings): # bindings: sequence of ast.Tuple: (k1, v1), (k2, v2), ..., (kn, vn)
53+
names = set()
54+
templates = []
55+
barenames = []
56+
for line in bindings:
57+
k, v = line.elts
58+
if type(k) is Name:
59+
name = k.id
60+
args = []
61+
elif type(k) is Call and type(k.func) is Name: # simple templating f(x, ...)
62+
name = k.func.id
63+
if any(type(a) is Starred for a in k.args): # *args (Python 3.5+)
64+
assert False, "in template, only positional slots supported (no *args)"
65+
args = [a.id for a in k.args]
66+
if k.keywords:
67+
assert False, "in template, only positional slots supported (no named args or **kwargs)"
68+
else:
69+
assert False, "expected a name (e.g. x) or a template (e.g. f(x, ...)) on the LHS"
70+
if name in names:
71+
assert False, "duplicate '{}'; names defined in the same let_syntax must be unique".format(name)
72+
names.add(name)
73+
target = templates if args else barenames
74+
target.append((name, args, v))
75+
return templates, barenames

0 commit comments

Comments
 (0)