hy/hy/macros.py
Brandon T. Willard 51c7efe6e8 Retain compiled source and file information for exceptions
This commit refactors the exception/error classes and their handling.
It also retains Hy source strings and their originating file information, when
available, all throughout the core parser and compiler functions.

As well, with these changes, calling code is no longer responsible for providing
source and file details to exceptions,

Closes hylang/hy#657.
2019-02-07 13:43:58 -05:00

402 lines
12 KiB
Python

# Copyright 2019 the authors.
# This file is part of Hy, which is free software licensed under the Expat
# license. See the LICENSE.
import sys
import importlib
import inspect
import pkgutil
from contextlib import contextmanager
from hy._compat import PY3, string_types, reraise
from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value
from hy.lex import mangle
from hy.errors import HyTypeError, HyMacroExpansionError
try:
# Check if we have the newer inspect.signature available.
# Otherwise fallback to the legacy getargspec.
inspect.signature # noqa
except AttributeError:
def has_kwargs(fn):
argspec = inspect.getargspec(fn)
return argspec.keywords is not None
def format_args(fn):
argspec = inspect.getargspec(fn)
return inspect.formatargspec(*argspec)
else:
def has_kwargs(fn):
parameters = inspect.signature(fn).parameters
return any(param.kind == param.VAR_KEYWORD
for param in parameters.values())
def format_args(fn):
return str(inspect.signature(fn))
CORE_MACROS = [
"hy.core.bootstrap",
]
EXTRA_MACROS = [
"hy.core.macros",
]
def macro(name):
"""Decorator to define a macro called `name`.
"""
name = mangle(name)
def _(fn):
fn.__name__ = '({})'.format(name)
try:
fn._hy_macro_pass_compiler = has_kwargs(fn)
except Exception:
# An exception might be raised if fn has arguments with
# names that are invalid in Python.
fn._hy_macro_pass_compiler = False
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`.
"""
def _(fn):
_name = mangle('#{}'.format(name))
if not PY3:
_name = _name.encode('UTF-8')
fn.__name__ = _name
module = inspect.getmodule(fn)
module_name = module.__name__
if module_name.startswith("hy.core"):
module_name = None
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 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.
"""
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":
name_assigns = [(k, k) for k in
tuple(source_macros.keys()) + tuple(source_tags.keys())]
else:
name_assigns = assignments
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):
"""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.
"""
builtin_macros = CORE_MACROS
if not module.__name__.startswith("hy.core"):
builtin_macros += EXTRA_MACROS
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):
try:
# This might fail if fn has parameters with funny names, like o!n. In
# such a case, we return a generic function that ensures the program
# can continue running. Unfortunately, the error message that might get
# raised later on while expanding a macro might not make sense at all.
formatted_args = format_args(fn)
fn_str = 'lambda {}: None'.format(
formatted_args.lstrip('(').rstrip(')'))
empty_fn = eval(fn_str)
except Exception:
def empty_fn(*args, **kwargs):
None
return empty_fn
@contextmanager
def macro_exceptions(module, macro_tree, compiler=None):
try:
yield
except Exception as e:
try:
filename = inspect.getsourcefile(module)
source = inspect.getsource(module)
except TypeError:
if compiler:
filename = compiler.filename
source = compiler.source
if not isinstance(e, HyTypeError):
exc_type = HyMacroExpansionError
msg = "expanding `{}': ".format(macro_tree[0])
msg += str(e).replace("<lambda>()", "", 1).strip()
else:
exc_type = HyTypeError
msg = e.message
reraise(exc_type,
exc_type(msg, filename, macro_tree, source),
sys.exc_info()[2].tb_next)
def macroexpand(tree, module, compiler=None, once=False):
"""Expand the toplevel macros for the given Hy AST tree.
Load the macros from the given `module`, then expand the (top-level) macros
in `tree` until we no longer can.
`HyExpression` resulting from macro expansions are assigned the module in
which the macro function is defined (determined using `inspect.getmodule`).
If the resulting `HyExpression` is itself macro expanded, then the
namespace of the assigned module is checked first for a macro corresponding
to the expression's head/car symbol. If the head/car symbol of such a
`HyExpression` is not found among the macros of its assigned module's
namespace, the outer-most namespace--e.g. the one given by the `module`
parameter--is used as a fallback.
Parameters
----------
tree: HyObject or list
Hy AST tree.
module: str or types.ModuleType
Module used to determine the local namespace for macros.
compiler: HyASTCompiler, optional
The compiler object passed to expanded macros.
once: boolean, optional
Only expand the first macro in `tree`.
Returns
------
out: HyObject
Returns a mutated tree with macros expanded.
"""
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 == []:
break
fn = tree[0]
if fn in ("quote", "quasiquote") or not isinstance(fn, HySymbol):
break
fn = mangle(fn)
expr_modules = (([] if not hasattr(tree, 'module') else [tree.module])
+ [module])
# Choose the first namespace with the macro.
m = next((mod.__macros__[fn]
for mod in expr_modules
if fn in mod.__macros__),
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
with macro_exceptions(module, tree, compiler):
m_copy = make_empty_fn_copy(m)
m_copy(module.__name__, *tree[1:], **opts)
obj = m(module.__name__, *tree[1:], **opts)
if isinstance(obj, HyExpression):
obj.module = inspect.getmodule(m)
tree = replace_hy_obj(obj, tree)
if once:
break
tree = wrap_value(tree)
return tree
def macroexpand_1(tree, module, compiler=None):
"""Expand the toplevel macro from `tree` once, in the context of
`compiler`."""
return macroexpand(tree, module, compiler, once=True)
def tag_macroexpand(tag, tree, module):
"""Expand the tag macro `tag` with argument `tree`."""
if not inspect.ismodule(module):
module = importlib.import_module(module)
expr_modules = (([] if not hasattr(tree, 'module') else [tree.module])
+ [module])
# Choose the first namespace with the macro.
tag_macro = next((mod.__tags__[tag]
for mod in expr_modules
if tag in mod.__tags__),
None)
if tag_macro is None:
raise HyTypeError("`{0}' is not a defined tag macro.".format(tag),
None, tag, None)
expr = tag_macro(tree)
if isinstance(expr, HyExpression):
expr.module = inspect.getmodule(tag_macro)
return replace_hy_obj(expr, tree)