1214 lines
55 KiB
Python
1214 lines
55 KiB
Python
# -*- coding: utf-8; -*-
|
|
"""Quasiquotes. Build ASTs in your macros, using syntax that mostly looks like regular code.
|
|
|
|
The macro operators `q`, `u`, `n`, `a`, `s`, `t`, `h` are the primary API.
|
|
|
|
The functions `capture_value` and `capture_as_macro` are public, so you can get the
|
|
benefits of hygienic capture also in old-school macros that build ASTs manually
|
|
without using quasiquotes.
|
|
|
|
The `astify` and `unastify` functions are the low-level quasiquote compiler
|
|
and uncompiler, respectively.
|
|
"""
|
|
|
|
__all__ = ["capture_value", "capture_macro", "capture_as_macro",
|
|
"is_captured_value", "is_captured_macro",
|
|
"astify", "unastify",
|
|
"q", "u", "n", "a", "s", "t", "h"]
|
|
|
|
import ast
|
|
import copy
|
|
import pickle
|
|
import sys
|
|
|
|
from .core import Done, MacroExpansionError, global_bindings
|
|
from .coreutils import _mcpyrate_attr
|
|
from .expander import MacroExpander, isnamemacro
|
|
from .markers import ASTMarker, check_no_markers_remaining, delete_markers
|
|
from .unparser import unparse, unparse_with_fallbacks
|
|
from .utils import (NestingLevelTracker, extract_bindings, flatten, gensym,
|
|
scrub_uuid)
|
|
|
|
|
|
def _mcpyrate_quotes_attr(attr, *, force_import=False):
|
|
"""Create an AST that, when compiled and run, looks up `mcpyrate.quotes.attr`.
|
|
|
|
If `force_import` is `True`, use the builtin `__import__` function to
|
|
first import the `mcpyrate.quotes` module. This is useful for e.g.
|
|
hygienically unquoted values, whose eventual use site might not import
|
|
any `mcpyrate` modules.
|
|
"""
|
|
return _mcpyrate_attr(f"quotes.{attr}", force_import=force_import)
|
|
|
|
|
|
class QuasiquoteMarker(ASTMarker):
|
|
"""Base class for AST markers used by quasiquotes. Compiled away by `astify`."""
|
|
|
|
class SpliceNodes(QuasiquoteMarker):
|
|
"""Splice a `list` of AST nodes into the surrounding context.
|
|
|
|
Command sent by `ast_literal` (run-time part of `a`)
|
|
to `splice_ast_literals` (run-time part of the surrounding `q`).
|
|
"""
|
|
|
|
class QuasiquoteSearchDone(Done, QuasiquoteMarker):
|
|
"""Marker used by nested quasiquotes to tell the expander a subtree is already done.
|
|
|
|
This inherits, but is separate, from the usual `Done`, because:
|
|
|
|
1. We need to tell the expander that is processing the nested quasiquotes
|
|
to stop expanding an invocation that has already been considered.
|
|
|
|
2. We need to be able to eliminate these (and only these) before
|
|
generating the final quoted output.
|
|
"""
|
|
|
|
# --------------------------------------------------------------------------------
|
|
# Unquote commands for `astify`. Each type corresponds to an unquote macro.
|
|
|
|
class Unquote(QuasiquoteMarker):
|
|
"""Interpolate the value of the given subtree into the quoted tree. Emitted by `u[]`."""
|
|
|
|
|
|
class LiftSourcecode(QuasiquoteMarker):
|
|
"""Parse a string as a Python expression, interpolate the resulting AST. Emitted by `n[]`.
|
|
|
|
This allows e.g. computing names of lexical variables.
|
|
"""
|
|
def __init__(self, body, filename):
|
|
super().__init__(body)
|
|
self.filename = filename
|
|
self._fields += ["filename"]
|
|
|
|
|
|
class ASTLiteral(QuasiquoteMarker): # similar to `macropy`'s `Literal`, but supports block mode, too.
|
|
"""Interpolate the given AST. Emitted by `a`."""
|
|
def __init__(self, body, syntax):
|
|
super().__init__(body)
|
|
self.syntax = syntax
|
|
self._fields += ["syntax"]
|
|
|
|
|
|
class ASTList(QuasiquoteMarker):
|
|
"""Interpolate the given iterable of AST nodes as an `ast.List` node. Emitted by `s[]`."""
|
|
|
|
|
|
class ASTTuple(QuasiquoteMarker):
|
|
"""Interpolate the given iterable of AST nodes as an `ast.Tuple` node. Emitted by `t[]`."""
|
|
|
|
|
|
class Capture(QuasiquoteMarker): # like `macropy`'s `Captured`
|
|
"""Capture given subtree hygienically. Emitted by `h[]`.
|
|
|
|
Details: capture the value or macro name the given subtree evaluates to,
|
|
at the use site of `q`. The value or macro reference is frozen (by pickle)
|
|
so that it can be restored also in another Python process later.
|
|
|
|
(It is important hygienic captures can be restored across process boundaries,
|
|
to support bytecode caching for source files that invoke a macro that uses
|
|
`h[]` in its output.)
|
|
"""
|
|
def __init__(self, body, name):
|
|
super().__init__(body)
|
|
self.name = name
|
|
self._fields += ["name"]
|
|
|
|
# --------------------------------------------------------------------------------
|
|
# Run-time parts of the operators.
|
|
|
|
# Unquote doesn't have its own function here, because it's a special case of `astify`.
|
|
|
|
def lift_sourcecode(value, filename="<unknown>"):
|
|
"""Parse a string as a Python expression. Run-time part of `n[]`.
|
|
|
|
Main use case is to access lexical variables with names computed at your macro definition site::
|
|
|
|
lift_sourcecode("kitty") -> Name(id='kitty')
|
|
|
|
More complex expressions work, too::
|
|
|
|
lift_sourcecode("kitty.tail") -> Attribute(value=Name(id='kitty'),
|
|
attr='tail')
|
|
lift_sourcecode("kitty.tail.color") -> Attribute(value=Attribute(value=Name(id='kitty'),
|
|
attr='tail'),
|
|
attr='color')
|
|
lift_sourcecode("kitties[3].paws[2].claws")
|
|
"""
|
|
if not isinstance(value, str):
|
|
raise TypeError(f"`n[]`: expected an expression that evaluates to str, result was {type(value)} with value {repr(value)}")
|
|
return ast.parse(value, filename=f"<invocation of n[] in '{filename}'>", mode="eval").body
|
|
|
|
|
|
def _typecheck(node, cls, macroname):
|
|
if isinstance(node, ASTMarker):
|
|
if isinstance(node.body, list): # statement suite inside a marker
|
|
for child in node.body:
|
|
_typecheck(child, cls, macroname)
|
|
return
|
|
# single AST node inside a marker
|
|
_typecheck(node.body, cls, macroname)
|
|
return
|
|
if not isinstance(node, cls):
|
|
raise TypeError(f"{macroname}: expected {cls}, got {type(node)} with value {repr(node)}")
|
|
|
|
def _flatten_and_typecheck_iterable(nodes, cls, macroname):
|
|
try:
|
|
lst = list(nodes)
|
|
except TypeError:
|
|
raise TypeError(f"{macroname}: expected an iterable of AST nodes, got {type(nodes)} with value {repr(nodes)}")
|
|
lst = flatten(lst)
|
|
for node in lst:
|
|
_typecheck(node, cls, macroname)
|
|
return lst
|
|
|
|
def ast_literal(tree, syntax):
|
|
"""Perform run-time typecheck on AST literal `tree`. Run-time part of `a`.
|
|
|
|
If `tree` is a run-time iterable, convert it to a `list`, flatten that `list`
|
|
locally, and inject a run-time marker for `splice_ast_literals`, to indicate
|
|
where splicing into the surrounding context is needed.
|
|
"""
|
|
if syntax not in ("expr", "block"):
|
|
raise ValueError(f"expected `syntax` either 'expr' or 'block', got {repr(syntax)}")
|
|
|
|
if syntax == "expr":
|
|
if isinstance(tree, ast.AST):
|
|
_typecheck(tree, ast.expr, "`a` (expr mode)")
|
|
return tree
|
|
else:
|
|
lst = _flatten_and_typecheck_iterable(tree, ast.expr, "`a` (expr mode)")
|
|
return SpliceNodes(lst)
|
|
|
|
assert syntax == "block"
|
|
# Block mode `a` always produces a `list` of the items in its body.
|
|
# Each item may refer, at run time, to a statement AST node or to a `list`
|
|
# of statement AST nodes.
|
|
#
|
|
# We flatten locally here to get rid of the sublists, so that all statement
|
|
# nodes injected by this invocation of block mode `a` become gathered into
|
|
# a single flat "master list".
|
|
#
|
|
# However, there's a piece of postprocessing we cannot do here: the splice
|
|
# of the master list into the surrounding context. For that, we mark the
|
|
# place for `splice_ast_literals`, which is the run-time part of the
|
|
# surrounding block mode `q` (which allows it to operate on the whole
|
|
# quoted tree).
|
|
#
|
|
# The splicer must splice only places marked by us, because lists occur
|
|
# in many places in a Python AST beside statement suites (e.g. `Assign`
|
|
# targets, the parameter list in a function definition, ...).
|
|
lst = _flatten_and_typecheck_iterable(tree, ast.stmt, "`a` (block mode)")
|
|
return SpliceNodes(lst)
|
|
|
|
|
|
def splice_ast_literals(tree, filename):
|
|
"""Splice list-valued `a` AST literals into the surrounding context. Run-time part of `q`."""
|
|
# We do this recursively to splice also at any inner levels of the quoted
|
|
# AST (e.g. `with a` inside an `if`).
|
|
def doit(thing):
|
|
if isinstance(thing, list):
|
|
newthing = []
|
|
for item in thing:
|
|
if isinstance(item, SpliceNodes):
|
|
doit(item.body)
|
|
# Discard the `SpliceNodes` marker and splice the `list` that was contained in it.
|
|
newthing.extend(item.body)
|
|
else:
|
|
doit(item)
|
|
newthing.append(item)
|
|
thing[:] = newthing
|
|
# As of Python 3.9, `Global` and `Nonlocal` are the only AST node types
|
|
# where a field contains a `list` of bare strings.
|
|
elif isinstance(thing, (ast.Global, ast.Nonlocal)):
|
|
pass
|
|
elif isinstance(thing, ast.AST):
|
|
for fieldname, value in ast.iter_fields(thing):
|
|
if isinstance(value, list):
|
|
doit(value)
|
|
else:
|
|
raise TypeError(f"Expected `list` or AST node, got {type(thing)} with value {repr(thing)}")
|
|
doit(tree)
|
|
|
|
try:
|
|
check_no_markers_remaining(tree, filename=filename, cls=SpliceNodes)
|
|
except MacroExpansionError:
|
|
err = RuntimeError("`q`: `SpliceNodes` markers remaining after expansion, likely a misplaced `a` unquote; did you mean `s[]` or `t[]`?")
|
|
# The list of remaining markers is not very useful, suppress it
|
|
# (but leave it available for introspection in the `__context__` attribute).
|
|
err.__suppress_context__ = True
|
|
raise err
|
|
|
|
return tree
|
|
|
|
|
|
def ast_list(nodes):
|
|
"""Interpolate an iterable of expression AST nodes as an `ast.List` node. Run-time part of `s[]`."""
|
|
lst = _flatten_and_typecheck_iterable(nodes, ast.expr, "`s[]`")
|
|
return ast.List(elts=lst)
|
|
|
|
|
|
def ast_tuple(nodes):
|
|
"""Interpolate an iterable of expression AST nodes as an `ast.Tuple` node. Run-time part of `t[]`."""
|
|
lst = _flatten_and_typecheck_iterable(nodes, ast.expr, "`t[]`")
|
|
return ast.Tuple(elts=lst)
|
|
|
|
|
|
def capture_value(value, name):
|
|
"""Hygienically capture a run-time value. Used by `h[]`.
|
|
|
|
`value`: A run-time value. Must be picklable.
|
|
`name`: For human-readability.
|
|
|
|
The return value is an AST that, when compiled and run, returns the
|
|
captured value (even in another Python process later).
|
|
"""
|
|
# If we didn't need to consider bytecode caching, we could just store the
|
|
# value in a dictionary (that lives at the top level of `mcpyrate.quotes`)
|
|
# that is populated at macro expansion time. Each unique value (by `id`)
|
|
# could be stored only once.
|
|
#
|
|
# But we want to support bytecode caching. To avoid introducing hard-to-find
|
|
# bugs into user code, we must provide consistent semantics, regardless of
|
|
# whether updating of the bytecode cache is actually enabled or not (see
|
|
# `sys.dont_write_bytecode`). So we must do the same thing regardless of
|
|
# whether the captured value is used in the current process, or in another
|
|
# Python process later.
|
|
#
|
|
# If the macro expansion result is to remain available for reuse from a
|
|
# `.pyc`, we must serialize and store the captured value to disk, so that
|
|
# values from "macro expansion time last week" are still available when the
|
|
# `.pyc` is loaded in another Python process later.
|
|
#
|
|
# Modules are macro-expanded independently (no global finalization for the
|
|
# whole codebase), and a `.pyc` may indeed later get loaded into some other
|
|
# codebase that imports the same module, so we can't make a centralized
|
|
# registry, like we could without bytecode caching.
|
|
#
|
|
# So really pretty much the only thing we can do reliably and simply is to
|
|
# store a fresh serialized copy of the value at the capture location in the
|
|
# source code, independently at each capture location.
|
|
#
|
|
# Putting these considerations together, we pickle the value, causing a copy
|
|
# and serialization.
|
|
#
|
|
frozen_value = pickle.dumps(value)
|
|
return ast.Call(_mcpyrate_quotes_attr("lookup_value", force_import=True),
|
|
[ast.Tuple(elts=[ast.Constant(value=name),
|
|
ast.Constant(value=frozen_value)])],
|
|
[])
|
|
|
|
|
|
_lookup_cache = {}
|
|
def lookup_value(key):
|
|
"""Look up a hygienically captured run-time value. Used by `h[]`.
|
|
|
|
Usually there's no need to call this function manually; `capture_value`
|
|
(and thus also `h[]`) will generate an AST that calls this automatically.
|
|
|
|
**NOTE**: For advanced macrology: if your own macros need to detect hygienic
|
|
captures using `is_captured_value`, and you want to look up the captured
|
|
value based on a key returned by that function, be aware that `lookup_value`
|
|
will only succeed if a value has been captured.
|
|
|
|
Trying to look up a key that was extracted from a pre-capture AST
|
|
raises `ValueError`. In terms of the discussion in the docstring of
|
|
`is_captured_value`, you need a `lookup_value` AST for a value to
|
|
be present; a `capture_value` AST is too early. The transition occurs
|
|
when the use site of `q` runs.
|
|
|
|
In that scenario, before you call `lookup_value` on your key, check that
|
|
`frozen_value is not None` (see docstring of `is_captured_value`);
|
|
that indicates that a value has been captured and can be decoded by
|
|
this function.
|
|
"""
|
|
name, frozen_value = key
|
|
|
|
# Trying to look up a result of `is_captured_value` that isn't captured yet.
|
|
if frozen_value is None:
|
|
raise ValueError(f"The given key does not (yet) point to a value: {repr(key)}")
|
|
|
|
cachekey = (name, id(frozen_value)) # id() so each capture instance behaves independently
|
|
if cachekey not in _lookup_cache:
|
|
_lookup_cache[cachekey] = pickle.loads(frozen_value)
|
|
return _lookup_cache[cachekey]
|
|
|
|
|
|
def capture_macro(macro, name):
|
|
"""Hygienically capture a macro. Used by `h[]`.
|
|
|
|
`macro`: A macro function. Must be picklable.
|
|
`name`: For human-readability. The recommended value is the name of
|
|
the macro, as it appeared in the bindings of the expander
|
|
it was captured from.
|
|
|
|
The name of the captured macro is automatically uniqified using
|
|
`gensym(name)`.
|
|
|
|
The return value is an AST that, when compiled and run, injects the macro
|
|
into the expander's global macro bindings table (even in another Python
|
|
process later), and then evaluates to the uniqified macro name as an
|
|
`ast.Name`.
|
|
"""
|
|
if not callable(macro):
|
|
raise TypeError(f"`macro` must be callable (a macro function), got {type(macro)} with value {repr(macro)}")
|
|
# Scrub any previous UUID suffix from the macro name. We'll get those when
|
|
# `unastify` uncompiles a hygienic macro capture, and then `astify`
|
|
# compiles the result again.
|
|
frozen_macro = pickle.dumps(macro)
|
|
name = scrub_uuid(name)
|
|
return ast.Call(_mcpyrate_quotes_attr("lookup_macro"),
|
|
[ast.Tuple(elts=[ast.Constant(value=name),
|
|
ast.Constant(value=gensym(name)),
|
|
ast.Constant(value=frozen_macro)])],
|
|
[])
|
|
|
|
|
|
def capture_as_macro(macro):
|
|
"""Hygienically capture a macro function as a macro, manually.
|
|
|
|
Like `capture_macro`, but with one less level of delay. This injects the
|
|
macro into the expander's global bindings table immediately, and returns
|
|
the uniqified `ast.Name` that can be used to refer to it hygienically,
|
|
using `a[]`.
|
|
|
|
The name is taken automatically from the name of the macro function.
|
|
"""
|
|
if not callable(macro):
|
|
raise TypeError(f"`macro` must be callable (a macro function), got {type(macro)} with value {repr(macro)}")
|
|
frozen_macro = pickle.dumps(macro)
|
|
name = macro.__name__
|
|
return lookup_macro((name, gensym(name), frozen_macro))
|
|
|
|
|
|
def lookup_macro(key):
|
|
"""Look up a hygienically captured macro. Used by `h[]`.
|
|
|
|
This injects the macro to the expander's global macro bindings table,
|
|
and then returns the macro name, as an `ast.Name`.
|
|
|
|
Usually there's no need to call this function manually; `capture_macro`
|
|
(and thus also `h[]`) will generate an AST that calls this automatically.
|
|
"""
|
|
name, unique_name, frozen_macro = key
|
|
if unique_name not in global_bindings:
|
|
global_bindings[unique_name] = pickle.loads(frozen_macro)
|
|
return ast.Name(id=unique_name)
|
|
|
|
|
|
# --------------------------------------------------------------------------------
|
|
# Advanced macrology support.
|
|
|
|
# TODO: In a future version, do we want to add an ASTMarker for captured values
|
|
# TODO: that are ready for consumption? We could save the actual AST (which is
|
|
# TODO: now detected directly) into the `body` attribute of the marker, and make
|
|
# TODO: the compiler delete `HygienicValue` markers (replacing each by its `.body`)
|
|
# TODO: as the last step before handing the AST over to Python.
|
|
|
|
def is_captured_value(tree):
|
|
"""Test whether `tree` is a hygienically captured run-time value.
|
|
|
|
This function is sometimes useful for advanced macrology. It facilitates
|
|
user-defined macros to work together in an environment where hygienic
|
|
captures are present. One macro, using quasiquotes, builds an AST, and
|
|
another macro analyzes the expanded AST later.
|
|
|
|
Consider first, however, if you can arrange things so that the second macro
|
|
could analyze an *unexpanded* AST; that's often much easier. When the first
|
|
macro simply must expand first (for whatever reason), that's where this function
|
|
comes in.
|
|
|
|
With this function, you can check (either by name or by value) whether some
|
|
`q[h[myfunction]]` points to the desired `"myfunction"`, so that e.g. the AST
|
|
produced by `q[h[myfunction](a0, ...)]` can be recognized as a call to your
|
|
`myfunction`. This allows your second macro to know it's `myfunction`,
|
|
so that it'll know how to interpret the args of that call.
|
|
|
|
Real-world examples of where this is useful are too unwieldy to explain
|
|
here, but can be found in `unpythonic.syntax`. Particularly, see any use
|
|
sites of the helper function `unpythonic.syntax.nameutil.isx`.
|
|
|
|
To detect a hygienically captured *macro*, use `is_captured_macro` instead.
|
|
|
|
Return value:
|
|
|
|
- On no match, return `False`.
|
|
|
|
- On match, return a tuple `(name, frozen_value)`, where:
|
|
|
|
- `name` (str) is the name of the captured identifier, or when the captured
|
|
value is from an arbitrary expression, the unparsed source code of that
|
|
expression. There is no name mangling for identifiers; it's the exact
|
|
original name that appeared in the source code.
|
|
|
|
- `frozen_value` is either a `bytes` object that stores the frozen value
|
|
as opaque binary data, or `None` if the value has not been captured yet.
|
|
|
|
The `bytes` object can be decoded by passing the whole return value as `key`
|
|
to `lookup_value`. That function will decode the data and return the actual
|
|
value, just as if the hygienic reference was decoded normally at run time.
|
|
|
|
**NOTE**:
|
|
|
|
Stages in the life of a hygienically captured *run-time value* in `mcpyrate`:
|
|
|
|
1. When the surrounding `q` expands, it first expands any unquotes nested
|
|
within it, but only those where the quote level hits zero. The `h[]` is
|
|
converted into a `Capture` AST marker; see the `h` operator for details.
|
|
|
|
2. Then, still while the surrounding `q` expands, `q` compiles quasiquote
|
|
markers. A `Capture` marker, in particular, compiles into a call to
|
|
the function `capture_value`. This is the output at macro expansion time
|
|
(of the use site of `q`).
|
|
|
|
3. When the use site of `q` reaches run time, the `capture_value` runs
|
|
(thus actually performing the capture), and replaces itself (in the
|
|
AST that was produced by `q`) with a call to the function `lookup_value`.
|
|
That `lookup_value` call is still an AST node.
|
|
|
|
4. In typical usage, that use site of `q` is inside the implementation
|
|
of some user-defined macro. When *that macro's use site* reaches run
|
|
time, the `lookup_value` runs (each time that expression is executed).
|
|
|
|
So in the macro expansion of `q`, we have a call to `capture_value`
|
|
representing the hygienically captured run-time value. But once the macro
|
|
that uses `q` has returned its output, then we instead have a call to
|
|
`lookup_value`. The latter is the most likely scenario for advanced
|
|
user-defined macros that work together.
|
|
"""
|
|
if type(tree) is not ast.Call:
|
|
return False
|
|
|
|
# The format is one of:
|
|
#
|
|
# - direct reference: `(mcpyrate.quotes).xxx`
|
|
# - reference by import: `(__import__("mcpyrate.quotes", ...).quotes).xxx`
|
|
#
|
|
# First check the `xxx` part:
|
|
callee = tree.func
|
|
if not (type(callee) is ast.Attribute and callee.attr in ("capture_value", "lookup_value")):
|
|
return False
|
|
# Then the rest:
|
|
if not _is_mcpyrate_quotes_reference(callee.value):
|
|
return False
|
|
|
|
# This AST destructuring and constant extraction must match the format
|
|
# of the argument lists produced by the quasiquote system for calls to
|
|
# `capture_value` and `lookup_value`.
|
|
if callee.attr == "capture_value": # the call is `capture_value(..., name)`
|
|
name_node = tree.args[1]
|
|
assert type(name_node) is ast.Constant and type(name_node.value) is str
|
|
return (name_node.value, None) # the value hasn't been captured yet
|
|
elif callee.attr == "lookup_value": # the call is `lookup_value(key)`
|
|
key_node = tree.args[0]
|
|
name_node, frozen_value_node = key_node.elts
|
|
assert type(name_node) is ast.Constant and type(name_node.value) is str
|
|
assert type(frozen_value_node) is ast.Constant and type(frozen_value_node.value) is bytes
|
|
return (name_node.value, frozen_value_node.value)
|
|
|
|
assert False # cannot happen
|
|
|
|
|
|
def is_captured_macro(tree):
|
|
"""Just like `is_captured_value`, but detect a hygienically captured macro instead.
|
|
|
|
To detect a hygienically captured *run-time value*, use `is_captured_value` instead.
|
|
|
|
Return value:
|
|
|
|
- On no match, return `False`.
|
|
|
|
- On match, return a tuple `(name, unique_name, frozen_macro)`, where:
|
|
|
|
- `name` (str) is the name of the macro, as it appeared in the bindings
|
|
of the expander instance it was captured from.
|
|
|
|
- `unique_name` (str) is `name` with an underscore and UUID appended,
|
|
to make it unique. This is the name the macro will be injected as
|
|
into the expander's global bindings table.
|
|
|
|
(By unique, we mean "universally unique anywhere for approximately
|
|
the next one thousand years"; see `mcpyrate.gensym`, which links to
|
|
the UUID spec used by the implementation.)
|
|
|
|
- `frozen_macro` is a `bytes` object that stores a reference to the
|
|
frozen macro function as opaque binary data.
|
|
|
|
The `bytes` object can be decoded by passing the whole return value as `key`
|
|
to `lookup_macro`. That function will decode the data, inject the macro into
|
|
the expander's global bindings table (if not already there), and give you an
|
|
`ast.Name` node whose `id` attribute contains the unique name (str), just as
|
|
if the hygienic reference was decoded normally at macro expansion time.
|
|
|
|
Then, once the injection has taken place, you can obtain the actual macro
|
|
function object by calling `expander.isbound(id)`.
|
|
|
|
**NOTE**:
|
|
|
|
Stages in the life of a hygienically captured *macro* in `mcpyrate` are as follows.
|
|
Note that unlike `capture_value`, a call to `capture_macro` never appears in the AST.
|
|
|
|
1. When the surrounding `q` expands, it first expands any unquotes nested
|
|
within it, but only those where the quote level hits zero. The `h[]` is
|
|
converted into a `Capture` AST marker; see the `h` operator for details.
|
|
|
|
2. Then, still while the surrounding `q` expands, `q` compiles quasiquote
|
|
markers. A `Capture` marker for a macro, in particular, triggers an
|
|
immediate call to the function `capture_macro`. The result is an AST
|
|
representing a call to the function `lookup_macro`. This gets injected
|
|
into the AST produced by `q`.
|
|
|
|
3. When the use site of `q` reaches run time, the `lookup_macro` runs,
|
|
injecting the macro (under its unique name) into the expander's global
|
|
bindings table. The `lookup_macro` call replaces itself with an `ast.Name`
|
|
whose `id` attribute contains the unique name of the macro.
|
|
|
|
4. In typical usage, that use site of `q` is inside the implementation
|
|
of some user-defined macro. Upon further macro expansion of *that macro's
|
|
use site*, the expander finds the now-bound unique name of the macro, and
|
|
proceeds to expand that macro.
|
|
|
|
So in the macro expansion of `q`, we have a call to `lookup_macro`
|
|
representing the hygienically captured macro. But this disappears after
|
|
a very brief window of time, namely when the use site of `q` reaches run
|
|
time. Thus, this function likely has much fewer use cases than
|
|
`is_captured_value`, but is provided for completeness.
|
|
|
|
(The point of hygienic macro capture is that a macro can safely return a further
|
|
macro invocation, and guarantee that this will invoke the intended macro - without
|
|
requiring the user to import that other macro, and without being forced to expand
|
|
it away before returning from the original macro.)
|
|
"""
|
|
if type(tree) is not ast.Call:
|
|
return False
|
|
|
|
callee = tree.func
|
|
if not (type(callee) is ast.Attribute and callee.attr == "lookup_macro"):
|
|
return False
|
|
if not _is_mcpyrate_quotes_reference(callee.value):
|
|
return False
|
|
|
|
# This AST destructuring and constant extraction must match the format
|
|
# of the argument lists produced by the quasiquote system for calls to
|
|
# `lookup_macro`.
|
|
key_node = tree.args[0] # the call is `lookup_macro(key)`
|
|
name_node, unique_name_node, frozen_macro_node = key_node.elts
|
|
assert type(name_node) is ast.Constant and type(name_node.value) is str
|
|
assert type(unique_name_node) is ast.Constant and type(unique_name_node.value) is str
|
|
assert type(frozen_macro_node) is ast.Constant and type(frozen_macro_node.value) is bytes
|
|
return (name_node.value, unique_name_node.value, frozen_macro_node.value)
|
|
|
|
|
|
def _is_mcpyrate_quotes_reference(tree):
|
|
"""Detect whether `tree` is a reference to `mcpyrate.quotes`.
|
|
|
|
This matches the ASTs corresponding to:
|
|
- direct reference: `mcpyrate.quotes`
|
|
- reference by import: `__import__("mcpyrate.quotes", ...).quotes`
|
|
|
|
Note `__import__` of a dotted module name returns the top-level module,
|
|
so we have the name `quotes` appear twice in different places.
|
|
|
|
See `_mcpyrate_quotes_attr` and `mcpyrate.coreutils._mcpyrate_attr`.
|
|
"""
|
|
if not (type(tree) is ast.Attribute and tree.attr == "quotes"):
|
|
return False
|
|
moduleref = tree.value
|
|
if type(moduleref) is ast.Name and moduleref.id == "mcpyrate":
|
|
return "direct" # ok, direct reference
|
|
elif (type(moduleref) is ast.Call and type(moduleref.func) is ast.Name and
|
|
moduleref.func.id == "__import__" and type(moduleref.args[0]) is ast.Constant and
|
|
moduleref.args[0].value == "mcpyrate.quotes"):
|
|
return "import" # ok, reference by import
|
|
else:
|
|
return False
|
|
|
|
|
|
# --------------------------------------------------------------------------------
|
|
# The quasiquote compiler and uncompiler.
|
|
|
|
def astify(x, expander=None): # like `macropy`'s `ast_repr`
|
|
"""Quasiquote compiler. Lift a value into its AST representation, if possible.
|
|
|
|
When the AST is compiled and run, it will evaluate to `x`.
|
|
|
|
Note the above implies that if `x` itself is an AST, then this produces
|
|
an AST that, when compiled and run, will generate the AST `x`. This is
|
|
the mechanism that `q` uses to produce the quoted AST.
|
|
|
|
If the input is a `list` of ASTs (e.g. body of block mode `q`), the return value
|
|
is a single `ast.List` node, with its `elts` taken from the input list
|
|
(after recursing into each element).
|
|
|
|
`expander` is a `BaseMacroExpander` instance, used for detecting macros
|
|
inside `Capture` markers. Macros can be hygienically captured only if
|
|
an `expander` is provided.
|
|
|
|
Raises `TypeError` if the lifting fails.
|
|
"""
|
|
def recurse(x): # second layer just to auto-pass `expander` by closure.
|
|
T = type(x)
|
|
|
|
# Compile the unquote commands.
|
|
#
|
|
# Minimally, `astify` must support `ASTLiteral`; the others could be
|
|
# implemented inside the unquote operators, as `ASTLiteral(ast.Call(...), "expr")`.
|
|
# But maybe this approach is cleaner.
|
|
if T is Unquote: # `u[]`
|
|
# We want to generate an AST that compiles to the *value* of `x.body`,
|
|
# evaluated at the use site of `q`. But when the `q` expands, it is
|
|
# too early. We must `astify` *at the use site* of `q`. So use an
|
|
# `ast.Call` to delay until run-time, and pass in `x.body` as-is.
|
|
return ast.Call(_mcpyrate_quotes_attr("astify"), [x.body], [])
|
|
|
|
elif T is LiftSourcecode: # `n[]`
|
|
# Delay the identifier lifting, so it runs at the use site of `q`,
|
|
# where the actual value of `x.body` becomes available.
|
|
return ast.Call(_mcpyrate_quotes_attr("lift_sourcecode"),
|
|
[x.body,
|
|
ast.Constant(value=x.filename)],
|
|
[])
|
|
|
|
elif T is ASTLiteral: # `a`
|
|
# Pass through this subtree as-is, but apply a run-time typecheck,
|
|
# as well as some special run-time handling for `list`s of AST nodes.
|
|
return ast.Call(_mcpyrate_quotes_attr("ast_literal"),
|
|
[x.body,
|
|
ast.Constant(value=x.syntax)],
|
|
[])
|
|
|
|
elif T is ASTList: # `s[]`
|
|
return ast.Call(_mcpyrate_quotes_attr("ast_list"), [x.body], [])
|
|
|
|
elif T is ASTTuple: # `t[]`
|
|
return ast.Call(_mcpyrate_quotes_attr("ast_tuple"), [x.body], [])
|
|
|
|
elif T is Capture: # `h[]`
|
|
if expander and type(x.body) is ast.Name:
|
|
function = expander.isbound(x.body.id)
|
|
if function:
|
|
# Hygienically capture a macro. We do this immediately,
|
|
# during the expansion of `q`, because the value we want to
|
|
# store, i.e. the macro function, is available only at
|
|
# macro-expansion time.
|
|
#
|
|
# This allows macros in scope at the use site of `q` to be
|
|
# hygienically propagated out to the use site of the macro
|
|
# that used `q`. So you can write macros that `q[h[macroname][...]]`,
|
|
# and `macroname` doesn't have to be macro-imported wherever
|
|
# that code gets spliced in.
|
|
return capture_macro(function, x.body.id)
|
|
# Hygienically capture a garden variety run-time value.
|
|
# At the use site of q[], this captures the value, and rewrites itself
|
|
# into an AST that represents a lookup. At the use site of the macro
|
|
# that used q[], that code runs, and looks up the captured value.
|
|
return ast.Call(_mcpyrate_quotes_attr("capture_value"),
|
|
[x.body,
|
|
ast.Constant(value=x.name)],
|
|
[])
|
|
|
|
# Builtin types. Mainly support for `u[]`, but also used by the
|
|
# general case for AST node fields that contain bare values.
|
|
elif T in (int, float, str, bytes, bool, type(None), type(...)):
|
|
return ast.Constant(value=x)
|
|
|
|
elif T is list:
|
|
return ast.List(elts=list(recurse(elt) for elt in x))
|
|
elif T is tuple:
|
|
return ast.Tuple(elts=list(recurse(elt) for elt in x))
|
|
elif T is dict:
|
|
return ast.Dict(keys=list(recurse(k) for k in x.keys()),
|
|
values=list(recurse(v) for v in x.values()))
|
|
elif T is set:
|
|
return ast.Set(elts=list(recurse(elt) for elt in x))
|
|
|
|
# We must support at least the `Done` AST marker, so that things like
|
|
# coverage dummy nodes and expanded name macros can be astified.
|
|
# (Note we support only exactly `Done`, not arbitrary descendants.)
|
|
elif T is Done:
|
|
fields = [ast.keyword(a, recurse(b)) for a, b in ast.iter_fields(x)]
|
|
# We have imported `Done`, so we can refer to it as `mcpyrate.quotes.Done`.
|
|
node = ast.Call(_mcpyrate_quotes_attr("Done"),
|
|
[],
|
|
fields)
|
|
return node
|
|
|
|
# General case.
|
|
elif isinstance(x, ast.AST):
|
|
# TODO: Add support for astifying general ASTMarkers?
|
|
# Otherwise the same as regular AST node, but need to refer to the
|
|
# module it is defined in, and we don't have everything in scope here.
|
|
if isinstance(x, ASTMarker):
|
|
raise TypeError(f"Cannot astify internal AST markers, got {unparse(x)}")
|
|
|
|
# The magic is in the Call. Take apart the input AST, and construct a
|
|
# new AST, that (when compiled and run) will re-generate the input AST.
|
|
#
|
|
# We refer to the stdlib `ast` module as `mcpyrate.quotes.ast` to avoid
|
|
# name conflicts at the use site of `q[]`.
|
|
fields = [ast.keyword(a, recurse(b)) for a, b in ast.iter_fields(x)]
|
|
node = ast.Call(ast.Attribute(value=_mcpyrate_quotes_attr("ast"),
|
|
attr=x.__class__.__name__),
|
|
[],
|
|
fields)
|
|
# Copy source location info for correct coverage reporting of a quoted block.
|
|
#
|
|
# The location info we fill in here is for the use site of `q`, which is
|
|
# typically inside a macro definition. Coverage for a quoted line of code
|
|
# means that the expansion of the quote contains input from that line.
|
|
# It says nothing about the run-time behavior of that code.
|
|
#
|
|
# Running the AST produced by the quote re-produces the input AST, which is
|
|
# indeed the whole point of quoting stuff. The AST is re-produced **without
|
|
# any source location info**. The fact that *this* location info is missing,
|
|
# on purpose, is the magic that allows the missing location fixer to fill
|
|
# the correct location info at the final use site, i.e. the use site of the
|
|
# macro that used `q`.
|
|
node = ast.copy_location(node, x)
|
|
return node
|
|
|
|
raise TypeError(f"Don't know how to astify {repr(x)}")
|
|
return recurse(x)
|
|
|
|
|
|
def unastify(tree):
|
|
"""Quasiquote uncompiler. Approximate inverse of `astify`.
|
|
|
|
`tree` must have been produced by `astify`. Otherwise raises `TypeError`.
|
|
|
|
This turns an "astified" AST, that represents code to construct a run-time
|
|
AST value, back into a direct AST. So in a sense, `unastify` is a top-level
|
|
unquote operator.
|
|
|
|
Note subtle difference in meaning to `u[]`. The `u[]` operator interpolates
|
|
a value from outside the quote context into the quoted representation - so
|
|
that the value actually becomes quoted! - whereas `unastify` inverts the
|
|
quote operation.
|
|
|
|
Note also that `astify` compiles unquote AST markers into ASTs for calls to
|
|
the run-time parts of the unquote operators. `unastify` uncompiles those
|
|
calls back into the corresponding AST markers. That's the best we can do;
|
|
the only context that has the user-provided names (where the unquoted data
|
|
comes from) in scope is each particular use site of `q`, at its run time.
|
|
|
|
The use case of `unastify` is to transform a quoted AST at macro expansion
|
|
time when the extra AST layer added by `astify` is still present. The
|
|
recipe is `unastify`, process just like any AST, then quote again.
|
|
(`expands` and `expand1s` in `mcpyrate.metatools` are examples of this.)
|
|
|
|
If you just want to macro-expand a quoted AST in the REPL, see the `expand`
|
|
family of macros. Prefer the `r` variants; they expand at run time, so
|
|
you'll get the final AST with the actual unquoted values spliced in.
|
|
"""
|
|
# CAUTION: in `unastify`, we implement only what we minimally need.
|
|
our_module_globals = globals()
|
|
def lookup_thing(dotted_name):
|
|
if not dotted_name.startswith("mcpyrate.quotes"):
|
|
raise NotImplementedError(f"Don't know how to look up {repr(dotted_name)}")
|
|
path = dotted_name.split(".")
|
|
if not all(component.isidentifier() for component in path):
|
|
raise NotImplementedError(f"Dotted name {repr(dotted_name)} contains at least one non-identifier component")
|
|
if len(path) < 3:
|
|
raise NotImplementedError(f"Dotted name {repr(dotted_name)} has fewer than two dots (expected 'mcpyrate.quotes.something')")
|
|
name_of_thing = path[2]
|
|
thing = our_module_globals[name_of_thing]
|
|
if len(path) > 3:
|
|
for attrname in path[3:]:
|
|
thing = getattr(thing, attrname)
|
|
return thing
|
|
|
|
T = type(tree)
|
|
|
|
if T is ast.Constant:
|
|
return tree.value
|
|
|
|
# Support machinery for `Call` AST node. This serendipitously supports also
|
|
# *args and **kwargs, because as of Python 3.6 those appear in `args` and
|
|
# `keywords`, and `Starred` needs no special support here.
|
|
elif T is list:
|
|
return [unastify(elt) for elt in tree]
|
|
elif T is ast.keyword:
|
|
return tree.arg, unastify(tree.value)
|
|
|
|
elif T is ast.List:
|
|
return [unastify(elt) for elt in tree.elts]
|
|
elif T is ast.Tuple:
|
|
return tuple(unastify(elt) for elt in tree.elts)
|
|
elif T is ast.Dict:
|
|
return {unastify(k): unastify(v) for k, v in zip(tree.keys, tree.values)}
|
|
elif T is ast.Set:
|
|
return {unastify(elt) for elt in tree.elts}
|
|
|
|
elif T is ast.Call:
|
|
dotted_name = unparse(tree.func)
|
|
|
|
# Drop the run-time part of `q`, if present. This is added by `q` itself,
|
|
# not `astify`, but `unastify` is usually applied to the output of `q`.
|
|
if dotted_name == "mcpyrate.quotes.splice_ast_literals": # `q[]`
|
|
body = tree.args[0]
|
|
return unastify(body)
|
|
|
|
# Even though the unquote operators compile into calls, `unastify`
|
|
# must not apply their run-time parts, because it's running in the
|
|
# wrong context. Those only work properly at run time, and they
|
|
# must run at the use site of `q`, where the user-provided names
|
|
# (where the unquoted data comes from) will be in scope.
|
|
#
|
|
# So we undo what `astify` did, converting the unquote calls back into
|
|
# the corresponding AST markers.
|
|
elif dotted_name == "mcpyrate.quotes.astify": # `u[]`
|
|
body = tree.args[0]
|
|
return Unquote(body)
|
|
elif dotted_name == "mcpyrate.quotes.lift_sourcecode": # `n[]`
|
|
body, filename = tree.args[0], tree.args[1].value
|
|
return LiftSourcecode(body, filename)
|
|
elif dotted_name == "mcpyrate.quotes.ast_literal": # `a[]`
|
|
body, syntax = tree.args[0], tree.args[1].value
|
|
return ASTLiteral(body, syntax)
|
|
elif dotted_name == "mcpyrate.quotes.ast_list": # `s[]`
|
|
body = tree.args[0]
|
|
return ASTList(body)
|
|
elif dotted_name == "mcpyrate.quotes.ast_tuple": # `t[]`
|
|
body = tree.args[0]
|
|
return ASTTuple(body)
|
|
elif dotted_name == "mcpyrate.quotes.capture_value": # `h[]` (run-time value)
|
|
body, name = tree.args[0], tree.args[1].value
|
|
return Capture(body, name)
|
|
elif dotted_name == "mcpyrate.quotes.lookup_macro": # `h[]` (macro)
|
|
# `capture_macro` is done and gone by the time we get here.
|
|
# `astify` has generated an `ast.Call` to `lookup_macro`.
|
|
#
|
|
# To make the this work properly even across process boundaries,
|
|
# we cannot simply run the `lookup_macro`. It injects the binding
|
|
# once, and then becomes an inert lexical name (pointing to that
|
|
# binding) - so that strategy only works inside the same process.
|
|
#
|
|
# We can't just leave the `lookup_macro` call in the AST, either,
|
|
# since that doesn't make any sense when the tree is later sent
|
|
# to `astify` to compile it again (we don't want another `ast.Call`
|
|
# layer around it).
|
|
#
|
|
# So we need something that triggers `capture_macro` when the
|
|
# result is astified again.
|
|
#
|
|
# Hence, we uncompile the `lookup_macro` into a `Capture` marker.
|
|
#
|
|
# But if the astified tree comes from an earlier run (in another
|
|
# Python process), the original macro name might not be in the
|
|
# expander's bindings any more.
|
|
#
|
|
# So we inject the captured macro into the expander's global
|
|
# bindings table now (by calling `lookup_macro`), and make the
|
|
# uncompiled capture command capture that macro.
|
|
#
|
|
# This does make the rather mild assumption that our input tree
|
|
# will be astified again in the same Python process, in order for
|
|
# the uncompiled capture to succeed when `astify` compiles it.
|
|
key = tree.args[0]
|
|
assert type(key) is ast.Tuple
|
|
assert all(type(elt) is ast.Constant for elt in key.elts)
|
|
name, unique_name, frozen_macro = [elt.value for elt in key.elts]
|
|
uniquename_node = lookup_macro((name, unique_name, frozen_macro))
|
|
return Capture(uniquename_node, name)
|
|
|
|
else:
|
|
# General case: an astified AST node.
|
|
callee = lookup_thing(dotted_name)
|
|
args = unastify(tree.args)
|
|
kwargs = {k: v for k, v in unastify(tree.keywords)}
|
|
node = callee(*args, **kwargs)
|
|
node = ast.copy_location(node, tree)
|
|
return node
|
|
|
|
raise TypeError(f"Don't know how to unastify {unparse_with_fallbacks(tree, debug=True, color=True)}")
|
|
|
|
# --------------------------------------------------------------------------------
|
|
# Quasiquote macros
|
|
#
|
|
# These operators are named after Qu'nasth, the goddess of quasiquotes in high-tech-elven mythology.
|
|
|
|
_quotelevel = NestingLevelTracker()
|
|
|
|
def _expand_quasiquotes(tree, expander):
|
|
"""Expand quasiquote macros only."""
|
|
# Use a second expander instance, with different bindings. Copy only the
|
|
# bindings of the quasiquote macros from the main `expander`, accounting
|
|
# for possible as-imports. This second expander won't even see other macros,
|
|
# thus leaving them alone.
|
|
bindings = extract_bindings(expander.bindings, q, u, n, a, s, t, h)
|
|
return MacroExpander(bindings, expander.filename).visit(tree)
|
|
|
|
# TODO: maybe make the rest of this a method of `MacroExpander`, and only wrap with `QuasiquoteSearchDone` here?
|
|
def _replace_tree_in_macro_invocation(invocation, newtree):
|
|
"""Helper function for handling nested quasiquotes.
|
|
|
|
Output a new invocation of the same macro, but wrapped in `QuasiquoteSearchDone`,
|
|
and with the `tree` inside replaced by `newtree`.
|
|
|
|
`expr` and `block` modes are supported; this is autodetected from `invocation`.
|
|
"""
|
|
new_invocation = copy.copy(invocation)
|
|
if type(new_invocation) is ast.Subscript:
|
|
if sys.version_info >= (3, 9, 0): # Python 3.9+: no ast.Index wrapper
|
|
new_invocation.slice = newtree
|
|
else:
|
|
new_invocation.slice = copy.copy(invocation.slice)
|
|
new_invocation.slice.value = newtree
|
|
elif type(new_invocation) is ast.With:
|
|
new_invocation.body = newtree
|
|
else:
|
|
raise NotImplementedError
|
|
return QuasiquoteSearchDone(body=new_invocation)
|
|
|
|
|
|
def q(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr/block] quasiquote. Lift code into its AST representation."""
|
|
if syntax not in ("expr", "block"):
|
|
raise SyntaxError("`q` is an expr and block macro only")
|
|
with _quotelevel.changed_by(+1):
|
|
tree = _expand_quasiquotes(tree, expander) # expand any unquotes corresponding to this level first
|
|
if _quotelevel.value > 1: # nested inside an outer quote?
|
|
# TODO: Implications when in block mode and not the only context manager in the `with`?
|
|
# TODO: Probably doesn't work in that case. Document that `q`, when used,
|
|
# TODO: should be the only ctxmgr in that particular `with`.
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
|
|
tree = delete_markers(tree, cls=QuasiquoteSearchDone)
|
|
|
|
tree = astify(tree, expander) # Magic part of `q`. Supply `expander` for `h[macro]` detection.
|
|
# `astify` should compile the unquote command markers away, and `SpliceNodes`
|
|
# markers only spring into existence when the run-time part of `a` runs
|
|
# (for communication with the run-time part of the surrounding `q`).
|
|
# So at this point, there should be no quasiquote markers in `tree`.
|
|
try:
|
|
check_no_markers_remaining(tree, filename=expander.filename, cls=QuasiquoteMarker)
|
|
except MacroExpansionError as err:
|
|
raise RuntimeError("`q`: internal error in quasiquote system") from err
|
|
|
|
# `a` introduces the need to splice any interpolated `list`s of ASTs at
|
|
# run time into the surrounding context (which is only available to the
|
|
# surrounding `q`). Inject a handler for that.
|
|
#
|
|
# Block mode `a` always produces a `list` of statement AST nodes.
|
|
#
|
|
# For expression mode `a`, a `list` of expression AST nodes is valid
|
|
# e.g. in a function call argument position, to splice the list into
|
|
# positional arguments of the `Call`.
|
|
tree = ast.Call(_mcpyrate_quotes_attr("splice_ast_literals"),
|
|
[tree,
|
|
ast.Constant(value=expander.filename)],
|
|
[])
|
|
|
|
if syntax == "block":
|
|
# Generate AST to perform the assignment for `with q as quoted`.
|
|
target = kw["optional_vars"] # List, Tuple, Name
|
|
if target is None:
|
|
raise SyntaxError("`q` (block mode) requires an asname to receive the quoted code")
|
|
if type(target) is not ast.Name:
|
|
raise SyntaxError(f"`q` (block mode) expected a single asname, got {unparse(target)}")
|
|
# This `Assign` runs at the use site of `q`, it's not part of the
|
|
# quoted code block. The statement nodes are packed into a `List` node,
|
|
# because the original `tree` was a `list` of AST nodes (because block mode),
|
|
# and we ran it through `astify`.
|
|
tree = ast.Assign([target], tree)
|
|
return tree
|
|
|
|
|
|
def u(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr] unquote. Splice a simple value into a quasiquote.
|
|
|
|
The value is lifted into an AST that re-constructs that value.
|
|
"""
|
|
if syntax != "expr":
|
|
raise SyntaxError("`u` is an expr macro only")
|
|
if _quotelevel.value < 1:
|
|
raise SyntaxError("`u` encountered while quotelevel < 1")
|
|
with _quotelevel.changed_by(-1):
|
|
tree = expander.visit_recursively(tree)
|
|
if _quotelevel.value > 0:
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
return Unquote(tree)
|
|
|
|
|
|
def n(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr] name-unquote. Parse a string, as Python source code, into an AST.
|
|
|
|
With `n[]`, you can e.g. compute a name (e.g. by `mcpyrate.gensym`) for a
|
|
variable and then use that variable in quasiquoted code - also as an assignment
|
|
target. Things like `n[f"self.{x}"]` and `n[f"kitties[{j}].paws[{k}].claws"]`
|
|
are also valid.
|
|
|
|
The use case this operator was designed for is variable access (identifiers,
|
|
attributes, subscripts, in any syntactically allowed nested combination) with
|
|
computed names, but who knows what else can be done with it?
|
|
|
|
The correct `ctx` is filled in automatically by the macro expander later.
|
|
|
|
See also `n[]`'s sister, `a`.
|
|
|
|
Generalized from `macropy`'s `n`, which converts a string into a variable access.
|
|
"""
|
|
if syntax != "expr":
|
|
raise SyntaxError("`n` is an expr macro only")
|
|
if _quotelevel.value < 1:
|
|
raise SyntaxError("`n` encountered while quotelevel < 1")
|
|
with _quotelevel.changed_by(-1):
|
|
tree = expander.visit_recursively(tree)
|
|
if _quotelevel.value > 0:
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
return LiftSourcecode(tree, expander.filename)
|
|
|
|
|
|
def a(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr/block] ast-unquote. Splice an AST into a quasiquote.
|
|
|
|
**Expression mode**::
|
|
|
|
a[expr]
|
|
|
|
The expression `expr` must evaluate, at run time at the use site of the
|
|
surrounding `q`, to an *expression* AST node, an AST marker containing
|
|
an *expression* AST node in its `body` attribute, or in certain contexts
|
|
where that is valid in the AST, a `list` of such values.
|
|
|
|
Typically, `expr` is the name of a variable that holds such data, but
|
|
it doesn't have to be; any expression that evaluates to acceptable data
|
|
is fine.
|
|
|
|
An example of a context that accepts a `list` of expression nodes is the
|
|
positional arguments of a function call. `q[myfunc(a[args])]` will splice
|
|
the `list` `args` into the positional arguments of the `Call`. Of course,
|
|
ast-unquoting single positional arguments such as `q[myfunc(a[arg1], a[arg2])]`
|
|
is also fine.
|
|
|
|
**Block mode**::
|
|
|
|
with a:
|
|
stmts
|
|
...
|
|
|
|
Each `stmts` must evaluate, at run time at the use site of the surrounding `q`,
|
|
to a *statement* AST node, an AST marker containing a *statement* AST node in
|
|
its `body` attribute, or a `list` of such values.
|
|
|
|
Typically, `stmts`t is the name of a variable that holds such data, but
|
|
it doesn't have to be; any expression that evaluates to acceptable data
|
|
is fine.
|
|
|
|
This expands as if all those statements appeared in the `with` body,
|
|
in the order listed.
|
|
|
|
The `with` body must not contain anything else.
|
|
|
|
See also `a`'s sister, `n[]`.
|
|
"""
|
|
if syntax not in ("expr", "block"):
|
|
raise SyntaxError("`a` is an expr and block macro only")
|
|
if _quotelevel.value < 1:
|
|
raise SyntaxError("`a` encountered while quotelevel < 1")
|
|
if syntax == "block" and kw['optional_vars'] is not None:
|
|
raise SyntaxError("`a` (block mode) does not take an asname")
|
|
|
|
with _quotelevel.changed_by(-1):
|
|
tree = expander.visit_recursively(tree)
|
|
if _quotelevel.value > 0:
|
|
# TODO: implications for block mode?
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
|
|
if syntax == "expr":
|
|
return ASTLiteral(tree, syntax)
|
|
|
|
assert syntax == "block"
|
|
# Block mode: strip `Expr` wrappers.
|
|
#
|
|
# When `with a` expands, the elements of the list `tree` are `Expr` nodes
|
|
# containing expressions. Typically each expression is just a `Name`
|
|
# node, or in general, any expression that at run time evaluates to
|
|
# a statement AST node, or to an iterable of statement AST nodes.
|
|
#
|
|
# We want to return, as an AST, a list of those expressions for processing
|
|
# later.
|
|
#
|
|
# The value of the expressions become available when the use site of
|
|
# `q` reaches run time. Because each expression may refer to a list of
|
|
# AST nodes, `q` injects a call to a postprocessor that, at run time
|
|
# (once the values are available), will flatten the quoted AST structure.
|
|
out = []
|
|
for stmt in tree:
|
|
if type(stmt) is not ast.Expr:
|
|
raise SyntaxError("`a` (block mode): each item in the body must be an expression (that at run time evaluates to a statement node or iterable of statement nodes)")
|
|
out.append(stmt.value)
|
|
return ASTLiteral(ast.List(elts=out), syntax)
|
|
|
|
|
|
def s(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr] ast-list-unquote. Splice an iterable of ASTs, as an `ast.List`, into a quasiquote."""
|
|
if syntax != "expr":
|
|
raise SyntaxError("`s` is an expr macro only")
|
|
if _quotelevel.value < 1:
|
|
raise SyntaxError("`s` encountered while quotelevel < 1")
|
|
with _quotelevel.changed_by(-1):
|
|
tree = expander.visit_recursively(tree)
|
|
if _quotelevel.value > 0:
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
return ASTList(tree)
|
|
|
|
|
|
def t(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr] ast-tuple-unquote. Splice an iterable of ASTs, as an `ast.Tuple`, into a quasiquote."""
|
|
if syntax != "expr":
|
|
raise SyntaxError("`t` is an expr macro only")
|
|
if _quotelevel.value < 1:
|
|
raise SyntaxError("`t` encountered while quotelevel < 1")
|
|
with _quotelevel.changed_by(-1):
|
|
tree = expander.visit_recursively(tree)
|
|
if _quotelevel.value > 0:
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
return ASTTuple(tree)
|
|
|
|
|
|
def h(tree, *, syntax, expander, invocation, **kw):
|
|
"""[syntax, expr] hygienic-unquote. Splice any value, from the macro definition site, into a quasiquote.
|
|
|
|
Supports also values that have no meaningful `repr`. The value is captured
|
|
at the use site of the surrounding `q`.
|
|
|
|
The value is frozen into the expanded source code as a pickled blob,
|
|
separately at each use site of `h[]`. Thus the value must be picklable,
|
|
and each capture will pickle it again.
|
|
|
|
This is done to ensure the value will remain available, when the
|
|
already-expanded code (due to `.pyc` caching) runs again in another
|
|
Python process. (In other words, values from "macro expansion time
|
|
last week" would not otherwise be available.)
|
|
|
|
Supports also macros. To hygienically splice a macro invocation,
|
|
`h[]` only the macro name.
|
|
"""
|
|
if syntax != "expr":
|
|
raise SyntaxError("`h` is an expr macro only")
|
|
if _quotelevel.value < 1:
|
|
raise SyntaxError("`h` encountered while quotelevel < 1")
|
|
|
|
with _quotelevel.changed_by(-1):
|
|
name = unparse(tree)
|
|
|
|
# TODO: This logic never does anything - any correctly placed `h[]`
|
|
# is always inside a `q`, which recurses with an expander that
|
|
# only knows about the quasiquote macros.
|
|
|
|
# Expand macros in the unquoted expression. The only case we need to
|
|
# look out for is a `@namemacro` if we have a `h[macroname]`. We're
|
|
# only capturing it, so don't expand it just yet.
|
|
expand = True
|
|
if type(tree) is ast.Name:
|
|
function = expander.isbound(tree.id)
|
|
if function and isnamemacro(function):
|
|
expand = False
|
|
if expand:
|
|
tree = expander.visit_recursively(tree)
|
|
|
|
if _quotelevel.value > 0:
|
|
return _replace_tree_in_macro_invocation(invocation, tree)
|
|
|
|
return Capture(tree, name)
|