Repository: hylang/hy

Issue:
`hy.as-model` raises `RecursionError` for self-referential objects
```
=> (setv x [1 2 3])
=> (setv (get x 1) x)
=> x
[1 [...] 3]
=> (hy.as-model x)
…
RecursionError: maximum recursion depth exceeded while calling a Python object
```

Ultimately we don't want to try to support self-referential or otherwise infinite Hy programs, but we can do a little better here by checking for self-references instead of trying to recurse infinitely.


Discussion:

Resolving patch:
diff --git a/hy/models.py b/hy/models.py
--- a/hy/models.py
+++ b/hy/models.py
@@ -109,7 +109,7 @@ def __hash__(self):
 
 
 _wrappers = {}
-
+_seen = set()
 
 def as_model(x):
     """Recurisvely promote an object ``x`` into its canonical model form.
@@ -143,6 +143,9 @@ def as_model(x):
              ; the literal int ``2`` to its model for ``(hy.model.Integer 2)``
     """
 
+    if id(x) in _seen:
+        raise HyWrapperError("Self-referential structure detected in {!r}".format(x))
+
     new = _wrappers.get(type(x), lambda y: y)(x)
     if not isinstance(new, Object):
         raise HyWrapperError("Don't know how to wrap {!r}: {!r}".format(type(x), x))
@@ -471,7 +474,15 @@ class List(Sequence):
 
 
 def recwrap(f):
-    return lambda l: f(as_model(x) for x in l)
+
+    def lambda_to_return(l):
+        _seen.add(id(l))
+        try:
+            return f(as_model(x) for x in l)
+        finally:
+            _seen.remove(id(l))
+
+    return lambda_to_return
 
 _wrappers[FComponent] = recwrap(FComponent)
 _wrappers[FString] = lambda fstr: FString(
@@ -517,8 +528,15 @@ def values(self):
     def items(self):
         return list(zip(self.keys(), self.values()))
 
+def _dict_wrapper(d):
+    _seen.add(id(d))
+    try:
+        return Dict(as_model(x) for x in sum(d.items(), ()))
+    finally:
+        _seen.remove(id(d))
+
 _wrappers[Dict] = recwrap(Dict)
-_wrappers[dict] = lambda d: Dict(as_model(x) for x in sum(d.items(), ()))
+_wrappers[dict] = _dict_wrapper
 
 
 class Expression(Sequence):


Test patch:
diff --git a/tests/test_models.py b/tests/test_models.py
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -1,6 +1,7 @@
 import copy
 import pytest
 import hy
+from hy.errors import HyWrapperError
 from hy.models import (
     FComponent, FString, as_model, replace_hy_obj, Symbol, Keyword, String, Integer,
     List, Dict, Set, Expression, Complex, Float, pretty)
@@ -216,3 +217,26 @@ def test_compound_model_repr():
             assert eval(repr(model([1, 2, 3]))) == model([1, 2, 3])
         for k, v in PRETTY_STRINGS.items():
             assert repr(hy.read_str(k)) == v
+
+
+def test_recursive_model_detection():
+    """ Check for self-references:
+    https://github.com/hylang/hy/issues/2153
+    """
+    self_ref_list = [1, 2, 3]
+    self_ref_dict = {1: 1, 2: 2}
+    self_ref_list[1] = self_ref_list
+    self_ref_dict[2] = self_ref_dict
+
+    mutually_ref_list = [1, 2, 3]
+    mutually_ref_dict = {1: 1, 2: 2}
+    mutually_ref_list[1] = mutually_ref_dict
+    mutually_ref_dict[2] = mutually_ref_list
+
+    for structure in [self_ref_list,
+                      self_ref_dict,
+                      mutually_ref_list,
+                      mutually_ref_dict]:
+        with pytest.raises(HyWrapperError) as exc:
+            as_model(structure)
+        assert "Self-referential" in str(exc)