From 1f95da2b0bf9f43754bd37812a3e62ee6af086d9 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Fri, 6 Apr 2018 09:26:01 -0700 Subject: [PATCH 1/5] Avoid using a core function name as a variable --- hy/core/language.hy | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/hy/core/language.hy b/hy/core/language.hy index 4cba9a3..17d0d8a 100644 --- a/hy/core/language.hy +++ b/hy/core/language.hy @@ -297,16 +297,12 @@ Return series of accumulated sums (or other binary function results)." (defn macroexpand [form] "Return the full macro expansion of `form`." (import hy.macros) - - (setv name (calling-module-name)) - (hy.macros.macroexpand form (HyASTCompiler name))) + (hy.macros.macroexpand form (HyASTCompiler (calling-module-name)))) (defn macroexpand-1 [form] "Return the single step macro expansion of `form`." (import hy.macros) - - (setv name (calling-module-name)) - (hy.macros.macroexpand-1 form (HyASTCompiler name))) + (hy.macros.macroexpand-1 form (HyASTCompiler (calling-module-name)))) (defn merge-with [f &rest maps] "Return the map of `maps` joined onto the first via the function `f`. From 4a6b633ad27b32f6c80c02c69ec57c99f05acbb3 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Fri, 6 Apr 2018 10:11:59 -0700 Subject: [PATCH 2/5] Add some missing imports --- hy/core/bootstrap.hy | 1 + tests/native_tests/quote.hy | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/hy/core/bootstrap.hy b/hy/core/bootstrap.hy index 675df9c..e061ef0 100644 --- a/hy/core/bootstrap.hy +++ b/hy/core/bootstrap.hy @@ -41,6 +41,7 @@ (if ~@(cut args 2)))))) (defmacro deftag [tag-name lambda-list &rest body] + (import hy.models) (if (and (not (isinstance tag-name hy.models.HySymbol)) (not (isinstance tag-name hy.models.HyString))) (raise (hy.errors.HyTypeError diff --git a/tests/native_tests/quote.hy b/tests/native_tests/quote.hy index 09aafc5..3b31350 100644 --- a/tests/native_tests/quote.hy +++ b/tests/native_tests/quote.hy @@ -2,7 +2,7 @@ ;; This file is part of Hy, which is free software licensed under the Expat ;; license. See the LICENSE. -(import [hy [HyExpression HySymbol HyString HyBytes]]) +(import [hy [HyExpression HySymbol HyString HyBytes HyDict]]) (defn test-quote [] @@ -43,7 +43,7 @@ (assert (= (get q 1) (quote bar))) (assert (= (get q 2) (quote baz))) (assert (= (get q 3) (quote quux))) - (assert (= (type q) hy.HyDict))) + (assert (= (type q) HyDict))) (defn test-quote-expr-in-dict [] From 116d7fa6ecc2ab53861f6654ee86c9852fb48ff6 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Fri, 6 Apr 2018 13:12:48 -0700 Subject: [PATCH 3/5] Copy in compile_atom instead of macroexpand_1 This copying is what keeps all the mutating code in the compiler methods (e.g., `expr.pop(0)`) from breaking cases in which Hy model objects are compiled more than once or inspected after compilation. --- hy/compiler.py | 2 ++ hy/macros.py | 7 ++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/hy/compiler.py b/hy/compiler.py index d94e94b..0dc28b7 100755 --- a/hy/compiler.py +++ b/hy/compiler.py @@ -423,6 +423,8 @@ class HyASTCompiler(object): # pass in `atom_type`. atom_compiler = _compile_table[atom_type] arity = hy.inspect.get_arity(atom_compiler) + # Compliation methods may mutate the atom, so copy it first. + atom = copy.copy(atom) ret = (atom_compiler(self, atom, atom_type) if arity == 3 else atom_compiler(self, atom)) diff --git a/hy/macros.py b/hy/macros.py index 110c37d..72dc8bd 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -181,8 +181,6 @@ def macroexpand_1(tree, compiler): fn = tree[0] if fn in ("quote", "quasiquote"): return tree - ntree = HyExpression(tree[:]) - ntree.replace(tree) opts = {} @@ -197,14 +195,14 @@ def macroexpand_1(tree, compiler): try: m_copy = make_empty_fn_copy(m) - m_copy(compiler.module_name, *ntree[1:], **opts) + m_copy(compiler.module_name, *tree[1:], **opts) except TypeError as e: msg = "expanding `" + str(tree[0]) + "': " msg += str(e).replace("()", "", 1).strip() raise HyMacroExpansionError(tree, msg) try: - obj = m(compiler.module_name, *ntree[1:], **opts) + obj = m(compiler.module_name, *tree[1:], **opts) except HyTypeError as e: if e.expression is None: e.expression = tree @@ -214,7 +212,6 @@ def macroexpand_1(tree, compiler): raise HyMacroExpansionError(tree, msg) replace_hy_obj(obj, tree) return obj - return ntree return tree From 026316ebef736f310ecbbd45363bf9f7d25fa2e3 Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Sat, 7 Apr 2018 17:17:40 -0700 Subject: [PATCH 4/5] Refactor macroexpand_1 --- hy/macros.py | 63 ++++++++++++++++++++++++---------------------------- 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/hy/macros.py b/hy/macros.py index 72dc8bd..a3e0806 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -174,45 +174,40 @@ def macroexpand(tree, compiler): def macroexpand_1(tree, compiler): """Expand the toplevel macro from `tree` once, in the context of `module_name`.""" - if isinstance(tree, HyExpression): - if tree == []: - return tree + if not isinstance(tree, HyExpression) or tree == []: + return tree - fn = tree[0] - if fn in ("quote", "quasiquote"): - return tree + fn = tree[0] + if fn in ("quote", "quasiquote") or not isinstance(fn, HySymbol): + return tree - opts = {} + fn = mangle(fn) + m = _hy_macros[compiler.module_name].get(fn) or _hy_macros[None].get(fn) + if not m: + return tree - if isinstance(fn, HySymbol): - fn = mangle(str_type(fn)) - m = _hy_macros[compiler.module_name].get(fn) - if m is None: - m = _hy_macros[None].get(fn) - if m is not None: - if m._hy_macro_pass_compiler: - opts['compiler'] = compiler + opts = {} + if m._hy_macro_pass_compiler: + opts['compiler'] = compiler - try: - m_copy = make_empty_fn_copy(m) - m_copy(compiler.module_name, *tree[1:], **opts) - except TypeError as e: - msg = "expanding `" + str(tree[0]) + "': " - msg += str(e).replace("()", "", 1).strip() - raise HyMacroExpansionError(tree, msg) + try: + m_copy = make_empty_fn_copy(m) + m_copy(compiler.module_name, *tree[1:], **opts) + except TypeError as e: + msg = "expanding `" + str(tree[0]) + "': " + msg += str(e).replace("()", "", 1).strip() + raise HyMacroExpansionError(tree, msg) - try: - obj = m(compiler.module_name, *tree[1:], **opts) - except HyTypeError as e: - if e.expression is None: - e.expression = tree - raise - except Exception as e: - msg = "expanding `" + str(tree[0]) + "': " + repr(e) - raise HyMacroExpansionError(tree, msg) - replace_hy_obj(obj, tree) - return obj - return tree + try: + obj = m(compiler.module_name, *tree[1:], **opts) + except HyTypeError as e: + if e.expression is None: + e.expression = tree + raise + except Exception as e: + msg = "expanding `" + str(tree[0]) + "': " + repr(e) + raise HyMacroExpansionError(tree, msg) + return replace_hy_obj(obj, tree) def tag_macroexpand(tag, tree, compiler): From c1a487cdf7bbaea8c7c8085596d7e585b5cada0f Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 9 Apr 2018 12:01:12 -0700 Subject: [PATCH 5/5] Move logic from macroexpand_1 to macroexpand By ending macro-expansion immediately when appropriate, this change fixes a bug arising from the fact that NaN != NaN. --- NEWS.rst | 1 + hy/macros.py | 85 ++++++++++++++-------------- tests/macros/test_macro_processor.py | 11 +++- 3 files changed, 54 insertions(+), 43 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index a52a2ae..9f48e13 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -42,6 +42,7 @@ Bug Fixes * Fixed a case where `->` and `->>` duplicated an argument * Fixed bugs that caused `defclass` to drop statements or crash * Fixed a REPL crash caused by illegle unicode escape string inputs +* `NaN` can no longer create an infinite loop during macro-expansion Misc. Improvements ---------------------------- diff --git a/hy/macros.py b/hy/macros.py index a3e0806..e4cafee 100644 --- a/hy/macros.py +++ b/hy/macros.py @@ -156,58 +156,59 @@ def make_empty_fn_copy(fn): return empty_fn -def macroexpand(tree, compiler): +def macroexpand(tree, compiler, once=False): """Expand the toplevel macros for the `tree`. Load the macros from the given `module_name`, then expand the (top-level) - macros in `tree` until it stops changing. + macros in `tree` until we no longer can. """ load_macros(compiler.module_name) - old = None - while old != tree: - old = tree - tree = macroexpand_1(tree, compiler) - return tree + while True: + + if not isinstance(tree, HyExpression) or tree == []: + return tree + + fn = tree[0] + if fn in ("quote", "quasiquote") or not isinstance(fn, HySymbol): + return tree + + fn = mangle(fn) + m = _hy_macros[compiler.module_name].get(fn) or _hy_macros[None].get(fn) + if not m: + return tree + + opts = {} + if m._hy_macro_pass_compiler: + opts['compiler'] = compiler + + try: + m_copy = make_empty_fn_copy(m) + m_copy(compiler.module_name, *tree[1:], **opts) + except TypeError as e: + msg = "expanding `" + str(tree[0]) + "': " + msg += str(e).replace("()", "", 1).strip() + raise HyMacroExpansionError(tree, msg) + + try: + obj = m(compiler.module_name, *tree[1:], **opts) + except HyTypeError as e: + if e.expression is None: + e.expression = tree + raise + except Exception as e: + msg = "expanding `" + str(tree[0]) + "': " + repr(e) + raise HyMacroExpansionError(tree, msg) + tree = replace_hy_obj(obj, tree) + + if once: + return tree def macroexpand_1(tree, compiler): """Expand the toplevel macro from `tree` once, in the context of - `module_name`.""" - if not isinstance(tree, HyExpression) or tree == []: - return tree - - fn = tree[0] - if fn in ("quote", "quasiquote") or not isinstance(fn, HySymbol): - return tree - - fn = mangle(fn) - m = _hy_macros[compiler.module_name].get(fn) or _hy_macros[None].get(fn) - if not m: - return tree - - opts = {} - if m._hy_macro_pass_compiler: - opts['compiler'] = compiler - - try: - m_copy = make_empty_fn_copy(m) - m_copy(compiler.module_name, *tree[1:], **opts) - except TypeError as e: - msg = "expanding `" + str(tree[0]) + "': " - msg += str(e).replace("()", "", 1).strip() - raise HyMacroExpansionError(tree, msg) - - try: - obj = m(compiler.module_name, *tree[1:], **opts) - except HyTypeError as e: - if e.expression is None: - e.expression = tree - raise - except Exception as e: - msg = "expanding `" + str(tree[0]) + "': " + repr(e) - raise HyMacroExpansionError(tree, msg) - return replace_hy_obj(obj, tree) + `compiler`.""" + return macroexpand(tree, compiler, once=True) def tag_macroexpand(tag, tree, compiler): diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py index dc14fec..cae39a2 100644 --- a/tests/macros/test_macro_processor.py +++ b/tests/macros/test_macro_processor.py @@ -5,7 +5,7 @@ from hy.macros import macro, macroexpand from hy.lex import tokenize -from hy.models import HyString, HyList, HySymbol, HyExpression +from hy.models import HyString, HyList, HySymbol, HyExpression, HyFloat from hy.errors import HyMacroExpansionError from hy.compiler import HyASTCompiler @@ -53,3 +53,12 @@ def test_preprocessor_exceptions(): except HyMacroExpansionError as e: assert "_hy_anon_fn_" not in str(e) assert "TypeError" not in str(e) + + +def test_macroexpand_nan(): + # https://github.com/hylang/hy/issues/1574 + import math + NaN = float('nan') + x = macroexpand(HyFloat(NaN), HyASTCompiler(__name__)) + assert type(x) is HyFloat + assert math.isnan(x)