Repository: hylang/hy

Issue:
Weirdness with result macros and `.` access
Invoking some of the result macros with an unnecessary attribute access somehow gets hy to make them async:

```python
# (defn normal [])
def normal():
    pass

# ((. defn) weird [])
async def weird():
    pass


# (with [normal])
_hy_anon_var_1 = None
with normal:
    _hy_anon_var_1 = None

# ((. with) [weird])
_hy_anon_var_2 = None
async with weird:
    _hy_anon_var_2 = None


# (fn/a [] (do 0 normal))
async def _hy_anon_var_3():
    0
    return normal

# ((. fn/a) [] (do 0 weird))
def _hy_anon_var_4():
    0
    return weird

# ((. fn) [] (do 0 weirdly-normal))
def _hy_anon_var_5():
    0
    return weirdly_normal
```

It's even more strange to me that `fn` has the _opposite_ behavior of `defn` and `with`.
There might be other examples as well, these are just the ones I stumbled across.


Discussion:
It boils down to e.g. `with` saying

```python
node = asty.With if root == "with" else asty.AsyncWith
```

and `root` being `(. with)` instead of `with`. `fn` is the odd one out because I wrote the conditional the other way in that case.

I would say the underlying bug is that `((. foo) a)` can call a macro `foo`, instead of only treating `foo` as a Python object, as would be more reasonable.
Ah, but we need `((. foo bar) x)` to be able to call a macro named `foo.bar`, because that's how `(foo.bar x)` is parsed. So it does indeed seem correct that `((. foo) x)` should be exactly equivalent to `(foo x)`. In that case, I think I just need to ensure `root` is set correctly.

Resolving patch:
diff --git a/hy/macros.py b/hy/macros.py
--- a/hy/macros.py
+++ b/hy/macros.py
@@ -52,13 +52,12 @@ def wrapper(hy_compiler, *args):
                     ).replace(hy_compiler.this)
 
                 expr = hy_compiler.this
-                root = unmangle(expr[0])
 
                 if py_version_required and sys.version_info < py_version_required:
                     raise hy_compiler._syntax_error(
                         expr,
                         "`{}` requires Python {} or later".format(
-                            root, ".".join(map(str, py_version_required))
+                            name, ".".join(map(str, py_version_required))
                         ),
                     )
 
@@ -68,10 +67,10 @@ def wrapper(hy_compiler, *args):
                     raise hy_compiler._syntax_error(
                         expr[min(e.state.pos + 1, len(expr) - 1)],
                         "parse error for pattern macro '{}': {}".format(
-                            root, e.msg.replace("end of input", "end of macro call")
+                            name, e.msg.replace("end of input", "end of macro call")
                         ),
                     )
-                return fn(hy_compiler, expr, root, *parse_tree)
+                return fn(hy_compiler, expr, name, *parse_tree)
 
             return wrapper
 


Test patch:
diff --git a/tests/native_tests/functions.hy b/tests/native_tests/functions.hy
--- a/tests/native_tests/functions.hy
+++ b/tests/native_tests/functions.hy
@@ -189,6 +189,15 @@
   (assert (= (asyncio.run (coro-test)) [1 2 3])))
 
 
+(defn test-root-set-correctly []
+  ; https://github.com/hylang/hy/issues/2475
+  ((. defn) not-async [] "ok")
+  (assert (= (not-async) "ok"))
+  (require builtins)
+  (builtins.defn also-not-async [] "ok")
+  (assert (= (also-not-async) "ok")))
+
+
 (defn test-return []
 
   ; `return` in main line