Produce Python AST for `require` statements and skip self `require`s

Closes hylang/hy#1211.
This commit is contained in:
Brandon T. Willard 2018-10-23 14:41:20 -04:00
parent 58003389c5
commit 144a7fa240
15 changed files with 491 additions and 212 deletions

View File

@ -13,6 +13,7 @@ import io
import importlib
import py_compile
import runpy
import types
import astor.code_gen
@ -47,10 +48,26 @@ builtins.quit = HyQuitter('quit')
builtins.exit = HyQuitter('exit')
class HyREPL(code.InteractiveConsole):
class HyREPL(code.InteractiveConsole, object):
def __init__(self, spy=False, output_fn=None, locals=None,
filename="<input>"):
super(HyREPL, self).__init__(locals=locals,
filename=filename)
# Create a proper module for this REPL so that we can obtain it easily
# (e.g. using `importlib.import_module`).
# Also, make sure it's properly introduced to `sys.modules` and
# consistently use its namespace as `locals` from here on.
module_name = self.locals.get('__name__', '__console__')
self.module = sys.modules.setdefault(module_name,
types.ModuleType(module_name))
self.module.__dict__.update(self.locals)
self.locals = self.module.__dict__
# Load cmdline-specific macros.
require('hy.cmdline', module_name, assignments='ALL')
self.spy = spy
if output_fn is None:
@ -65,9 +82,6 @@ class HyREPL(code.InteractiveConsole):
else:
self.output_fn = __builtins__[mangle(output_fn)]
code.InteractiveConsole.__init__(self, locals=locals,
filename=filename)
# Pre-mangle symbols for repl recent results: *1, *2, *3
self._repl_results_symbols = [mangle("*{}".format(i + 1)) for i in range(3)]
self.locals.update({sym: None for sym in self._repl_results_symbols})
@ -102,8 +116,7 @@ class HyREPL(code.InteractiveConsole):
new_ast = ast.Module(main_ast.body +
[ast.Expr(expr_ast.body)])
print(astor.to_source(new_ast))
value = hy_eval(do, self.locals, "__console__",
ast_callback)
value = hy_eval(do, self.locals, self.module, ast_callback)
except HyTypeError as e:
if e.source is None:
e.source = source
@ -181,8 +194,6 @@ def ideas_macro(ETname):
""")])
require("hy.cmdline", "__console__", assignments="ALL")
require("hy.cmdline", "__main__", assignments="ALL")
SIMPLE_TRACEBACKS = True
@ -199,7 +210,8 @@ def pretty_error(func, *args, **kw):
def run_command(source):
tree = hy_parse(source)
pretty_error(hy_eval, tree, module_name="__main__")
require("hy.cmdline", "__main__", assignments="ALL")
pretty_error(hy_eval, tree, None, importlib.import_module('__main__'))
return 0
@ -208,13 +220,13 @@ def run_repl(hr=None, **kwargs):
sys.ps1 = "=> "
sys.ps2 = "... "
namespace = {'__name__': '__console__', '__doc__': ''}
if not hr:
hr = HyREPL(**kwargs)
namespace = hr.locals
with completion(Completer(namespace)):
if not hr:
hr = HyREPL(locals=namespace, **kwargs)
hr.interact("{appname} {version} using "
"{py}({build}) {pyversion} on {os}".format(
appname=hy.__appname__,
@ -409,7 +421,6 @@ def hyc_main():
# entry point for cmd line script "hy2py"
def hy2py_main():
import platform
module_name = "<STDIN>"
options = dict(prog="hy2py", usage="%(prog)s [options] [FILE]",
formatter_class=argparse.RawDescriptionHelpFormatter)
@ -448,7 +459,7 @@ def hy2py_main():
print()
print()
_ast = pretty_error(hy_compile, hst, module_name)
_ast = pretty_error(hy_compile, hst, '__main__')
if options.with_ast:
if PY3 and platform.system() == "Windows":
_print_for_windows(astor.dump_tree(_ast))

View File

@ -13,14 +13,16 @@ from hy.errors import HyCompileError, HyTypeError
from hy.lex import mangle, unmangle
import hy.macros
from hy._compat import (
str_type, bytes_type, long_type, PY3, PY35, raise_empty)
from hy.macros import require, macroexpand, tag_macroexpand
from hy._compat import (str_type, string_types, bytes_type, long_type, PY3,
PY35, raise_empty)
from hy.macros import require, load_macros, macroexpand, tag_macroexpand
import hy.importer
import traceback
import importlib
import inspect
import pkgutil
import types
import ast
import sys
import copy
@ -283,28 +285,45 @@ _stdlib = {}
class HyASTCompiler(object):
"""A Hy-to-Python AST compiler"""
def __init__(self, module_name):
def __init__(self, module):
"""
Parameters
----------
module: str or types.ModuleType
Module in which the Hy tree is evaluated.
"""
self.anon_var_count = 0
self.imports = defaultdict(set)
self.module_name = module_name
self.temp_if = None
if not inspect.ismodule(module):
module = importlib.import_module(module)
self.module = module
self.module_name = module.__name__
self.can_use_stdlib = (
not module_name.startswith("hy.core")
or module_name == "hy.core.macros")
not self.module_name.startswith("hy.core")
or self.module_name == "hy.core.macros")
# Load stdlib macros into the module namespace.
load_macros(self.module)
# Everything in core needs to be explicit (except for
# the core macros, which are built with the core functions).
if self.can_use_stdlib and not _stdlib:
# Populate _stdlib.
import hy.core
for module in hy.core.STDLIB:
mod = importlib.import_module(module)
for e in map(ast_str, mod.EXPORTS):
for stdlib_module in hy.core.STDLIB:
mod = importlib.import_module(stdlib_module)
for e in map(ast_str, getattr(mod, 'EXPORTS', [])):
if getattr(mod, e) is not getattr(builtins, e, ''):
# Don't bother putting a name in _stdlib if it
# points to a builtin with the same name. This
# prevents pointless imports.
_stdlib[e] = module
_stdlib[e] = stdlib_module
def get_anon_var(self):
self.anon_var_count += 1
@ -1098,11 +1117,6 @@ class HyASTCompiler(object):
brackets(SYM, sym(":as"), _symn) |
brackets(SYM, brackets(many(_symn + maybe(sym(":as") + _symn)))))])
def compile_import_or_require(self, expr, root, entries):
"""
TODO for `require`: keep track of what we've imported in this run and
then "unimport" it after we've completed `thing' so that we don't
pollute other envs.
"""
ret = Result()
for entry in entries:
@ -1128,8 +1142,9 @@ class HyASTCompiler(object):
else:
assignments = [(k, v or k) for k, v in kids]
ast_module = ast_str(module, piecewise=True)
if root == "import":
ast_module = ast_str(module, piecewise=True)
module = ast_module.lstrip(".")
level = len(ast_module) - len(module)
if assignments == "ALL" and prefix == "":
@ -1150,10 +1165,23 @@ class HyASTCompiler(object):
for k, v in assignments]
ret += node(
expr, module=module or None, names=names, level=level)
else: # root == "require"
importlib.import_module(module)
require(module, self.module_name,
assignments=assignments, prefix=prefix)
elif require(ast_module, self.module, assignments=assignments,
prefix=prefix):
# Actually calling `require` is necessary for macro expansions
# occurring during compilation.
self.imports['hy.macros'].update([None])
# The `require` we're creating in AST is the same as above, but used at
# run-time (e.g. when modules are loaded via bytecode).
ret += self.compile(HyExpression([
HySymbol('hy.macros.require'),
HyString(ast_module),
HySymbol('None'),
HyKeyword('assignments'),
(HyString("ALL") if assignments == "ALL" else
[[HyString(k), HyString(v)] for k, v in assignments]),
HyKeyword('prefix'),
HyString(prefix)]).replace(expr))
return ret
@ -1484,7 +1512,8 @@ class HyASTCompiler(object):
[x for pair in attrs[0] for x in pair]).replace(attrs)))
for e in body:
e = self.compile(self._rewire_init(macroexpand(e, self)))
e = self.compile(self._rewire_init(
macroexpand(e, self.module, self)))
bodyr += e + e.expr_as_stmt()
return bases + asty.ClassDef(
@ -1520,20 +1549,16 @@ class HyASTCompiler(object):
return self.compile(tag_macroexpand(
HyString(mangle(tag)).replace(tag),
arg,
self))
_namespaces = {}
self.module))
@special(["eval-and-compile", "eval-when-compile"], [many(FORM)])
def compile_eval_and_compile(self, expr, root, body):
new_expr = HyExpression([HySymbol("do").replace(expr[0])]).replace(expr)
if self.module_name not in self._namespaces:
# Initialize a compile-time namespace for this module.
self._namespaces[self.module_name] = {
'hy': hy, '__name__': self.module_name}
hy.importer.hy_eval(new_expr + body,
self._namespaces[self.module_name],
self.module_name)
self.module.__dict__,
self.module)
return (self._compile_branch(body)
if ast_str(root) == "eval_and_compile"
else Result())
@ -1541,7 +1566,7 @@ class HyASTCompiler(object):
@builds_model(HyExpression)
def compile_expression(self, expr):
# Perform macro expansions
expr = macroexpand(expr, self)
expr = macroexpand(expr, self.module, self)
if not isinstance(expr, HyExpression):
# Go through compile again if the type changed.
return self.compile(expr)
@ -1699,20 +1724,41 @@ class HyASTCompiler(object):
return ret + asty.Dict(m, keys=keyvalues[::2], values=keyvalues[1::2])
def hy_compile(tree, module_name, root=ast.Module, get_expr=False):
def hy_compile(tree, module, root=ast.Module, get_expr=False):
"""
Compile a HyObject tree into a Python AST Module.
Compile a Hy tree into a Python AST tree.
If `get_expr` is True, return a tuple (module, last_expression), where
`last_expression` is the.
Parameters
----------
module: str or types.ModuleType
Module, or name of the module, in which the Hy tree is evaluated.
root: ast object, optional (ast.Module)
Root object for the Python AST tree.
get_expr: bool, optional (False)
If true, return a tuple with `(root_obj, last_expression)`.
Returns
-------
out : A Python AST tree
"""
if isinstance(module, string_types):
if module.startswith('<') and module.endswith('>'):
module = types.ModuleType(module)
else:
module = importlib.import_module(ast_str(module, piecewise=True))
if not inspect.ismodule(module):
raise TypeError('Invalid module type: {}'.format(type(module)))
tree = wrap_value(tree)
if not isinstance(tree, HyObject):
raise HyCompileError("`tree` must be a HyObject or capable of "
"being promoted to one")
compiler = HyASTCompiler(module_name)
compiler = HyASTCompiler(module)
result = compiler.compile(tree)
expr = result.force_expr

View File

@ -39,13 +39,15 @@ class Completer(object):
self.namespace = namespace
self.path = [hy.compiler._special_form_compilers,
builtins.__dict__,
hy.macros._hy_macros[None],
namespace]
self.tag_path = [hy.macros._hy_tag[None]]
if '__name__' in namespace:
module_name = namespace['__name__']
self.path.append(hy.macros._hy_macros[module_name])
self.tag_path.append(hy.macros._hy_tag[module_name])
self.tag_path = []
namespace.setdefault('__macros__', {})
namespace.setdefault('__tags__', {})
self.path.append(namespace['__macros__'])
self.tag_path.append(namespace['__tags__'])
def attr_matches(self, text):
# Borrowed from IPython's completer

View File

@ -5,6 +5,7 @@
(import [hy [HyExpression HyDict]]
[functools [partial]]
[importlib [import-module]]
[collections [OrderedDict]]
[hy.macros [macroexpand :as mexpand]]
[hy.compiler [HyASTCompiler]])
@ -42,9 +43,11 @@
(defn macroexpand-all [form &optional module-name]
"Recursively performs all possible macroexpansions in form."
(setv module-name (or module-name (calling-module-name))
(setv module (or (and module-name
(import-module module-name))
(calling-module))
quote-level [0]
ast-compiler (HyASTCompiler module-name)) ; TODO: make nonlocal after dropping Python2
ast-compiler (HyASTCompiler module)) ; TODO: make nonlocal after dropping Python2
(defn traverse [form]
(walk expand identity form))
(defn expand [form]
@ -68,7 +71,7 @@
[(= (first form) (HySymbol "require"))
(ast-compiler.compile form)
(return)]
[True (traverse (mexpand form ast-compiler))])
[True (traverse (mexpand form module ast-compiler))])
(if (coll? form)
(traverse form)
form)))

View File

@ -21,7 +21,7 @@
(import [hy.models [HySymbol HyKeyword]])
(import [hy.lex [LexException PrematureEndOfInput tokenize mangle unmangle]])
(import [hy.compiler [HyASTCompiler]])
(import [hy.importer [hy-eval :as eval]])
(import [hy.importer [calling-module hy-eval :as eval]])
(defn butlast [coll]
"Return an iterator of all but the last item in `coll`."
@ -295,12 +295,14 @@ Return series of accumulated sums (or other binary function results)."
(defn macroexpand [form]
"Return the full macro expansion of `form`."
(import hy.macros)
(hy.macros.macroexpand form (HyASTCompiler (calling-module-name))))
(setv module (calling-module))
(hy.macros.macroexpand form module (HyASTCompiler module)))
(defn macroexpand-1 [form]
"Return the single step macro expansion of `form`."
(import hy.macros)
(hy.macros.macroexpand-1 form (HyASTCompiler (calling-module-name))))
(setv module (calling-module))
(hy.macros.macroexpand-1 form module (HyASTCompiler module)))
(defn merge-with [f &rest maps]
"Return the map of `maps` joined onto the first via the function `f`.
@ -467,8 +469,8 @@ Even objects with the __name__ magic will work."
(or a b)))
(setv EXPORTS
'[*map accumulate butlast calling-module-name chain coll? combinations
comp complement compress constantly count cycle dec distinct
'[*map accumulate butlast calling-module calling-module-name chain coll?
combinations comp complement compress constantly count cycle dec distinct
disassemble drop drop-last drop-while empty? eval even? every? exec first
filter flatten float? fraction gensym group-by identity inc input instance?
integer integer? integer-char? interleave interpose islice iterable?

View File

@ -245,34 +245,10 @@ Such 'o!' params are available within `body` as the equivalent 'g!' symbol."
Use ``#doc foo`` instead for help with tag macro ``#foo``.
Use ``(help foo)`` instead for help with runtime objects."
`(try
(import [importlib [import-module]])
(help (. (import-module "hy")
macros
_hy_macros
[__name__]
['~symbol]))
(except [KeyError]
(help (. (import-module "hy")
macros
_hy_macros
[None]
['~symbol])))))
`(help (.get __macros__ '~symbol None)))
(deftag doc [symbol]
"tag macro documentation
Gets help for a tag macro function available in this module."
`(try
(import [importlib [import-module]])
(help (. (import-module "hy")
macros
_hy_tag
[__name__]
['~symbol]))
(except [KeyError]
(help (. (import-module "hy")
macros
_hy_tag
[None]
['~symbol])))))
`(help (.get __tags__ '~symbol None)))

View File

@ -16,7 +16,7 @@
(setv _cache (frozenset (map unmangle (+
hy.core.language.EXPORTS
hy.core.shadow.EXPORTS
(list (.keys (get hy.macros._hy_macros None)))
(list (.keys hy.core.macros.__macros__))
keyword.kwlist
(list (.keys hy.compiler._special_form_compilers))
(list hy.compiler._bad_roots)))))))

View File

@ -17,9 +17,10 @@ import importlib
import __future__
from functools import partial
from contextlib import contextmanager
from hy.errors import HyTypeError
from hy.compiler import hy_compile
from hy.compiler import hy_compile, ast_str
from hy.lex import tokenize, LexException
from hy.models import HyExpression, HySymbol
from hy._compat import string_types, PY3
@ -29,6 +30,36 @@ hy_ast_compile_flags = (__future__.CO_FUTURE_DIVISION |
__future__.CO_FUTURE_PRINT_FUNCTION)
def calling_module(n=1):
"""Get the module calling, if available.
As a fallback, this will import a module using the calling frame's
globals value of `__name__`.
Parameters
----------
n: int, optional
The number of levels up the stack from this function call.
The default is one level up.
Returns
-------
out: types.ModuleType
The module at stack level `n + 1` or `None`.
"""
frame_up = inspect.stack(0)[n + 1][0]
module = inspect.getmodule(frame_up)
if module is None:
# This works for modules like `__main__`
module_name = frame_up.f_globals.get('__name__', None)
if module_name:
try:
module = importlib.import_module(module_name)
except ImportError:
pass
return module
def ast_compile(ast, filename, mode):
"""Compile AST.
@ -65,13 +96,9 @@ def hy_parse(source):
return HyExpression([HySymbol("do")] + tokenize(source + "\n"))
def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
def hy_eval(hytree, locals=None, module=None, ast_callback=None):
"""Evaluates a quoted expression and returns the value.
The optional second and third arguments specify the dictionary of globals
to use and the module name. The globals dictionary defaults to ``(local)``
and the module name defaults to the name of the current module.
Examples
--------
@ -89,13 +116,15 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
hytree: a Hy expression tree
Source code to parse.
namespace: dict, optional
Namespace in which to evaluate the Hy tree. Defaults to the calling
frame.
locals: dict, optional
Local environment in which to evaluate the Hy tree. Defaults to the
calling frame.
module_name: str, optional
Name of the module to which the Hy tree is assigned. Defaults to
the calling frame's module, if any, and '__eval__' otherwise.
module: str or types.ModuleType, optional
Module, or name of the module, to which the Hy tree is assigned and
the global values are taken.
Defaults to the calling frame's module, if any, and '__eval__'
otherwise.
ast_callback: callable, optional
A callback that is passed the Hy compiled tree and resulting
@ -105,19 +134,23 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
Returns
-------
out : Result of evaluating the Hy compiled tree.
"""
if namespace is None:
if module is None:
module = calling_module()
if isinstance(module, string_types):
module = importlib.import_module(ast_str(module, piecewise=True))
elif not inspect.ismodule(module):
raise TypeError('Invalid module type: {}'.format(type(module)))
if locals is None:
frame = inspect.stack()[1][0]
namespace = inspect.getargvalues(frame).locals
if module_name is None:
m = inspect.getmodule(inspect.stack()[1][0])
module_name = '__eval__' if m is None else m.__name__
locals = inspect.getargvalues(frame).locals
if not isinstance(module_name, string_types):
raise TypeError("Module name must be a string")
if not isinstance(locals, dict):
raise TypeError("Locals must be a dictionary")
_ast, expr = hy_compile(hytree, module_name, get_expr=True)
_ast, expr = hy_compile(hytree, module, get_expr=True)
# Spoof the positions in the generated ast...
for node in ast.walk(_ast):
@ -131,14 +164,13 @@ def hy_eval(hytree, namespace=None, module_name=None, ast_callback=None):
if ast_callback:
ast_callback(_ast, expr)
if not isinstance(namespace, dict):
raise TypeError("Globals must be a dictionary")
globals = module.__dict__
# Two-step eval: eval() the body of the exec call
eval(ast_compile(_ast, "<eval_body>", "exec"), namespace)
eval(ast_compile(_ast, "<eval_body>", "exec"), globals, locals)
# Then eval the expression context and return that
return eval(ast_compile(expr, "<eval>", "eval"), namespace)
return eval(ast_compile(expr, "<eval>", "eval"), globals, locals)
def cache_from_source(source_path):
@ -167,6 +199,52 @@ def cache_from_source(source_path):
return os.path.join(d, re.sub(r"(?:\.[^.]+)?\Z", ".pyc", f))
@contextmanager
def loader_module_obj(loader):
"""Use the module object associated with a loader.
This is intended to be used by a loader object itself, and primarily as a
work-around for attempts to get module and/or file code from a loader
without actually creating a module object. Since Hy currently needs the
module object for macro importing, expansion, and whatnot, using this will
reconcile Hy with such attempts.
For example, if we're first compiling a Hy script starting from
`runpy.run_path`, the Hy compiler will need a valid module object in which
to run, but, given the way `runpy.run_path` works, there might not be one
yet (e.g. `__main__` for a .hy file). We compensate by properly loading
the module here.
The function `inspect.getmodule` has a hidden-ish feature that returns
modules using their associated filenames (via `inspect.modulesbyfile`),
and, since the Loaders (and their delegate Loaders) carry a filename/path
associated with the parent package, we use it as a more robust attempt to
obtain an existing module object.
When no module object is found, a temporary, minimally sufficient module
object is created for the duration of the `with` body.
"""
tmp_mod = False
try:
module = inspect.getmodule(None, _filename=loader.path)
except KeyError:
module = None
if module is None:
tmp_mod = True
module = sys.modules.setdefault(loader.name,
types.ModuleType(loader.name))
module.__file__ = loader.path
module.__name__ = loader.name
try:
yield module
finally:
if tmp_mod:
del sys.modules[loader.name]
def _hy_code_from_file(filename, loader_type=None):
"""Use PEP-302 loader to produce code for a given Hy source file."""
full_fname = os.path.abspath(filename)
@ -226,7 +304,8 @@ if PY3:
source = data.decode("utf-8")
try:
hy_tree = hy_parse(source)
data = hy_compile(hy_tree, self.name)
with loader_module_obj(self) as module:
data = hy_compile(hy_tree, module)
except (HyTypeError, LexException) as e:
if e.source is None:
e.source = source
@ -276,6 +355,15 @@ else:
super(HyLoader, self).__init__(fullname, fileobj, filename, etc)
def __getattr__(self, item):
# We add these for Python >= 3.4 Loader interface compatibility.
if item == 'path':
return self.filename
elif item == 'name':
return self.fullname
else:
return super(HyLoader, self).__getattr__(item)
def exec_module(self, module, fullname=None):
fullname = self._fix_name(fullname)
code = self.get_code(fullname)
@ -283,7 +371,7 @@ else:
def load_module(self, fullname=None):
"""Same as `pkgutil.ImpLoader`, with an extra check for Hy
source"""
source and the option to not run `self.exec_module`."""
fullname = self._fix_name(fullname)
ext_type = self.etc[0]
mod_type = self.etc[2]
@ -298,7 +386,7 @@ else:
mod = sys.modules[fullname]
else:
mod = sys.modules.setdefault(
fullname, imp.new_module(fullname))
fullname, types.ModuleType(fullname))
# TODO: Should we set these only when not in `sys.modules`?
if mod_type == imp.PKG_DIRECTORY:
@ -351,7 +439,8 @@ else:
try:
hy_source = self.get_source(fullname)
hy_tree = hy_parse(hy_source)
hy_ast = hy_compile(hy_tree, fullname)
with loader_module_obj(self) as module:
hy_ast = hy_compile(hy_tree, module)
code = compile(hy_ast, self.filename, 'exec',
hy_ast_compile_flags)
@ -363,7 +452,7 @@ else:
if not sys.dont_write_bytecode:
try:
hyc_compile(code)
hyc_compile(code, module=fullname)
except IOError:
pass
return code
@ -470,7 +559,8 @@ else:
_py_compile_compile = py_compile.compile
def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False):
def hyc_compile(file_or_code, cfile=None, dfile=None, doraise=False,
module=None):
"""Write a Hy file, or code object, to pyc.
This is a patched version of Python 2.7's `py_compile.compile`.
@ -489,6 +579,9 @@ else:
The filename to use for compile-time errors.
doraise : bool, default False
If `True` raise compilation exceptions; otherwise, ignore them.
module : str or types.ModuleType, optional
The module, or module name, in which the Hy tree is expanded.
Default is the caller's module.
Returns
-------
@ -510,7 +603,13 @@ else:
flags = None
if _could_be_hy_src(filename):
hy_tree = hy_parse(source_str)
source = hy_compile(hy_tree, '<hyc_compile>')
if module is None:
module = inspect.getmodule(inspect.stack()[1][0])
elif not inspect.ismodule(module):
module = importlib.import_module(module)
source = hy_compile(hy_tree, module)
flags = hy_ast_compile_flags
codeobject = compile(source, dfile or filename, 'exec', flags)

