Repository: hylang/hy

Issue:
Consider making Hy models immutable.
We've discussed this a bit recently in other issues. Particularly hylang/hy#1519. (also hylang/hy#1538). But it's important enough for a dedicated issue.

The mutable models are causing serious but subtle bugs (the worst kind) in the compiler which are most obvious in macros. User macros may run into the same kind of issues, even if we add workarounds to ours. Not good.

Fixing the compiler without switching to immutable Hy models may be very difficult. Our tests are not adequate--so we can't be very sure we've fixed all the problems, and it would be easy to reintroduce similar problems by accident. It not clear we could add enough tests to catch everything either. The best practice without changing the interface may be to copy on receipt and copy-on-write everywhere in the compiler, which would be slow. Switching to immutable models may actually be easier and perform faster.

The obvious approach would be to base our models on `tuple` instead of `list`, but that's not the only option. We could use Pyrsistent, or roll our own basic linked list, or add an immutable seq abstraction for iterators in general hylang/hyrule#32.

Regardless of the chosen approach, this is a very deep change that will break the world--to fix it. Fixing Hy's core will be a pain. Most basic user code would be unchanged, but anything that touches the Hy models directly (particularly macros) will break and have to be rewritten to the new interface.


Discussion:
Also related to this, I think this could improve hy2py as well with macro output. It be nicer to see this:

```python
HyExpression([HySymbol('setv'), HySymbol('x'), HyInteger(1)])
```

instead of 

