Repository: hylang/hy

Issue:
More misplaced tracebacks
There are cases not fixed by #2423 in which position information is still set incorrectly:

```
$ echo ';;;;;;\n(do-mac (quote (raise (ValueError))))' >ex.hy
$ hy ex.hy 
Traceback (most recent call last):
  …
  File "/tmp/ex.hy", line 1, in <module>
    ;;;;;;
ValueError
```

```
$ echo ';;;;;\n(defmacro m [] (quote (do (raise (ValueError)))))\n(m)' >ex.hy                      
$ hy ex.hy
Traceback (most recent call last):
  …
  File "/tmp/ex.hy", line 1, in <module>
    ;;;;;
ValueError
```

(Note that the `do` is necessary to reproduce the bug in the second case.)

These don't look so easy to fix. I think #1951 may have been ill-advised: producing bogus default positions for models without positions leads to bugs that are harder to track down than crashes that occur immediately upon constructing the AST. But ultimately there may be no reasonable positions to assign to some dynamically generated models, in which case Python still needs something. I'm not sure.

I've opened https://github.com/pytest-dev/pytest/pull/10840 to prevent pytest from crashing on this and any other cases. (Edit: This was merged, and indeed pytest no longer seems to have those crashes.)


Discussion:

Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -422,7 +422,7 @@ def compile(self, tree):
             # 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)
+            exc_msg = "Internal Compiler Bug\n {}".format(f_exc)
             raise HyCompileError(exc_msg)
 
     def _syntax_error(self, expr, message):
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
@@ -135,7 +135,7 @@ def compile_eval_foo_compile(compiler, expr, root, body):
         raise HyEvalError(str(e), compiler.filename, body, compiler.source)
 
     return (
-        compiler.compile(as_model(value))
+        compiler.compile(as_model(value).replace(expr))
         if root == "do-mac"
         else compiler._compile_branch(body)
         if root == "eval-and-compile"
diff --git a/hy/models.py b/hy/models.py
--- a/hy/models.py
+++ b/hy/models.py
@@ -385,12 +385,19 @@ class Sequence(Object, tuple):
     for this purpose.
     """
 
+    _extra_kwargs = ()
+
     def replace(self, other, recursive=True):
-        if recursive:
-            for x in self:
-                replace_hy_obj(x, other)
-        Object.replace(self, other)
-        return self
+        return (
+            Object.replace(
+                Object.replace(
+                    type(self)(
+                        (replace_hy_obj(x, other) for x in self),
+                        **{k: getattr(self, k) for k in self._extra_kwargs}),
+                    self),
+                other)
+            if recursive
+            else Object.replace(self, other))
 
     def __add__(self, other):
         return self.__class__(
@@ -431,6 +438,8 @@ class FComponent(Sequence):
     format spec (if any).
     """
 
+    _extra_kwargs = ("conversion",)
+
     def __new__(cls, s=None, conversion=None):
         value = super().__new__(cls, s)
         value.conversion = conversion
@@ -465,6 +474,8 @@ class FString(Sequence):
     :ivar brackets: As in :class:`hy.models.String`.
     """
 
+    _extra_kwargs = ("brackets",)
+
     def __new__(cls, s=None, brackets=None):
         value = super().__new__(
             cls,


Test patch:
diff --git a/tests/test_positions.py b/tests/test_positions.py
new file mode 100644
--- /dev/null
+++ b/tests/test_positions.py
@@ -0,0 +1,27 @@
+'''Check that positioning attributes for AST nodes (which Python
+ultimately uses for tracebacks) are set correctly.'''
+
+import ast
+from hy import read_many
+from hy.compiler import hy_compile
+
+
+def cpl(string):
+    '''Compile the Hy `string` and get its final body element. A
+    newline is prepended so that line 1 is guaranteed to be the wrong
+    position for generated nodes.'''
+    return hy_compile(read_many('\n' + string), __name__).body[-1]
+
+
+def test_do_mac():
+    # https://github.com/hylang/hy/issues/2424
+    x = cpl("(do-mac '9)")
+    assert isinstance(x, ast.Expr)
+    assert x.lineno == 2
+
+
+def test_defmacro_raise():
+    # https://github.com/hylang/hy/issues/2424
+    x = cpl("(defmacro m [] '(do (raise)))\n(m)")
+    assert isinstance(x, ast.Raise)
+    assert x.lineno == 3