Repository: hylang/hy

Issue:
"else" does not return a value from a try form
I would expect this:

```
(defn x []
  (try
    (some-call)
    (catch [e Exception] 1)
    (else 2)))
```

To compile to this:

```
def x():
    try:
        _hy_anon_var_1 = some_call()
    except Exception as e:
        _hy_anon_var_1 = 1L
    else:
        _hy_anon_var_1 = 2L
    return _hy_anon_var_1
```

But it compiles to this:

```
def x():
    try:
        _hy_anon_var_1 = some_call()
    except Exception as e:
        _hy_anon_var_1 = 1L
    else:
        2L
    return _hy_anon_var_1
```

Note that "else" does not produce a return value for the function. Same result for s/else/finally.



Discussion:
I'd argue that `finally` is right not to return a value. In my experience it's almost always used for final cleanup operations:

```
(try
  (something-that-might-break)
  (finally
    (clean-up)))
```

We're more interested in the return value of `(something-that-might-break)` than we're interested in the return value of `(clean-up)`.

These are my 2 cents, I completely concur on `(else)`.

@farhaven good point! 

(I had actually started working on this Issue (temporary work-in-progress commit: zackmdavis/hy@5e0c0ae3), but I'm not done yet.)

Let's turn to Clojure for guidance. Clojure is using Java's `try/catch/finally` exception system like Hy is using Python's `try/except/finally` statements.

``` Clojure
user=> (try :foo (catch Exception e :caught!)(finally (println "FINALLY!") :returned?))
FINALLY!
:foo
user=> (try (/ 0) (catch Exception e :caught!)(finally (println "FINALLY!") :returned?))
FINALLY!
:caught!
```

We see that the `finally` form is only evaluated for side effects as `returned?` is never returned. On the other hand, the `catch` form can return a value, as in the second example, but we would lose this if `finally` returned something else. So I also agree that `finally` ought not to return its value in Hy.

I don't think Clojure has an equivalent of the optional `else` part, so that's no help. But consider that as with `do/progn`, `try` can evaluate multiple expressions for side effects and will return the value of the last one. If there are no exceptions raised, the `else` part is kind of a continuation of the `try` block in Python. Therefore, I would argue that in the usual case that no exceptions are raised, the `try` should evaluate to the value of the last expression in the `else` form (if present).

Just tripped over this. I agree with the interpretation of `else` as continuation of the `try` block in case no exception occurred and that the result of `else` should be the result of the `try` then.
Tested in [`test-try-else-return`](https://github.com/hylang/hy/blob/eeb0be8fb0bdbd9d7d23dc908be2d24d4e475d0a/tests/native_tests/language.hy#L1174).

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -769,13 +769,8 @@ def compile_raise_expression(self, expr):
     def compile_try_expression(self, expr):
         expr.pop(0)  # try
 
-        try:
-            body = expr.pop(0)
-        except IndexError:
-            body = []
-
         # (try something…)
-        body = self.compile(body)
+        body = self.compile(expr.pop(0) if expr else [])
 
         var = self.get_anon_var()
         name = ast.Name(id=ast_str(var), arg=ast_str(var),
@@ -791,55 +786,56 @@ def compile_try_expression(self, expr):
         returnable = Result(expr=expr_name, temp_variables=[expr_name, name],
                             contains_yield=body.contains_yield)
 
-        body += ast.Assign(targets=[name],
-                           value=body.force_expr,
-                           lineno=expr.start_line,
-                           col_offset=expr.start_column)
-
-        body = body.stmts
-        if not body:
-            body = [ast.Pass(lineno=expr.start_line,
-                             col_offset=expr.start_column)]
-
+        if not all(expr):
+            raise HyTypeError(expr, "Empty list not allowed in `try'")
+        handler_results = Result()
+        handlers = []
+        while expr and expr[0][0] == HySymbol("except"):
+            handler_results += self._compile_catch_expression(expr.pop(0),
+                                                              name)
+            handlers.append(handler_results.stmts.pop())
         orelse = []
+        if expr and expr[0][0] == HySymbol("else"):
+            orelse = self._compile_branch(expr.pop(0)[1:])
+            orelse += ast.Assign(targets=[name],
+                                 value=orelse.force_expr,
+                                 lineno=expr.start_line,
+                                 col_offset=expr.start_column)
+            orelse += orelse.expr_as_stmt()
+            orelse = orelse.stmts
         finalbody = []
-        handlers = []
-        handler_results = Result()
-
-        for e in expr:
-            if not len(e):
-                raise HyTypeError(e, "Empty list not allowed in `try'")
-
-            if e[0] == HySymbol("except"):
-                handler_results += self._compile_catch_expression(e, name)
-                handlers.append(handler_results.stmts.pop())
-            elif e[0] == HySymbol("else"):
-                orelse = self.try_except_helper(e, HySymbol("else"), orelse)
-            elif e[0] == HySymbol("finally"):
-                finalbody = self.try_except_helper(e, HySymbol("finally"),
-                                                   finalbody)
-            else:
-                raise HyTypeError(e, "Unknown expression in `try'")
+        if expr and expr[0][0] == HySymbol("finally"):
+            finalbody = self._compile_branch(expr.pop(0)[1:])
+            finalbody += finalbody.expr_as_stmt()
+            finalbody = finalbody.stmts
+        if expr:
+            if expr[0][0] in ("except", "else", "finally"):
+                raise HyTypeError(expr, "Incorrect order "
+                                  "of `except'/`else'/`finally' in `try'")
+            raise HyTypeError(expr, "Unknown expression in `try'")
 
         # Using (else) without (except) is verboten!
         if orelse and not handlers:
             raise HyTypeError(
-                e,
+                expr,
                 "`try' cannot have `else' without `except'")
-
-        # (try) or (try BODY)
-        # Generate a default handler for Python >= 3.3 and pypy
-        if not handlers and not finalbody and not orelse:
-            handlers = [ast.ExceptHandler(
-                lineno=expr.start_line,
-                col_offset=expr.start_column,
-                type=None,
-                name=None,
-                body=[ast.Raise(lineno=expr.start_line,
-                                col_offset=expr.start_column)])]
+        # Likewise a bare (try) or (try BODY).
+        if not (handlers or finalbody):
+            raise HyTypeError(
+                expr,
+                "`try' must have an `except' or `finally' clause")
 
         ret = handler_results
 
+        body += body.expr_as_stmt() if orelse else ast.Assign(
+            targets=[name],
+            value=body.force_expr,
+            lineno=expr.start_line,
+            col_offset=expr.start_column)
+
+        body = body.stmts or [ast.Pass(lineno=expr.start_line,
+                                       col_offset=expr.start_column)]
+
         if PY3:
             # Python 3.3 features a merge of TryExcept+TryFinally into Try.
             return ret + ast.Try(
@@ -876,17 +872,6 @@ def compile_try_expression(self, expr):
             body=body,
             orelse=orelse) + returnable
 
-    def try_except_helper(self, hy_obj, symbol, accumulated):
-        if accumulated:
-            raise HyTypeError(
-                hy_obj,
-                "`try' cannot have more than one `%s'" % symbol)
-        else:
-            accumulated = self._compile_branch(hy_obj[1:])
-            accumulated += accumulated.expr_as_stmt()
-            accumulated = accumulated.stmts
-        return accumulated
-
     @builds("except")
     def magic_internal_form(self, expr):
         raise HyTypeError(expr,
@@ -895,10 +880,7 @@ def magic_internal_form(self, expr):
     def _compile_catch_expression(self, expr, var):
         catch = expr.pop(0)  # catch
 
-        try:
-            exceptions = expr.pop(0)
-        except IndexError:
-            exceptions = HyList()
+        exceptions = expr.pop(0) if expr else HyList()
 
         # exceptions catch should be either:
         # [[list of exceptions]]
@@ -920,6 +902,7 @@ def _compile_catch_expression(self, expr, var):
 
         # [variable [list of exceptions]]
         # let's pop variable and use it as name
+        name = None
         if len(exceptions) == 2:
             name = exceptions.pop(0)
             if not isinstance(name, HySymbol):
@@ -937,13 +920,8 @@ def _compile_catch_expression(self, expr, var):
             else:
                 # Python2 requires an ast.Name, set to ctx Store.
                 name = self._storeize(name, self.compile(name))
-        else:
-            name = None
 
-        try:
-            exceptions_list = exceptions.pop(0)
-        except IndexError:
-            exceptions_list = []
+        exceptions_list = exceptions.pop(0) if exceptions else []
 
         if isinstance(exceptions_list, list):
             if len(exceptions_list):


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
@@ -113,23 +113,26 @@ def test_ast_bad_raise():
 
 def test_ast_good_try():
     "Make sure AST can compile valid try"
-    can_compile("(try)")
-    can_compile("(try 1)")
     can_compile("(try 1 (except) (else 1))")
-    can_compile("(try 1 (else 1) (except))")
-    can_compile("(try 1 (finally 1) (except))")
     can_compile("(try 1 (finally 1))")
     can_compile("(try 1 (except) (finally 1))")
-    can_compile("(try 1 (except) (finally 1) (else 1))")
+    can_compile("(try 1 (except [x]) (except [y]) (finally 1))")
     can_compile("(try 1 (except) (else 1) (finally 1))")
+    can_compile("(try 1 (except [x]) (except [y]) (else 1) (finally 1))")
 
 
 def test_ast_bad_try():
     "Make sure AST can't compile invalid try"
+    cant_compile("(try)")
+    cant_compile("(try 1)")
     cant_compile("(try 1 bla)")
     cant_compile("(try 1 bla bla)")
+    cant_compile("(try (do bla bla))")
     cant_compile("(try (do) (else 1) (else 2))")
     cant_compile("(try 1 (else 1))")
+    cant_compile("(try 1 (else 1) (except))")
+    cant_compile("(try 1 (finally 1) (except))")
+    cant_compile("(try 1 (except) (finally 1) (else 1))")
 
 
 def test_ast_good_except():
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
@@ -421,21 +421,10 @@
   "NATIVE: test do"
   (do))
 
-(defn test-bare-try [] (try
-    (try (raise ValueError))
-  (except [ValueError])
-  (else (assert False))))
-
 
 (defn test-exceptions []
   "NATIVE: test Exceptions"
 
-  (try)
-
-  (try (do))
-
-  (try (do))
-
   (try (do) (except))
 
   (try (do) (except [IOError]) (except))
@@ -1174,20 +1163,34 @@
   (assert (= foo 4)))
 
 
-#@(pytest.mark.xfail
 (defn test-try-else-return []
   "NATIVE: test that we can return from the `else` clause of a `try`"
   ; https://github.com/hylang/hy/issues/798
+
   (assert (= "ef" ((fn []
     (try (+ "a" "b")
       (except [NameError] (+ "c" "d"))
       (else (+ "e" "f")))))))
+
   (setv foo
     (try (+ "A" "B")
       (except [NameError] (+ "C" "D"))
       (else (+ "E" "F"))))
-  (assert (= foo "EF"))))
-
+  (assert (= foo "EF"))
+
+  ; Check that the lvalue isn't assigned in the main `try` body
+  ; there's an `else`.
+  (setv x 1)
+  (setv y 0)
+  (setv x
+    (try (+ "G" "H")
+      (except [NameError] (+ "I" "J"))
+      (else
+        (setv y 1)
+        (assert (= x 1))
+        (+ "K" "L"))))
+  (assert (= x "KL"))
+  (assert (= y 1)))
 
 (defn test-require []
   "NATIVE: test requiring macros from python code"