```python
HyExpression([] + [HySymbol('setv')] + [HySymbol('x')] + [HyInteger(1)])
```
It's a consequence of the implementation of quasiquoting. We'd basically need the list monad to get the `~@` unquote splice, i.e. if it's already an iterable, just convert it to a list instead of wrapping it in one. E.g.
```
`(0 ~@[1 2 3] 4)
```
currently compiles to
```
HyExpression([] + [HyInteger(0)] + list([1, 2, 3] or []) + [HyInteger(4)])
```
but could be
```
HyExpression([HyInteger(0), *list([1, 2, 3] or []), HyInteger(4)])
```
but only since PEP 448 in v3.5. We're still supporting 3.4 though.

[Edit:
better yet, use tuples:
```
HyExpression([HyInteger(0), *tuple([1, 2, 3] or ()), HyInteger(4)])
```
]

I'm not sure how immutable models help us here.

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1345,17 +1345,16 @@ def compile_def_expression(self, expr, root, pairs):
 
     def _compile_assign(self, root, name, result):
 
-        str_name = "%s" % name
-        if str_name in ("None", "True", "False"):
+        if name in [HySymbol(x) for x in ("None", "True", "False")]:
             raise self._syntax_error(name,
-                "Can't assign to `%s'" % str_name)
+                "Can't assign to `{}'".format(name))
 
         result = self.compile(result)
         ld_name = self.compile(name)
 
         if isinstance(ld_name.expr, ast.Call):
             raise self._syntax_error(name,
-                "Can't assign to a callable: `%s'" % str_name)
+                "Can't assign to a callable: `{}'".format(name))
 
         if (result.temp_variables
                 and isinstance(name, HySymbol)
diff --git a/hy/models.py b/hy/models.py
--- a/hy/models.py
+++ b/hy/models.py
@@ -243,7 +243,7 @@ def __new__(cls, real, imag=0, *args, **kwargs):
 _wrappers[complex] = HyComplex
 
 
-class HySequence(HyObject, list):
+class HySequence(HyObject, tuple):
     """
     An abstract type for sequence-like models to inherit from.
     """
@@ -256,7 +256,8 @@ def replace(self, other, recursive=True):
         return self
 
     def __add__(self, other):
-        return self.__class__(super(HySequence, self).__add__(other))
+        return self.__class__(super(HySequence, self).__add__(
+            tuple(other) if isinstance(other, list) else other))
 
     def __getslice__(self, start, end):
         return self.__class__(super(HySequence, self).__getslice__(start, end))
@@ -326,10 +327,10 @@ def __str__(self):
                 return '' + g("HyDict()")
 
     def keys(self):
-        return self[0::2]
+        return list(self[0::2])
 
     def values(self):
-        return self[1::2]
+        return list(self[1::2])
 
     def items(self):
         return list(zip(self.keys(), self.values()))


Test patch:
diff --git a/tests/native_tests/contrib/walk.hy b/tests/native_tests/contrib/walk.hy
--- a/tests/native_tests/contrib/walk.hy
+++ b/tests/native_tests/contrib/walk.hy
@@ -21,17 +21,16 @@
              walk-form)))
 
 (defn test-walk []
-  (setv acc '())
-  (assert (= (walk (partial collector acc) identity walk-form)
+  (setv acc [])
+  (assert (= (list (walk (partial collector acc) identity walk-form))
              [None None]))
-  (assert (= acc walk-form))
+  (assert (= acc (list walk-form)))
   (setv acc [])
   (assert (= (walk identity (partial collector acc) walk-form)
              None))
   (assert (= acc [walk-form])))
 
 (defn test-walk-iterators []
-  (setv acc [])
   (assert (= (walk (fn [x] (* 2 x)) (fn [x] x)
                    (drop 1 [1 [2 [3 [4]]]]))
              [[2 [3 [4]] 2 [3 [4]]]])))
@@ -44,19 +43,19 @@
   (assert (= (macroexpand-all '(foo-walk))
              42))
   (assert (= (macroexpand-all '(with [a 1]))
-             '(with* [a 1] (do))))
+             '(with* [a 1])))
   (assert (= (macroexpand-all '(with [a 1 b 2 c 3] (for [d c] foo)))
-             '(with* [a 1] (with* [b 2] (with* [c 3] (do (for [d c] foo)))))))
+             '(with* [a 1] (with* [b 2] (with* [c 3] (for [d c] foo))))))
   (assert (= (macroexpand-all '(with [a 1]
                                  '(with [b 2])
                                  `(with [c 3]
                                     ~(with [d 4])
                                     ~@[(with [e 5])])))
              '(with* [a 1]
-                (do '(with [b 2])
-                    `(with [c 3]
-                       ~(with* [d 4] (do))
-                       ~@[(with* [e 5] (do))])))))
+                '(with [b 2])
+                `(with [c 3]
+                   ~(with* [d 4])
+                   ~@[(with* [e 5])]))))
 
   (defmacro require-macro []
     `(do
@@ -130,7 +129,7 @@
                '(foo `(bar a ~a ~"x"))))
     (assert (= `(foo ~@[a])
                '(foo "x")))
-    (assert (= `(foo `(bar [a] ~@[a] ~@~[a 'a `a] ~~@[a]))
+    (assert (= `(foo `(bar [a] ~@[a] ~@~(HyList [a 'a `a]) ~~@[a]))
                '(foo `(bar [a] ~@[a] ~@["x" a a] ~"x"))))))
 
 (defn test-let-except []
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
@@ -9,7 +9,7 @@
   "NATIVE: test for quoting functionality"
   (setv q (quote (a b c)))
   (assert (= (len q) 3))
-  (assert (= q [(quote a) (quote b) (quote c)])))
+  (assert (= q (HyExpression [(quote a) (quote b) (quote c)]))))
 
 
 (defn test-basic-quoting []
diff --git a/tests/test_models.py b/tests/test_models.py
--- a/tests/test_models.py
+++ b/tests/test_models.py
@@ -56,8 +56,8 @@ def test_list_add():
     a = HyList([1, 2, 3])
     b = HyList([3, 4, 5])
     c = a + b
-    assert c == [1, 2, 3, 3, 4, 5]
-    assert c.__class__ == HyList
+    assert c == HyList([1, 2, 3, 3, 4, 5])
+    assert type(c) is HyList
 
 
 def test_list_slice():
@@ -91,7 +91,7 @@ def test_dict_values():
 
 
 def test_set():
-    assert hyset == [3, 1, 2, 2]
+    assert list(hyset) == [3, 1, 2, 2]
 
 
 def test_number_model_copy():