Repository: hylang/hy

Issue:
Lvalue of setv is checked too early when using `or`
This is a subtopic of the dreaded #233 that I think should have been fixed by #824. Consider the code:

    (import [random [random]])
    (setv x (or (< (random) .5) (do (setv y 1) y)))

This gets translated to:

    from random import random
    _hy_anon_var_1 = (random() < 0.5)
    if (not x):
        y = 1
        _hy_anon_var_1 = y

so running it raises `NameError: name 'x' is not defined
`.

For what it's worth, back in 0.11.1, the result would've been:

    from random import random
    y = 1
    x = ((random() < 0.5) or y)

which is wrong because we aren't even attempting the short-circuit.


Discussion:
I've developed a macro that appears to do the right thing. See if you can break it. If it's all working properly this is a good starting point for what the AST should look like.

``` Hy
=> ;; hy --spy

=> (defmacro -or (&rest args)
...   (if (empty? (cdr args))
...       (first args)
...       (do
...         (setv g (gensym))
...         `(do
...            (setv ~g ~(first args))
...            (if* ~g
...                 ~g
...                 (-or ~@(cdr args)))))))
from hy.core.language import first, gensym, is_empty
import hy
from hy import HyExpression, HySymbol

@hy.macros.macro('_or')
def _hy_anon_fn_1(*args):
    if is_empty(args[1:]):
        _hy_anon_var_1 = first(args)
    else:
        g = gensym()
        _hy_anon_var_1 = HyExpression(((([] + [HySymbol('do')]) + [HyExpression(((([] + [HySymbol('setv')]) + [g]) + [first(args)]))]) + [HyExpression((((([] + [HySymbol('if*')]) + [g]) + [g]) + [HyExpression((([] + [HySymbol('_or')]) + list(args[1:])))]))]))
    return _hy_anon_var_1
=> (import [random [random]])
from random import random
=> (setv x (or (< (random) .5) (do (setv y 1) y)))  ; with original broken version
_hy_anon_var_1 = (random() < 0.5)
if (not x):
    y = 1
    _hy_anon_var_1 = y
Traceback (most recent call last):
  File "<input>", line 1, in <module>
NameError: name 'x' is not defined
=> (setv x (-or (< (random) .5) (do (setv y 1) y)))  ; with new macro version
:G_1235 = (random() < 0.5)
if :G_1235:
    x = :G_1235
else:
    y = 1
    x = y
=>
```

We _could_ just use the macro, honestly. The choice of true special form vs. macro makes little difference to the end user, but I think the built-in version can be more efficient, since it can compile to Python's `or` in some cases and avoid wasted assignments.

Very nice! I'll give it a closer look tomorrow. Don't forget about the 0-argument case. Have you tried it against the `or` tests we already have?

While using the native `or` when possible is nice, it's not a big deal, in my opinion. It's not even obvious to me that it would actually be faster.

The nullary case works:

``` Hy
=> (or)
None
=> (-or)
None
```

I didn't test it that way, but it's probably a good idea to use the existing tests. I'm not sure how easy that would be. I wonder if the tests would just work if you changed the macro name from `-or` to `or`. (Don't forget to rename the recursive part). You might want to disable compilation of `or` just to be sure we're testing the right thing, but that would break the world, so you'd have to define the macro pretty early. (i.e. actually implement the macro version in Hy) Maybe it's easier to just do a find and replace in the tests and keep the name `-or`.

The performance difference might not be noticeable most of the time, but there is a gensym assignment statement that isn't always necessary:

``` Hy
=> (or 1 2)
(1 or 2)
1
=> (-or 1 2)
:G_1235 = 1
(:G_1235 if :G_1235 else 2)
1
```

See the difference?

@kirbyfan64, you implemented the current `or` version, is this an easy fix?


Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1807,16 +1807,17 @@ def compile_logical_or_and_and_operator(self, expression):
                                  ctx=ast.Load(),
                                  lineno=root_line,
                                  col_offset=root_column)
+            temp_variables = [name, expr_name]
 
             def make_assign(value, node=None):
                 if node is None:
                     line, column = root_line, root_column
                 else:
                     line, column = node.lineno, node.col_offset
-                return ast.Assign(targets=[ast.Name(id=var,
-                                                    ctx=ast.Store(),
-                                                    lineno=line,
-                                                    col_offset=column)],
+                positioned_name = ast.Name(id=var, ctx=ast.Store(),
+                                           lineno=line, col_offset=column)
+                temp_variables.append(positioned_name)
+                return ast.Assign(targets=[positioned_name],
                                   value=value,
                                   lineno=line,
                                   col_offset=column)
@@ -1846,7 +1847,7 @@ def make_assign(value, node=None):
                                       orelse=[]))
                 current = current[-1].body
             ret = sum(root, ret)
-            ret += Result(expr=expr_name, temp_variables=[expr_name, name])
+            ret += Result(expr=expr_name, temp_variables=temp_variables)
         else:
             ret += ast.BoolOp(op=opnode(),
                               lineno=root_line,


Test patch:
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -955,6 +955,28 @@
   (and 0 (setv a 2))
   (assert (= a 1)))
 
+(defn test-and-#1151-do []
+  (setv a (and 0 (do 2 3)))
+  (assert (= a 0))
+  (setv a (and 1 (do 2 3)))
+  (assert (= a 3)))
+
+(defn test-and-#1151-for []
+  (setv l [])
+  (setv x (and 0 (for [n [1 2]] (.append l n))))
+  (assert (= x 0))
+  (assert (= l []))
+  (setv x (and 15 (for [n [1 2]] (.append l n))))
+  (assert (= l [1 2])))
+
+(defn test-and-#1151-del []
+  (setv l ["a" "b"])
+  (setv x (and 0 (del (get l 1))))
+  (assert (= x 0))
+  (assert (= l ["a" "b"]))
+  (setv x (and 15 (del (get l 1))))
+  (assert (= l ["a"])))
+
 
 (defn test-or []
   "NATIVE: test the or function"
@@ -973,6 +995,27 @@
   (or 1 (setv a 2))
   (assert (= a 1)))
 
+(defn test-or-#1151-do []
+  (setv a (or 1 (do 2 3)))
+  (assert (= a 1))
+  (setv a (or 0 (do 2 3)))
+  (assert (= a 3)))
+
+(defn test-or-#1151-for []
+  (setv l [])
+  (setv x (or 15 (for [n [1 2]] (.append l n))))
+  (assert (= x 15))
+  (assert (= l []))
+  (setv x (or 0 (for [n [1 2]] (.append l n))))
+  (assert (= l [1 2])))
+
+(defn test-or-#1151-del []
+  (setv l ["a" "b"])
+  (setv x (or 15 (del (get l 1))))
+  (assert (= x 15))
+  (assert (= l ["a" "b"]))
+  (setv x (or 0 (del (get l 1))))
+  (assert (= l ["a"])))
 
 (defn test-xor []
   "NATIVE: test the xor macro"