Repository: hylang/hy

Issue:
`with` suppressing an exception can break temporary variable management
Consider:

    (defclass E [] []
      (defn __enter__ [self])
      (defn __exit__ [self exc-type exc-value traceback]
        True)) ; Prevent the exception from being propagated

    (defn f []
      (with [(E)]
        (/ 1 0)))

    (f)

This crashes with `UnboundLocalError: local variable '_hy_anon_var_1' referenced before assignment.`, because it compiles to:

    class E:

        def __enter__(self):
            pass

        def __exit__(self, exc_type, exc_value, traceback):
            return True

    def f():
        with E():
            _hy_anon_var_1 = (1 / 0)
        return _hy_anon_var_1
    f()

The exception breaks out of the `with` statement before `_hy_anon_var_1` is assigned, but we still try to `return _hy_anon_var_1`.



Discussion:
Probably the right solution to this is to define `with` as returning `None` when it's jumped out of with an exception, which can be implemented by putting `_hy_anon_var_[N] = None` before the `with` statement.
returning ``None`` sounds like a reasonable approach

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -1452,29 +1452,36 @@ def compile_with_expression(self, expr):
             raise HyTypeError(expr,
                               "with expects a list, received `{0}'".format(
                                   type(args).__name__))
-        if len(args) < 1:
-            raise HyTypeError(expr, "with needs [[arg (expr)]] or [[(expr)]]]")
-
-        args.reverse()
-        ctx = self.compile(args.pop(0))
+        if len(args) not in (1, 2):
+            raise HyTypeError(expr, "with needs [arg (expr)] or [(expr)]")
 
         thing = None
-        if args != []:
+        if len(args) == 2:
             thing = self._storeize(args[0], self.compile(args.pop(0)))
+        ctx = self.compile(args.pop(0))
 
         body = self._compile_branch(expr)
 
+        # Store the result of the body in a tempvar
         var = self.get_anon_var()
         name = ast.Name(id=ast_str(var), arg=ast_str(var),
                         ctx=ast.Store(),
                         lineno=expr.start_line,
                         col_offset=expr.start_column)
-
-        # Store the result of the body in a tempvar
         body += ast.Assign(targets=[name],
                            value=body.force_expr,
                            lineno=expr.start_line,
                            col_offset=expr.start_column)
+        # Initialize the tempvar to None in case the `with` exits
+        # early with an exception.
+        initial_assign = ast.Assign(targets=[name],
+                                    value=ast.Name(
+                                        id=ast_str("None"),
+                                        ctx=ast.Load(),
+                                        lineno=expr.start_line,
+                                        col_offset=expr.start_column),
+                                    lineno=expr.start_line,
+                                    col_offset=expr.start_column)
 
         the_with = ast.With(context_expr=ctx.force_expr,
                             lineno=expr.start_line,
@@ -1486,7 +1493,7 @@ def compile_with_expression(self, expr):
             the_with.items = [ast.withitem(context_expr=ctx.force_expr,
                                            optional_vars=thing)]
 
-        ret = ctx + the_with
+        ret = Result(stmts = [initial_assign]) + ctx + the_with
         ret.contains_yield = ret.contains_yield or body.contains_yield
         # And make our expression context our temp variable
         expr_name = ast.Name(id=ast_str(var), arg=ast_str(var),
@@ -1494,7 +1501,10 @@ def compile_with_expression(self, expr):
                              lineno=expr.start_line,
                              col_offset=expr.start_column)
 
-        ret += Result(expr=expr_name, temp_variables=[expr_name, name])
+        ret += Result(expr=expr_name)
+        # We don't give the Result any temp_vars because we don't want
+        # Result.rename to touch `name`. Otherwise, initial_assign will
+        # clobber any preexisting value of the renamed-to variable.
 
         return ret
 


Test patch:
diff --git a/tests/native_tests/with_test.hy b/tests/native_tests/with_test.hy
--- a/tests/native_tests/with_test.hy
+++ b/tests/native_tests/with_test.hy
@@ -34,12 +34,37 @@
         (assert (= t2 2))
         (assert (= t3 3))))
 
-  (defn test-quince-with []
-    "NATIVE: test four withs, one with no args"
-    (with [t1 (WithTest 1)
-           t2 (WithTest 2)
-           t3 (WithTest 3)
-           _ (WithTest 4)]
-          (assert (= t1 1))
-          (assert (= t2 2))
-          (assert (= t3 3))))
+(defn test-quince-with []
+  "NATIVE: test four withs, one with no args"
+  (with [t1 (WithTest 1)
+         t2 (WithTest 2)
+         t3 (WithTest 3)
+         _ (WithTest 4)]
+        (assert (= t1 1))
+        (assert (= t2 2))
+        (assert (= t3 3))))
+
+(defclass SuppressZDE [object]
+  (defn --enter-- [self])
+  (defn --exit-- [self exc-type exc-value traceback]
+    (and (not (none? exc-type)) (issubclass exc-type ZeroDivisionError))))
+
+(defn test-exception-suppressing-with []
+  ; https://github.com/hylang/hy/issues/1320
+
+  (setv x (with [(SuppressZDE)] 5))
+  (assert (= x 5))
+
+  (setv y (with [(SuppressZDE)] (/ 1 0)))
+  (assert (none? y))
+
+  (setv z (with [(SuppressZDE)] (/ 1 0) 5))
+  (assert (none? z))
+
+  (defn f [] (with [(SuppressZDE)] (/ 1 0)))
+  (assert (none? (f)))
+
+  (setv w 7  l [])
+  (setv w (with [(SuppressZDE)] (.append l w) (/ 1 0) 5))
+  (assert (none? w))
+  (assert (= l [7])))