View File

@ -1,15 +1,13 @@
# Copyright 2018 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
import inspect
import importlib
import inspect
import pkgutil
from collections import defaultdict
from hy._compat import PY3
from hy._compat import PY3, string_types
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle
from hy._compat import str_type
from hy.errors import HyTypeError, HyMacroExpansionError
@ -44,21 +42,9 @@ EXTRA_MACROS = [
"hy.core.macros",
]
_hy_macros = defaultdict(dict)
_hy_tag = defaultdict(dict)
def macro(name):
"""Decorator to define a macro called `name`.
This stores the macro `name` in the namespace for the module where it is
defined.
If the module where it is defined is in `hy.core`, then the macro is stored
in the default `None` namespace.
This function is called from the `defmacro` special form in the compiler.
"""
name = mangle(name)
def _(fn):
@ -70,88 +56,190 @@ def macro(name):
# names that are invalid in Python.
fn._hy_macro_pass_compiler = False
module_name = fn.__module__
if module_name.startswith("hy.core"):
module_name = None
_hy_macros[module_name][name] = fn
module = inspect.getmodule(fn)
module_macros = module.__dict__.setdefault('__macros__', {})
module_macros[name] = fn
return fn
return _
def tag(name):
"""Decorator to define a tag macro called `name`.
This stores the macro `name` in the namespace for the module where it is
defined.
If the module where it is defined is in `hy.core`, then the macro is stored
in the default `None` namespace.
This function is called from the `deftag` special form in the compiler.
"""
def _(fn):
_name = mangle('#{}'.format(name))
if not PY3:
_name = _name.encode('UTF-8')
fn.__name__ = _name
module_name = fn.__module__
module = inspect.getmodule(fn)
module_name = module.__name__
if module_name.startswith("hy.core"):
module_name = None
_hy_tag[module_name][mangle(name)] = fn
module_tags = module.__dict__.setdefault('__tags__', {})
module_tags[mangle(name)] = fn
return fn
return _
def _same_modules(source_module, target_module):
"""Compare the filenames associated with the given modules names.
This tries to not actually load the modules.
"""
if not (source_module or target_module):
return False
if target_module == source_module:
return True
def _get_filename(module):
filename = None
try:
if not inspect.ismodule(module):
loader = pkgutil.get_loader(module)
if loader:
filename = loader.get_filename()
else:
filename = inspect.getfile(module)
except (TypeError, ImportError):
pass
return filename
source_filename = _get_filename(source_module)
target_filename = _get_filename(target_module)
return (source_filename and target_filename and
source_filename == target_filename)
def require(source_module, target_module, assignments, prefix=""):
"""Load macros from `source_module` in the namespace of
`target_module`. `assignments` maps old names to new names, or
should be the string "ALL". If `prefix` is nonempty, it is
prepended to the name of each imported macro. (This means you get
macros named things like "mymacromodule.mymacro", which looks like
an attribute of a module, although it's actually just a symbol
with a period in its name.)
"""Load macros from one module into the namespace of another.
This function is called from the `require` special form in the compiler.
Parameters
----------
source_module: str or types.ModuleType
The module from which macros are to be imported.
target_module: str, types.ModuleType or None
The module into which the macros will be loaded. If `None`, then
the caller's namespace.
The latter is useful during evaluation of generated AST/bytecode.
assignments: str or list of tuples of strs
The string "ALL" or a list of macro name and alias pairs.
prefix: str, optional ("")
If nonempty, its value is prepended to the name of each imported macro.
This allows one to emulate namespaced macros, like
"mymacromodule.mymacro", which looks like an attribute of a module.
Returns
-------
out: boolean
Whether or not macros and tags were actually transferred.
"""
seen_names = set()
if target_module is None:
parent_frame = inspect.stack()[1][0]
target_namespace = parent_frame.f_globals
target_module = target_namespace.get('__name__', None)
elif isinstance(target_module, string_types):
target_module = importlib.import_module(target_module)
target_namespace = target_module.__dict__
elif inspect.ismodule(target_module):
target_namespace = target_module.__dict__
else:
raise TypeError('`target_module` is not a recognized type: {}'.format(
type(target_module)))
# Let's do a quick check to make sure the source module isn't actually
# the module being compiled (e.g. when `runpy` executes a module's code
# in `__main__`).
# We use the module's underlying filename for this (when they exist), since
# it's the most "fixed" attribute.
if _same_modules(source_module, target_module):
return False
if not inspect.ismodule(source_module):
source_module = importlib.import_module(source_module)
source_macros = source_module.__dict__.setdefault('__macros__', {})
source_tags = source_module.__dict__.setdefault('__tags__', {})
if len(source_module.__macros__) + len(source_module.__tags__) == 0:
if assignments != "ALL":
raise ImportError('The module {} has no macros or tags'.format(
source_module))
else:
return False
target_macros = target_namespace.setdefault('__macros__', {})
target_tags = target_namespace.setdefault('__tags__', {})
if prefix:
prefix += "."
if assignments != "ALL":
assignments = {mangle(str_type(k)): v for k, v in assignments}
for d in _hy_macros, _hy_tag:
for name, macro in d[source_module].items():
seen_names.add(name)
if assignments == "ALL":
d[target_module][mangle(prefix + name)] = macro
elif name in assignments:
d[target_module][mangle(prefix + assignments[name])] = macro
if assignments == "ALL":
# Only add macros/tags created in/by the source module.
name_assigns = [(n, n) for n, f in source_macros.items()
if inspect.getmodule(f) == source_module]
name_assigns += [(n, n) for n, f in source_tags.items()
if inspect.getmodule(f) == source_module]
else:
# If one specifically requests a macro/tag not created in the source
# module, I guess we allow it?
name_assigns = assignments
if assignments != "ALL":
unseen = frozenset(assignments.keys()).difference(seen_names)
if unseen:
raise ImportError("cannot require names: " + repr(list(unseen)))
for name, alias in name_assigns:
_name = mangle(name)
alias = mangle(prefix + alias)
if _name in source_module.__macros__:
target_macros[alias] = source_macros[_name]
elif _name in source_module.__tags__:
target_tags[alias] = source_tags[_name]
else:
raise ImportError('Could not require name {} from {}'.format(
_name, source_module))
return True
def load_macros(module_name):
def load_macros(module):
"""Load the hy builtin macros for module `module_name`.
Modules from `hy.core` can only use the macros from CORE_MACROS.
Other modules get the macros from CORE_MACROS and EXTRA_MACROS.
"""
for module in CORE_MACROS:
importlib.import_module(module)
builtin_macros = CORE_MACROS
if module_name.startswith("hy.core"):
return
if not module.__name__.startswith("hy.core"):
builtin_macros += EXTRA_MACROS
for module in EXTRA_MACROS:
importlib.import_module(module)
module_macros = module.__dict__.setdefault('__macros__', {})
module_tags = module.__dict__.setdefault('__tags__', {})
for builtin_mod_name in builtin_macros:
builtin_mod = importlib.import_module(builtin_mod_name)
# Make sure we don't overwrite macros in the module.
if hasattr(builtin_mod, '__macros__'):
module_macros.update({k: v
for k, v in builtin_mod.__macros__.items()
if k not in module_macros})
if hasattr(builtin_mod, '__tags__'):
module_tags.update({k: v
for k, v in builtin_mod.__tags__.items()
if k not in module_tags})
def make_empty_fn_copy(fn):
@ -174,14 +262,17 @@ def make_empty_fn_copy(fn):
return empty_fn
def macroexpand(tree, compiler, once=False):
def macroexpand(tree, module, compiler=None, once=False):
"""Expand the toplevel macros for the `tree`.
Load the macros from the given `compiler.module_name`, then expand the
Load the macros from the given `module`, then expand the
(top-level) macros in `tree` until we no longer can.
"""
load_macros(compiler.module_name)
if not inspect.ismodule(module):
module = importlib.import_module(module)
assert not compiler or compiler.module == module
while True:
if not isinstance(tree, HyExpression) or tree == []:
@ -192,24 +283,27 @@ def macroexpand(tree, compiler, once=False):
break
fn = mangle(fn)
m = _hy_macros[compiler.module_name].get(fn) or _hy_macros[None].get(fn)
m = module.__macros__.get(fn, None)
if not m:
break
opts = {}
if m._hy_macro_pass_compiler:
if compiler is None:
from hy.compiler import HyASTCompiler
compiler = HyASTCompiler(module)
opts['compiler'] = compiler
try:
m_copy = make_empty_fn_copy(m)
m_copy(compiler.module_name, *tree[1:], **opts)
m_copy(module.__name__, *tree[1:], **opts)
except TypeError as e:
msg = "expanding `" + str(tree[0]) + "': "
msg += str(e).replace("<lambda>()", "", 1).strip()
raise HyMacroExpansionError(tree, msg)
try:
obj = m(compiler.module_name, *tree[1:], **opts)
obj = m(module.__name__, *tree[1:], **opts)
except HyTypeError as e:
if e.expression is None:
e.expression = tree
@ -225,25 +319,22 @@ def macroexpand(tree, compiler, once=False):
tree = wrap_value(tree)
return tree
def macroexpand_1(tree, compiler):
def macroexpand_1(tree, module, compiler=None):
"""Expand the toplevel macro from `tree` once, in the context of
`compiler`."""
return macroexpand(tree, compiler, once=True)
return macroexpand(tree, module, compiler, once=True)
def tag_macroexpand(tag, tree, compiler):
"""Expand the tag macro "tag" with argument `tree`."""
load_macros(compiler.module_name)
def tag_macroexpand(tag, tree, module):
"""Expand the tag macro `tag` with argument `tree`."""
if not inspect.ismodule(module):
module = importlib.import_module(module)
tag_macro = module.__tags__.get(tag, None)
tag_macro = _hy_tag[compiler.module_name].get(tag)
if tag_macro is None:
try:
tag_macro = _hy_tag[None][tag]
except KeyError:
raise HyTypeError(
tag,
"`{0}' is not a defined tag macro.".format(tag)
)
raise HyTypeError(tag, "'{0}' is not a defined tag macro.".format(tag))
expr = tag_macro(tree)
return replace_hy_obj(expr, tree)

View File

@ -56,7 +56,7 @@ def test_runpy():
def test_stringer():
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '')
_ast = hy_compile(hy_parse("(defn square [x] (* x x))"), '__main__')
assert type(_ast.body[0]) == ast.FunctionDef
@ -80,7 +80,7 @@ def test_import_error_reporting():
def _import_error_test():
try:
_ = hy_compile(hy_parse("(import \"sys\")"), '')
_ = hy_compile(hy_parse("(import \"sys\")"), '__main__')
except HyTypeError:
return "Error reported"

View File

@ -22,6 +22,7 @@ def tmac(ETname, *tree):
def test_preprocessor_simple():
""" Test basic macro expansion """
obj = macroexpand(tokenize('(test "one" "two")')[0],
__name__,
HyASTCompiler(__name__))
assert obj == HyList(["one", "two"])
assert type(obj) == HyList
@ -30,6 +31,7 @@ def test_preprocessor_simple():
def test_preprocessor_expression():
""" Test that macro expansion doesn't recurse"""
obj = macroexpand(tokenize('(test (test "one" "two"))')[0],
__name__,
HyASTCompiler(__name__))
assert type(obj) == HyList
@ -41,13 +43,13 @@ def test_preprocessor_expression():
obj = HyList([HyString("one"), HyString("two")])
obj = tokenize('(shill ["one" "two"])')[0][1]
assert obj == macroexpand(obj, HyASTCompiler(""))
assert obj == macroexpand(obj, __name__, HyASTCompiler(__name__))
def test_preprocessor_exceptions():
""" Test that macro expansion raises appropriate exceptions"""
with pytest.raises(HyMacroExpansionError) as excinfo:
macroexpand(tokenize('(defn)')[0], HyASTCompiler(__name__))
macroexpand(tokenize('(defn)')[0], __name__, HyASTCompiler(__name__))
assert "_hy_anon_fn_" not in excinfo.value.message
assert "TypeError" not in excinfo.value.message
@ -56,6 +58,6 @@ def test_macroexpand_nan():
# https://github.com/hylang/hy/issues/1574
import math
NaN = float('nan')
x = macroexpand(HyFloat(NaN), HyASTCompiler(__name__))
x = macroexpand(HyFloat(NaN), __name__, HyASTCompiler(__name__))
assert type(x) is HyFloat
assert math.isnan(x)

