# Copyright 2020 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 import traceback from contextlib import contextmanager from hy._compat import reraise, PY38 from hy.models import replace_hy_obj, HyExpression, HySymbol, wrap_value from hy.lex import mangle from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError, HyRequireError) 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 = rename_function(fn, 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)) fn = rename_function(fn, _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, str): target_module = importlib.import_module(target_module) target_namespace = target_module.__dict__ elif inspect.ismodule(target_module): target_namespace = target_module.__dict__ else: raise HyTypeError('`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): try: source_module = importlib.import_module(source_module) except ImportError as e: reraise(HyRequireError, HyRequireError(e.args[0]), None) 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 HyRequireError('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 HyRequireError('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}) @contextmanager def macro_exceptions(module, macro_tree, compiler=None): try: yield except HyLanguageError as e: # These are user-level Hy errors occurring in the macro. # We want to pass them up to the user. reraise(type(e), e, sys.exc_info()[2]) except Exception as e: if compiler: filename = compiler.filename source = compiler.source else: filename = None source = None exc_msg = ' '.join(traceback.format_exception_only( sys.exc_info()[0], sys.exc_info()[1])) msg = "expanding macro {}\n ".format(str(macro_tree[0])) msg += exc_msg reraise(HyMacroExpansionError, HyMacroExpansionError( msg, macro_tree, filename, source), sys.exc_info()[2]) 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 isinstance(tree, HyExpression) and tree: 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): 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) def rename_function(func, new_name): """Creates a copy of a function and [re]sets the name at the code-object level. """ c = func.__code__ new_code = type(c)(*[getattr(c, 'co_{}'.format(a)) if a != 'name' else str(new_name) for a in code_obj_args]) _fn = type(func)(new_code, func.__globals__, str(new_name), func.__defaults__, func.__closure__) _fn.__dict__.update(func.__dict__) return _fn code_obj_args = ['argcount', 'posonlyargcount', 'kwonlyargcount', 'nlocals', 'stacksize', 'flags', 'code', 'consts', 'names', 'varnames', 'filename', 'name', 'firstlineno', 'lnotab', 'freevars', 'cellvars'] if not PY38: code_obj_args.remove("posonlyargcount")