Repository: hylang/hy

Issue:
Python statements are executed unconditionally
When translating a Hy expression into a Python statement (`print`, `yield`, ...), it gets executed unconditionally. For instance:

``` clj
=> (and False (do (print "hello") True))
hello
False
```

More info can be found in [this discussion](https://github.com/hylang/hy/pull/222#discussion_r5041876).

and/or doesn't always short-circuit
Example:

```
ryan@DevPC-LX:~$ hy --spy
hy 0.10.1 using CPython(default) 2.7.6 on Linux
=> (and 0 (assert 0))
assert 0L
(0L and None)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AssertionError
=> (or 1 (assert 0))
assert 0L
(1L or None)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
AssertionError
=> 
```

I have a potential fix [here](https://github.com/kirbyfan64/hy/commit/d640f5e4a86f3ea1a18b7b8376794fb423df7612) that wraps the expression sides in lambdas, but it makes `defmacro/g!` and therefore 2.7's `yield-from` crash...



Discussion:
If you try it through hy2py, it turns into

``` Python
print u'hello'
(False and True)
```

(Which, frankly, I don't see as bad)

Due to Python's distinction between expr and stmt. It's just a side-effect, and the fact it stops at the first isn't _really_ that important.

We can wrap it in a function or something if we need to, but I'm not totally convinced it's needed yet.

I don't know how to use hy2py (can you point me to a doc or even show me how to install/run it?) but I'm pretty sure you'll get a separate behavior in Python3 where `print` is a function.

We could circumvent the issue by importing the function from `__future__` (cc @olasd) but it won't solve the issue for `yield` and/or other statements.

Should we care that we have the same behavior for all versions of Python?

so print function won't solve the general problem with stmts in an expr place in the Python AST.

`hy2py` can be run by invoking `./bin/hy2py file.hy` in place in the git repo :)

Thanks. It's worth mentioning that `hy2py` won't work with Py3 because `astor` isn't Py3-ready :-(

So what's your take on the discrepancies between Py2/Py3? Does it justify confirming this bug?

There's nothing we can do about changed behavior - it's like saying there's an issue because two bits of Python code act differently on 2.x to 3.x, it's just a fact of  Python life :)

We need to take the Pythonic "We're all consenting adults" approach and trust the programmer can make sound decisions that work for their usecase :)

We make small accommodations (future division, etc), but on the whole, I think we can trust the programmer knows what version of Python the code is running under

This is a major issue of Hy IMHO. You can't just consider this as a side effect and let it be. This is going to bite a lot of people, and documenting isn't going to be enough, this has to be fixed.

I'd say that writing `and' as a macro might be a way to fix it.

> I'd say that writing `and' as a macro might be a way to fix it.

Is it just a problem with `and` (and `or`)? Aren't there other ways of deferring evaluation where statements might get "accidentally" executed?

What's the issue with transforming statements into a function?

First off, I'll note that I'm firmly of the opinion that the current behavior (I implemented) is a stop-gap before a proper solution can be implemented. Leaking statements that wouldn't be executed if they were "expressions" is confusing behavior.

