Repository: hylang/hy

Issue:
Hy produces an illegal implicit `return` in async generators
This bug looks almost the same as #691 

```lisp
(defn/a naysayer []
  (yield "nope"))
```

raises `SyntaxError: 'return' with value in async generator`. The output of `hy2py` is

```python
async def naysayer():
    return (yield 'nope')
```

The current workaround is to use `assert` 

```lisp
(defn/a naysayer []
  (yield "nope")
  (assert True))
```


Discussion:
This is nasty. Either the compiler needs to look at each function definition to check whether it's an async generator, which could be messy, or the user needs to be warned to make sure each async generator doesn't end with `return`. Frustratingly, Python even forbids `return None` at the end of the function in this case, even though it should be a no-op.
Apparently you can make an async generator which returns.
The code in CPython responsible for this [compile.c@3f84a19e62#L3200](https://github.com/python/cpython/blob/3f84a19e6291db682fc9a570e7612e80e2ffbbb5/Python/compile.c#L3200) 

```python
>>> async def naysayer():
...     yield 'nope'
...     return
...
>>> async def naysayer():
...     yield 'nope'
...     return None
...
  File "<stdin>", line 3
SyntaxError: 'return' with value in async generator
````

Therefore for the more clean looks of temporary fix we may use an explicit `return` instead of the `assert`:
```lisp
(defn/a naysayer []
  (yield "nope")
  (return))
```
Oh dang, that's even weirder. But good to know.
I think adding a little bit of tracking to `ScopeFn` should be able to let us solve this, I'm testing it out right now

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
@@ -58,7 +58,7 @@
     is_unpack,
 )
 from hy.reader import mangle
-from hy.scoping import OuterVar, ScopeFn, ScopeGen, ScopeLet, is_inside_function_scope
+from hy.scoping import OuterVar, ScopeFn, ScopeGen, ScopeLet, is_function_scope, is_inside_function_scope, nearest_python_scope
 
 # ------------------------------------------------
 # * Helpers
@@ -1457,6 +1457,7 @@ def compile_try_expression(compiler, expr, root, body, catchers, orelse, finalbo
 @pattern_macro(["fn", "fn/a"],
     [maybe(type_params), maybe_annotated(lambda_list), many(FORM)])
 def compile_function_lambda(compiler, expr, root, tp, params, body):
+    is_async = root == "fn/a"
     params, returns = params
     posonly, args, rest, kwonly, kwargs = params
     has_annotations = returns is not None or any(
@@ -1464,17 +1465,19 @@ def compile_function_lambda(compiler, expr, root, tp, params, body):
         for param in (posonly or []) + args + kwonly + [rest, kwargs]
     )
     args, ret = compile_lambda_list(compiler, params)
-    with compiler.local_state(), compiler.scope.create(ScopeFn, args):
+    with compiler.local_state(), compiler.scope.create(ScopeFn, args, is_async) as scope:
         body = compiler._compile_branch(body)
 
     # Compile to lambda if we can
-    if not (has_annotations or tp or body.stmts or root == "fn/a"):
+    if not (has_annotations or tp or body.stmts or is_async):
         return ret + asty.Lambda(expr, args=args, body=body.force_expr)
 
     # Otherwise create a standard function
-    node = asty.AsyncFunctionDef if root == "fn/a" else asty.FunctionDef
+    node = asty.AsyncFunctionDef if is_async else asty.FunctionDef
     name = compiler.get_anon_var()
-    ret += compile_function_node(compiler, expr, node, [], tp, name, args, returns, body)
+    ret += compile_function_node(
+        compiler, expr, node, [], tp, name, args, returns, body, scope
+    )
 
     # return its name as the final expr
     return ret + Result(expr=ret.temp_variables[0])
@@ -1485,26 +1488,30 @@ def compile_function_lambda(compiler, expr, root, tp, params, body):
     [maybe(brackets(many(FORM))), maybe(type_params), maybe_annotated(SYM), lambda_list, many(FORM)],
 )
 def compile_function_def(compiler, expr, root, decorators, tp, name, params, body):
+    is_async = root == "defn/a"
     name, returns = name
-    node = asty.FunctionDef if root == "defn" else asty.AsyncFunctionDef
+    node = asty.AsyncFunctionDef if is_async else asty.FunctionDef
     decorators, ret, _ = compiler._compile_collect(decorators[0] if decorators else [])
     args, ret2 = compile_lambda_list(compiler, params)
     ret += ret2
     name = mangle(compiler._nonconst(name))
     compiler.scope.define(name)
-    with compiler.local_state(), compiler.scope.create(ScopeFn, args):
+    with compiler.local_state(), compiler.scope.create(ScopeFn, args, is_async) as scope:
         body = compiler._compile_branch(body)
 
     return ret + compile_function_node(
-        compiler, expr, node, decorators, tp, name, args, returns, body
+        compiler, expr, node, decorators, tp, name, args, returns, body, scope
     )
 
 
-def compile_function_node(compiler, expr, node, decorators, tp, name, args, returns, body):
+def compile_function_node(compiler, expr, node, decorators, tp, name, args, returns, body, scope):
     ret = Result()
 
     if body.expr:
-        body += asty.Return(body.expr, value=body.expr)
+        # implicitly return final expression,
+        # except for async generators
+        enode = asty.Expr if scope.is_async and scope.has_yield else asty.Return
+        body += enode(body.expr, value=body.expr)
 
     ret += node(
         expr,
@@ -1665,6 +1672,8 @@ def compile_return(compiler, expr, root, arg):
 
 @pattern_macro("yield", [maybe(FORM)])
 def compile_yield_expression(compiler, expr, root, arg):
+    if is_inside_function_scope(compiler.scope):
+        nearest_python_scope(compiler.scope).has_yield = True
     ret = Result()
     if arg is not None:
         ret += compiler.compile(arg)
@@ -1673,6 +1682,8 @@ def compile_yield_expression(compiler, expr, root, arg):
 
 @pattern_macro(["yield-from", "await"], [FORM])
 def compile_yield_from_or_await_expression(compiler, expr, root, arg):
+    if root == "yield-from" and is_inside_function_scope(compiler.scope):
+        nearest_python_scope(compiler.scope).has_yield = True
     ret = Result() + compiler.compile(arg)
     node = asty.YieldFrom if root == "yield-from" else asty.Await
     return ret + node(expr, value=ret.force_expr)
diff --git a/hy/scoping.py b/hy/scoping.py
--- a/hy/scoping.py
+++ b/hy/scoping.py
@@ -292,7 +292,7 @@ def add(self, target, new_name=None):
 class ScopeFn(ScopeBase):
     """Scope that corresponds to Python's own function or class scopes."""
 
-    def __init__(self, compiler, args=None):
+    def __init__(self, compiler, args=None, is_async=False):
         super().__init__(compiler)
         self.defined = set()
         "set: of all vars defined in this scope"
@@ -305,6 +305,12 @@ def __init__(self, compiler, args=None):
         bool: `True` if this scope is being used to track a python
         function `False` for classes
         """
+        self.is_async = is_async
+        """bool: `True` if this scope is for an async function,
+        which may need special handling during compilation"""
+        self.has_yield = False
+        """bool: `True` if this scope is tracking a function that has `yield`
+        statements, as generator functions may need special handling"""
 
         if args:
             for arg in itertools.chain(


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,19 @@
   (assert (= (asyncio.run (coro-test)) [1 2 3])))
 
 
+(defn [async-test] test-no-async-gen-return []
+  ; https://github.com/hylang/hy/issues/2523
+  (defn/a runner [gen]
+    (setv vals [])
+    (for [:async val (gen)]
+      (.append vals val))
+    vals)
+  (defn/a naysayer []
+    (yield "nope"))
+  (assert (= (asyncio.run (runner naysayer)) ["nope"]))
+  (assert (= (asyncio.run (runner (fn/a [] (yield "dope!")) ["dope!"])))))
+
+
 (defn test-root-set-correctly []
   ; https://github.com/hylang/hy/issues/2475
   ((. defn) not-async [] "ok")