Repository: hylang/hy

Issue:
(macroexpand 'NaN) enters infinite macro-expansion loop



Discussion:
> (if a b NaN) or (if NaN) enters infinite macro-expansion loop

I don't think the problem is in the `if` macro.
```
=> (macroexpand 'NaN)
from hy.core.language import macroexpand
from hy import HyFloat
macroexpand(HyFloat(1e1000 - 1e1000))


KeyboardInterrupt
```
`macroexpand` is choking on a naked `NaN`. `macroexpand-1` doesn't seem to have a problem with it.

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -423,6 +423,8 @@ def compile_atom(self, atom_type, atom):
             # 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
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -156,66 +156,59 @@ def empty_fn(*args, **kwargs):
     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:
 
-
-def macroexpand_1(tree, compiler):
-    """Expand the toplevel macro from `tree` once, in the context of
-    `module_name`."""
-    if isinstance(tree, HyExpression):
-        if tree == []:
+        if not isinstance(tree, HyExpression) or tree == []:
             return tree
 
         fn = tree[0]
-        if fn in ("quote", "quasiquote"):
+        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
-        ntree = HyExpression(tree[:])
-        ntree.replace(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("<lambda>()", "", 1).strip()
+            raise HyMacroExpansionError(tree, msg)
 
-        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
-
-                try:
-                    m_copy = make_empty_fn_copy(m)
-                    m_copy(compiler.module_name, *ntree[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, *ntree[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 ntree
-    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)
+        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
+    `compiler`."""
+    return macroexpand(tree, compiler, once=True)
 
 
 def tag_macroexpand(tag, tree, compiler):


Test patch:
diff --git a/tests/macros/test_macro_processor.py b/tests/macros/test_macro_processor.py
--- 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)
diff --git a/tests/native_tests/quote.hy b/tests/native_tests/quote.hy
--- 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 []