Repository: python/mypy
Pull request: [mypyc] Remangle redefined names produced by async with
Fixes mypyc/mypyc#1001.

Problem statement:
Error when reusing variable name in async context manager
With latest mypy, the following passes a check with `mypy` but fails to compile with `mypyc`:

```python
import asyncio
import aiofiles

async def x() -> None:
    async with aiofiles.open("README.md") as f:
        await f.readline()
    async with aiofiles.open("README.md") as f:
        await f.readline()

asyncio.run(x())
```

The error messages don't make it immediately obvious to me what has failed.

Notably, it compiles successfully if the `async with` is not used:

```python
import asyncio
import aiofiles

from aiofiles.threadpool.text import AsyncTextIOWrapper

async def x() -> None:
    f: AsyncTextIOWrapper = await aiofiles.open("README.md")
    await f.readline()
    await f.close()
    f = await aiofiles.open("README.md")
    await f.readline()
    await f.close()

asyncio.run(x())
```

In this case, it doesn't matter if `f` is given an explicit type annotation or not; either way will compile.

And by the way, thanks for your hard work on mypyc thus far!

Hints from issue discussion:
If you don't want to install `aiofiles`, what seems to be the same error can be reproduced with a stdlib MWE:

```python
import asyncio
from typing import Any

class ConMan:
    async def __aenter__(self):
        print("start")
    async def __aexit__(self, *exc: Any):
        print("end")

async def x() -> None:
    async with ConMan() as f:
        print("Entered", f)
    async with ConMan() as f:
        print("Entered again", f)

asyncio.run(x())
```

Commit message: [mypyc] Remangle redefined names produced by async with

Patch to review:
diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py
index 8c68f91bf633..e8a1c4efa7f5 100644
--- a/mypyc/irbuild/builder.py
+++ b/mypyc/irbuild/builder.py
@@ -1238,14 +1238,15 @@ def add_var_to_env_class(
     ) -> AssignmentTarget:
         # First, define the variable name as an attribute of the environment class, and then
         # construct a target for that attribute.
-        self.fn_info.env_class.attributes[var.name] = rtype
-        attr_target = AssignmentTargetAttr(base.curr_env_reg, var.name)
+        name = remangle_redefinition_name(var.name)
+        self.fn_info.env_class.attributes[name] = rtype
+        attr_target = AssignmentTargetAttr(base.curr_env_reg, name)
 
         if reassign:
             # Read the local definition of the variable, and set the corresponding attribute of
             # the environment class' variable to be that value.
             reg = self.read(self.lookup(var), self.fn_info.fitem.line)
-            self.add(SetAttr(base.curr_env_reg, var.name, reg, self.fn_info.fitem.line))
+            self.add(SetAttr(base.curr_env_reg, name, reg, self.fn_info.fitem.line))
 
         # Override the local definition of the variable to instead point at the variable in
         # the environment class.
diff --git a/mypyc/test-data/run-async.test b/mypyc/test-data/run-async.test
index 85ad172d61df..f35b706579d6 100644
--- a/mypyc/test-data/run-async.test
+++ b/mypyc/test-data/run-async.test
@@ -143,3 +143,30 @@ async def foo() -> AsyncIterable[int]:
 yields, val = run_generator(async_iter(foo()))
 assert yields == (0,1,2), yields
 assert val == 'lol no', val
+
+[case testAsyncWithVarReuse]
+class ConMan:
+    async def __aenter__(self) -> int:
+        return 1
+    async def __aexit__(self, *exc: object):
+        pass
+
+class ConManB:
+    async def __aenter__(self) -> int:
+        return 2
+    async def __aexit__(self, *exc: object):
+        pass
+
+async def x() -> None:
+    value = 2
+    async with ConMan() as f:
+        value += f
+    async with ConManB() as f:
+        value += f
+    assert value == 5
+
+[typing fixtures/typing-full.pyi]
+[file driver.py]
+import asyncio
+import native
+asyncio.run(native.x())


Code review comments:
diff_hunk: @@ -143,3 +143,30 @@ async def foo() -> AsyncIterable[int]:
 yields, val = run_generator(async_iter(foo()))
 assert yields == (0,1,2), yields
 assert val == 'lol no', val
+
+[case testAsyncWithVarReuse]
+class ConMan:
+    async def __aenter__(self) -> int:
+        return 1
+    async def __aexit__(self, *exc: object):
+        pass
+
+class ConManB:
+    async def __aenter__(self) -> int:
+        return 2
+    async def __aexit__(self, *exc: object):
+        pass
+
+async def x() -> None:
+    value = 2
+    async with ConMan() as f:
+        value += f
+    async with ConManB() as f:
+        value += f
+    assert value == 5
path: mypyc/test-data/run-async.test
text: @user1:
```suggestion
    assert value == 5, value
```
So if this ever fails, we have a little more to go on.