View File

@ -11,6 +11,7 @@ def test_tag_macro_error():
"""Check if we get correct error with wrong dispatch character"""
try:
macroexpand(tokenize("(dispatch_tag_macro '- '())")[0],
__name__,
HyASTCompiler(__name__))
except HyTypeError as e:
assert "with the character `-`" in str(e)

View File

@ -0,0 +1,8 @@
(defmacro bar [expr]
`(print ~expr))
(defmacro foo [expr]
`(do (require [tests.resources.bin.circular-macro-require [bar]])
(bar ~expr)))
(foo 42)

View File

@ -0,0 +1,3 @@
(require [hy.extra.anaphoric [ap-if]])
(print (eval '(ap-if (+ "a" "b") (+ it "c"))))

View File

@ -388,3 +388,38 @@ def test_bin_hy_file_no_extension():
"""Confirm that a file with no extension is processed as Hy source"""
output, _ = run_cmd("hy tests/resources/no_extension")
assert "This Should Still Work" in output
def test_bin_hy_circular_macro_require():
"""Confirm that macros can require themselves during expansion and when
run from the command line."""
# First, with no bytecode
test_file = "tests/resources/bin/circular_macro_require.hy"
rm(cache_from_source(test_file))
assert not os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "42" == output.strip()
# Now, with bytecode
assert os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "42" == output.strip()
def test_bin_hy_macro_require():
"""Confirm that a `require` will load macros into the non-module namespace
(i.e. `exec(code, locals)`) used by `runpy.run_path`.
In other words, this confirms that the AST generated for a `require` will
load macros into the unnamed namespace its run in."""
# First, with no bytecode
test_file = "tests/resources/bin/require_and_eval.hy"
rm(cache_from_source(test_file))
assert not os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip()
# Now, with bytecode
assert os.path.exists(cache_from_source(test_file))
output, _ = run_cmd("hy {}".format(test_file))
assert "abc" == output.strip()