Repository: hylang/hy

Issue:
Incorrect errors displayed in HyREPL
```hy
hy 0.20.0+89.g33ff26a3.dirty using CPython(default) 3.9.2 on Linux

=> (defn a [a a])
Traceback (most recent call last):
  File "/usr/lib/python3.9/code.py", line 63, in runsource
    code = self.compile(source, filename, symbol)
  File "/usr/lib/python3.9/codeop.py", line 178, in __call__
    return _maybe_compile(self.compiler, source, filename, symbol)
  File "/usr/lib/python3.9/codeop.py", line 143, in __call__
    codeob = compile(source, filename, symbol, self.flags, True)
  File "stdin-c6000d2ccd1df263b2bd313815bfae618dc57cf3", line 1
SyntaxError: duplicate argument 'a' in function definition
None

=> ; but...

=> (defn a [/])
Traceback (most recent call last):
  File "stdin-c4590d5b379ab946ccd4a96ce4e897f3dddb0494", line 1, in <module>
    (defn a [/])
  File "<stdin>", line 1
    (defn a [/])
            ^
hy.errors.HySyntaxError: positional only delimiter '/' must have an argument

=> (defn a [a a])  ; wrong error is printed below!
Traceback (most recent call last):
  File "<stdin>", line 1
    (defn a [/])
            ^
hy.errors.HySyntaxError: positional only delimiter '/' must have an argument
```

I think the error handling in the HyREPL will need a little refactoring to catch this one. The syntax error for duplicate arguments (and possibly other errors) somehow skips our try/except handling that catches most other syntax errors and simplifies stack traces, so when it gets printed it's using stale error info.

Discussion:
Kodiologist: Nice. Try adding a test to `test_bin`, if it's easy. If it turns out to be really involved, don't worry about it.
scauligi: @Kodiologist In addition to the other tests?
Kodiologist: Oh yeah, there already are tests. I guess I'm blind.

Patch:
diff --git a/NEWS.rst b/NEWS.rst
index d51f6697..5c209868 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -18,6 +18,10 @@ Removals
   * From `functools`: `reduce`
   * From `fractions`: `Fraction` (formerly `fraction`)
 
+Bug Fixes
+------------------------------
+* REPL now properly displays SyntaxErrors.
+
 1.0a1 (released 2021-04-12)
 ==============================
 
diff --git a/hy/_compat.py b/hy/_compat.py
index 56cb91c2..ec16fe82 100644
--- a/hy/_compat.py
+++ b/hy/_compat.py
@@ -10,13 +10,6 @@ PY3_9 = sys.version_info >= (3, 9)
 PY3_10 = sys.version_info >= (3, 10)
 
 
-def reraise(exc_type, value, traceback=None):
-    try:
-        raise value.with_traceback(traceback)
-    finally:
-        traceback = None
-
-
 if PY3_9:
 
     import ast
diff --git a/hy/cmdline.py b/hy/cmdline.py
index 88869e13..b120fb46 100644
--- a/hy/cmdline.py
+++ b/hy/cmdline.py
@@ -199,11 +199,18 @@ class HyCompile(codeop.Compile, object):
             sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
             self._update_exc_info()
             exec_code = super(HyCompile, self).__call__(
-                'import hy._compat; hy._compat.reraise('
-                '_hy_last_type, _hy_last_value, _hy_last_traceback)',
+                'raise _hy_last_value.with_traceback(_hy_last_traceback)',
                 name, symbol)
             eval_code = super(HyCompile, self).__call__('None', name, 'eval')
 
+        except SyntaxError:
+            # Capture and save the error before we get to the superclass handler
+            sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info()
+            # Per the python REPL, SyntaxErrors should not display a traceback
+            sys.last_traceback = None
+            self._update_exc_info()
+            raise
+
         return exec_code, eval_code
 
 
