Repository: hylang/hy

Issue:
Statements pulled out of `assert` still execute under `python3 -O`
```sh
$ echo '(assert (do (print "hi") 1))' >ex.hy
$ python3 -Oc 'import hy, ex'
hi
```

The cause is pretty straightforward: after statements are pulled out of the `assert`, Python doesn't know it's supposed to remove them. This can probably be fixed by guarding the statements with `if __debug__: …`.


Discussion:
Interestingly:
```sh
$ echo '(assert (do (print "hi") 1) "assert message")' >ex.hy
$ python3 -Oc 'import hy, ex'
<no output>
$ python3 -c 'import hy, ex'
hi
```
Running hy2py:
```python
import hy
_hy_anon_var_2 = __debug__
if _hy_anon_var_2:
    print('hi')
    _hy_anon_var_2 = not 1
if _hy_anon_var_2:
    _hy_anon_var_1 = 'assert message'
    assert False, _hy_anon_var_1
    _hy_anon_var_3 = None
else:
    _hy_anon_var_3 = None
```

So we're already (manually) gating on `__debug__` when there's an assert message, but we're not properly encapsulating a message-less `assert`.

Resolving patch:
diff --git a/hy/core/result_macros.py b/hy/core/result_macros.py
--- a/hy/core/result_macros.py
+++ b/hy/core/result_macros.py
@@ -1833,27 +1833,33 @@ def compile_import(compiler, expr, root, entries):
 
 @pattern_macro("assert", [FORM, maybe(FORM)])
 def compile_assert_expression(compiler, expr, root, test, msg):
-    if msg is None or type(msg) is Symbol:
-        ret = compiler.compile(test)
-        return ret + asty.Assert(
-            expr,
-            test=ret.force_expr,
-            msg=(None if msg is None else compiler.compile(msg).force_expr),
-        )
+    test = compiler.compile(test)
+    if msg:
+        msg = compiler.compile(msg)
 
-    # The `msg` part may involve statements, which we only
-    # want to be executed if the assertion fails. Rewrite the
-    # form to set `msg` to a variable.
-    msg_var = compiler.get_anon_var()
-    return compiler.compile(
-        mkexpr(
-            "if",
-            mkexpr("and", "__debug__", mkexpr("not", [test])),
-            mkexpr(
-                "do", mkexpr("setv", msg_var, [msg]), mkexpr("assert", "False", msg_var)
-            ),
-            Symbol("None"),
-        ).replace(expr)
+    if not (test.stmts or (msg and msg.stmts)):
+        return asty.Assert(expr, test=test.force_expr, msg=msg and msg.force_expr)
+
+    return asty.If(
+        expr,
+        test=asty.Name(expr, id="__debug__", ctx=ast.Load()),
+        orelse=[],
+        body=test.stmts
+        + [
+            asty.If(
+                test,
+                test=asty.UnaryOp(test, op=ast.Not(), operand=test.force_expr),
+                orelse=[],
+                body=(msg.stmts if msg else [])
+                + [
+                    asty.Assert(
+                        expr,
+                        test=asty.Constant(test, value=False),
+                        msg=msg and msg.force_expr,
+                    )
+                ],
+            )
+        ],
     )
 
 


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
@@ -1537,16 +1537,6 @@ cee"} dee" "ey bee\ncee dee"))
    (= (identify-keywords 1 "bloo" :foo)
       ["other" "other" "keyword"])))
 
-(defn test-assert-multistatements []
-  ; https://github.com/hylang/hy/issues/1390
-  (setv l [])
-  (defn f [x]
-    (.append l x)
-    False)
-  (with [(pytest.raises AssertionError)]
-    (assert (do (f 1) (f 2)) (do (f 3) (f 4))))
-  (assert (= l [1 2 3 4])))
-
 (defn test-underscore_variables []
   ; https://github.com/hylang/hy/issues/1340
   (defclass XYZ []
diff --git a/tests/test_bin.py b/tests/test_bin.py
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -691,3 +691,29 @@ def file_is(arg, expected_py3_9):
         tmp_path / "realdir"
         if platform.system() == "Windows" else
         tmp_path / "realdir" / "child" / "..")
+
+
+def test_assert(tmp_path, monkeypatch):
+    # Check when statements pulled out of `assert` are run.
+    # https://github.com/hylang/hy/issues/1390
+    # https://github.com/hylang/hy/issues/2319
+
+    monkeypatch.chdir(tmp_path)
+
+    for has_msg in False, True:
+
+        Path("ex.hy").write_text(
+            "(defn f [test] (assert {} {}))".format(
+               '(do (print "testing") test)',
+               '(do (print "msging") "bye")' if has_msg else ""))
+
+        for optim, test in [(0, 0), (0, 1), (1, 0), (1, 1)]:
+            out, err = run_cmd(
+                cmd = "python3 {} {}".format(
+                    ("-O" if optim else ""),
+                    f"-c 'import hy, ex; ex.f({test})'"),
+                expect = (1 if not optim and not test else 0))
+            assert ("testing" in out) == (not optim)
+            show_msg = has_msg and not optim and not test
+            assert ("msging" in out) == show_msg
+            assert ("bye" in err) == show_msg