Writing `and` as a macro would not solve the general problem (and I think the perf hit wouldn't be worth it)

Transforming statements into a function means creating a closure, which means losing the possibility of effecting on the global namespace. Which I think would be confusing too.

One solution for this problem, would be to resort to a hack hooking `storeize` to collect variable names and using `global` to enable access to the toplevel names in the closure (making it leak). But that sounds gross.

I'm open to less-gross ideas if you have any.

I believe these are some other manifestations of the same issue:

``` hy
(setv x 1)
(setv y (+= x 1))
(print y)
```

That should print 2, but it prints None.

``` hy
(setv l ["a" "b" "c"])
(defn f [a]
  (print "hi")
  a)
(setv y (setv (get l (f 0)) "A"))
(print y)
```

That should print "hi" only once, but it prints "hi" twice.



Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1669,17 +1669,78 @@ def compile_require(self, expression):
 
     @builds("and")
     @builds("or")
-    @checkargs(min=2)
     def compile_logical_or_and_and_operator(self, expression):
-        ops = {"and": ast.And,
-               "or": ast.Or}
+        ops = {"and": (ast.And, "True"),
+               "or": (ast.Or, "None")}
         operator = expression.pop(0)
-        values, ret, _ = self._compile_collect(expression)
+        opnode, default = ops[operator]
+        root_line, root_column = operator.start_line, operator.start_column
+        if len(expression) == 0:
+            return ast.Name(id=default,
+                            ctx=ast.Load(),
+                            lineno=root_line,
+                            col_offset=root_column)
+        elif len(expression) == 1:
+            return self.compile(expression[0])
+        ret = Result()
+        values = list(map(self.compile, expression))
+        has_stmt = any(value.stmts for value in values)
+        if has_stmt:
+            # Compile it to an if...else sequence
+            var = self.get_anon_var()
+            name = ast.Name(id=var,
+                            ctx=ast.Store(),
+                            lineno=root_line,
+                            col_offset=root_column)
+            expr_name = ast.Name(id=var,
+                                 ctx=ast.Load(),
+                                 lineno=root_line,
+                                 col_offset=root_column)
 
-        ret += ast.BoolOp(op=ops[operator](),
-                          lineno=operator.start_line,
-                          col_offset=operator.start_column,
-                          values=values)
+            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)],
+                                  value=value,
+                                  lineno=line,
+                                  col_offset=column)
+            root = []
+            current = root
+            for i, value in enumerate(values):
+                if value.stmts:
+                    node = value.stmts[0]
+                    current.extend(value.stmts)
+                else:
+                    node = value.expr
+                current.append(make_assign(value.force_expr, value.force_expr))
+                if i == len(values)-1:
+                    # Skip a redundant 'if'.
+                    break
+                if operator == "and":
+                    cond = expr_name
+                elif operator == "or":
+                    cond = ast.UnaryOp(op=ast.Not(),
+                                       operand=expr_name,
+                                       lineno=node.lineno,
+                                       col_offset=node.col_offset)
+                current.append(ast.If(test=cond,
+                                      body=[],
+                                      lineno=node.lineno,
+                                      col_offset=node.col_offset,
+                                      orelse=[]))
+                current = current[-1].body
+            ret = sum(root, ret)
+            ret += Result(expr=expr_name, temp_variables=[expr_name, name])
+        else:
+            ret += ast.BoolOp(op=opnode(),
+                              lineno=root_line,
+                              col_offset=root_column,
+                              values=[value.force_expr for value in values])
         return ret
 
     @builds("=")


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
@@ -765,19 +765,35 @@
 (defn test-and []
   "NATIVE: test the and function"
   (let [[and123 (and 1 2 3)]
-        [and-false (and 1 False 3)]]
+        [and-false (and 1 False 3)]
+        [and-true (and)]
+        [and-single (and 1)]]
     (assert (= and123 3))
-    (assert (= and-false False))))
+    (assert (= and-false False))
+    (assert (= and-true True))
+    (assert (= and-single 1)))
+  ; short circuiting
+  (setv a 1)
+  (and 0 (setv a 2))
+  (assert (= a 1)))
 
 
 (defn test-or []
   "NATIVE: test the or function"
   (let [[or-all-true (or 1 2 3 True "string")]
         [or-some-true (or False "hello")]
-        [or-none-true (or False False)]]
+        [or-none-true (or False False)]
+        [or-false (or)]
+        [or-single (or 1)]]
     (assert (= or-all-true 1))
     (assert (= or-some-true "hello"))
-    (assert (= or-none-true False))))
+    (assert (= or-none-true False))
+    (assert (= or-false nil))
+    (assert (= or-single 1)))
+  ; short circuiting
+  (setv a 1)
+  (or 1 (setv a 2))
+  (assert (= a 1)))
 
 
 (defn test-if-return-branching []