diff --git a/hy/compiler.py b/hy/compiler.py
index c60a75f1..7bc4e2be 100755
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -15,7 +15,7 @@ from hy.errors import (HyCompileError, HyTypeError, HyLanguageError,
 
 from hy.lex import mangle, unmangle, hy_parse, parse_one_thing, LexException
 
-from hy._compat import (PY3_8, reraise)
+from hy._compat import PY3_8
 from hy.macros import require, macroexpand
 
 import textwrap
@@ -376,13 +376,13 @@ class HyASTCompiler(object):
             raise
         except HyLanguageError as e:
             # These are expected errors that should be passed to the user.
-            reraise(type(e), e, sys.exc_info()[2])
+            raise e
         except Exception as e:
             # These are unexpected errors that will--hopefully--never be seen
             # by the user.
             f_exc = traceback.format_exc()
             exc_msg = "Internal Compiler Bug 😱\n⤷ {}".format(f_exc)
-            reraise(HyCompileError, HyCompileError(exc_msg), sys.exc_info()[2])
+            raise HyCompileError(exc_msg)
 
     def _syntax_error(self, expr, message):
         return HySyntaxError(message, expr, self.filename, self.source)
@@ -1656,12 +1656,7 @@ class HyASTCompiler(object):
             # the compilation *process* (although compilation did technically
             # fail).
             # We wrap these exceptions and pass them through.
-            reraise(HyEvalError,
-                    HyEvalError(str(e),
-                                self.filename,
-                                body,
-                                self.source),
-                    sys.exc_info()[2])
+            raise HyEvalError(str(e), self.filename, body, self.source)
 
         return (self._compile_branch(body)
                 if mangle(root) == "eval_and_compile"
diff --git a/hy/macros.py b/hy/macros.py
index c10166d0..e80c4fa4 100644
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -10,7 +10,7 @@ import traceback
 
 from contextlib import contextmanager
 
-from hy._compat import reraise, PY3_8
+from hy._compat import PY3_8
 from hy.models import replace_hy_obj, Expression, Symbol, wrap_value
 from hy.lex import mangle, unmangle
 from hy.errors import (HyLanguageError, HyMacroExpansionError, HyTypeError,
@@ -166,7 +166,7 @@ def require(source_module, target_module, assignments, prefix=""):
                 package = None
             source_module = importlib.import_module(source_module, package)
         except ImportError as e:
-            reraise(HyRequireError, HyRequireError(e.args[0]), None)
+            raise HyRequireError(e.args[0]).with_traceback(None)
 
     source_macros = source_module.__dict__.setdefault('__macros__', {})
 
@@ -233,7 +233,7 @@ def macro_exceptions(module, macro_tree, compiler=None):
     except HyLanguageError as e:
         # These are user-level Hy errors occurring in the macro.
         # We want to pass them up to the user.
-        reraise(type(e), e, sys.exc_info()[2])
+        raise
     except Exception as e:
 
         if compiler:
@@ -249,10 +249,7 @@ def macro_exceptions(module, macro_tree, compiler=None):
         msg = "expanding macro {}\n  ".format(str(macro_tree[0]))
         msg += exc_msg
 
-        reraise(HyMacroExpansionError,
-                HyMacroExpansionError(
-                    msg, macro_tree, filename, source),
-                sys.exc_info()[2])
+        raise HyMacroExpansionError(msg, macro_tree, filename, source)
 
 
 def macroexpand(tree, module, compiler=None, once=False):


Test patch:
diff --git a/tests/test_bin.py b/tests/test_bin.py
index 0847f88a..2aee5de6 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -216,6 +216,17 @@ def test_bin_hy_error_parts_length():
     assert err_parts[2] == '    ^----^'
 
 
+def test_bin_hy_syntax_errors():
+    # https://github.com/hylang/hy/issues/2004
+    _, err = run_cmd("hy", "(defn foo [/])\n(defn bar [a a])")
+    assert 'SyntaxError: duplicate argument' in err
+
+    # https://github.com/hylang/hy/issues/2014
+    _, err = run_cmd("hy", "(defn foo []\n(import [re [*]]))")
+    assert 'SyntaxError: import * only allowed' in err
+    assert 'PrematureEndOfInput' not in err
+
+
 def test_bin_hy_stdin_bad_repr():
     # https://github.com/hylang/hy/issues/1389
     output, err = run_cmd("hy", """