From c1a487cdf7bbaea8c7c8085596d7e585b5cada0f Mon Sep 17 00:00:00 2001 From: Kodi Arfer Date: Mon, 9 Apr 2018 12:01:12 -0700 Subject: [PATCH] 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)