Repository: hylang/hy

Issue:
Bodies should be optional in more places where Python accepts `pass`
I notice in particular that Hy doesn't allow `(while (f))` or `(for [x (generator)])`.


Discussion:
Python takes `pass` literally anywhere a statement is allowed. (Notably, not in the "body" of `lambda`.) But in Hy everything is an expression. What value should `(while (f))` evaluate to? `None`? or the result of `(f)`? What should an empty `for` evaluate to?

Should we allow empty `try` forms too like `(try (except))` or `(try (finally))`? A `pass` is allowed there. But what should they evaluate to?

I think the other forms corresponding to Python reserved words with bodies already work with empty bodies. We should take a look at our core macros though.
`while` and `for` return `None` in every other case. I see no reason to make an exception for an empty body.

I don't see the point of a `try` with no body. If I understand correctly, none of the `except` clauses can run, and any `else` or `finally` is run just the same as if you didn't have a `try`.
> while and for return None in every other case.

Good point.
> the point of a try with no body.

If it's written directly in production that way, yeah, it's got to be an error. But during development, you might comment stuff out like
```Hy
(try
  ;; (risky-foo)
  (finally
    (stuff-I-still-want-running))
```
Restructuring the entire form to pull out the finally when you just want to disable one line is a pain. In Python, you'd have to add a `pass` if it was the entire body though. We actually have a `comment` macro that substitutes a `None` for this kind of thing. But the line-wise `;` or form-wise `#_` would have the same problem. You'd need to add a `None` or something.

There are certain tradeoffs between flexibility and enforcing correctness for this kind of thing, but in Lisp, because of macros, we want to avoid special cases more than we want to enforce correctness for this kind of thing. If a macro expands to a `try` form, and the body could reasonably be empty for the macro's semantics, then it's an additional special case we're forcing the macro writer to handle unnecessarily.
Yeah, that makes sense. 

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1820,7 +1820,7 @@ def compile_for_expression(self, expression):
         ret += node(expression,
                     target=target,
                     iter=ret.force_expr,
-                    body=body.stmts,
+                    body=body.stmts or [asty.Pass(expression)],
                     orelse=orel.stmts)
 
         ret.contains_yield = body.contains_yield
@@ -1828,7 +1828,7 @@ def compile_for_expression(self, expression):
         return ret
 
     @builds("while")
-    @checkargs(min=2)
+    @checkargs(min=1)
     def compile_while_expression(self, expr):
         expr.pop(0)  # "while"
         cond = expr.pop(0)
@@ -1867,7 +1867,8 @@ def e(*x): return HyExpression(x)
 
         ret = cond_compiled + asty.While(
             expr, test=cond_compiled.force_expr,
-            body=body.stmts, orelse=orel.stmts)
+            body=body.stmts or [asty.Pass(expr)],
+            orelse=orel.stmts)
         ret.contains_yield = body.contains_yield
 
         return ret


Test patch:
diff --git a/tests/compilers/test_ast.py b/tests/compilers/test_ast.py
--- a/tests/compilers/test_ast.py
+++ b/tests/compilers/test_ast.py
@@ -97,7 +97,6 @@ def test_ast_invalid_unary_op():
 def test_ast_bad_while():
     "Make sure AST can't compile invalid while"
     cant_compile("(while)")
-    cant_compile("(while (True))")
 
 
 def test_ast_good_do():
@@ -535,14 +534,6 @@ def test_for_compile_error():
         can_compile("(fn [] (for [x] x))")
     assert excinfo.value.message == "`for' requires an even number of args."
 
-    with pytest.raises(HyTypeError) as excinfo:
-        can_compile("(fn [] (for [x xx]))")
-    assert excinfo.value.message == "`for' requires a body to evaluate"
-
-    with pytest.raises(HyTypeError) as excinfo:
-        can_compile("(fn [] (for [x xx] (else 1)))")
-    assert excinfo.value.message == "`for' requires a body to evaluate"
-
 
 def test_attribute_access():
     """Ensure attribute access compiles correctly"""
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
@@ -232,7 +232,15 @@
   (assert (= (list ((fn [] (for [x [[1] [2 3]] y x] (yield y)))))
              (list-comp y [x [[1] [2 3]] y x])))
   (assert (= (list ((fn [] (for [x [[1] [2 3]] y x z (range 5)] (yield z)))))
-             (list-comp z [x [[1] [2 3]] y x z (range 5)]))))
+             (list-comp z [x [[1] [2 3]] y x z (range 5)])))
+
+  (setv l [])
+  (defn f []
+    (for [x [4 9 2]]
+      (.append l (* 10 x))
+      (yield x)))
+  (for [_ (f)])
+  (assert (= l [40 90 20])))
 
 
 (defn test-nasty-for-nesting []
@@ -283,7 +291,21 @@
     (setv fact (* fact count))
     (setv count (- count 1)))
   (assert (= count 0))
-  (assert (= fact 120)))
+  (assert (= fact 120))
+
+  (setv l [])
+  (defn f []
+    (.append l 1)
+    (len l))
+  (while (!= (f) 4))
+  (assert (= l [1 1 1 1]))
+
+  (setv l [])
+  (defn f []
+    (.append l 1)
+    (len l))
+  (while (!= (f) 4) (do))
+  (assert (= l [1 1 1 1])))
 
 (defn test-while-loop-else []
   (setv count 5)