From 6060e6f7cc50d940dd3717caa88f531e6a531968 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 14 Sep 2026 18:12:23 +0100 Subject: [PATCH 1/2] [mypyc] Fix compiler crash for empty async with body Fix a regression introduced by #21982. CFG cleanup correctly removes an unreachable exception-handler block, but generated resume cleanup paths still reference its raw CPy_CatchError CallC result. This leaves malformed IR and causes a KeyError during reference count insertion. Keep the saved exception state in a register so it remains available across the generated control flow. Add regression coverage for compiling and running an async with statement with an empty body. --- mypyc/irbuild/builder.py | 5 ++++- mypyc/irbuild/statement.py | 3 +++ mypyc/test-data/run-async.test | 10 +++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index f9964f936d98..f17227d55e5e 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -1039,7 +1039,10 @@ def pop_loop_stack(self) -> None: self.nonlocal_control.pop() def ensure_register(self, value: Value) -> Register: - """Return an assignable register containing a value.""" + """Return an assignable register containing a value. + + Values that may remain live across arbitrary control flow should use a register. + """ if isinstance(value, Register): return value diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index a3e571b63377..a53b39693364 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -712,6 +712,9 @@ def transform_try_except( builder.builder.push_error_handler(double_except_block) builder.activate_block(except_entry) old_exc = builder.call_c(error_catch_op, [], line) + if builder.fn_info.is_generator: + # Let the generator spill pass preserve the saved exception state across a suspension. + old_exc = builder.ensure_register(old_exc) # Compile the except blocks with the nonlocal control flow overridden to clear exc_info builder.nonlocal_control.append(ExceptNonlocalControl(builder.nonlocal_control[-1], old_exc)) diff --git a/mypyc/test-data/run-async.test b/mypyc/test-data/run-async.test index 9a729f8fff46..5eebb7348cf3 100644 --- a/mypyc/test-data/run-async.test +++ b/mypyc/test-data/run-async.test @@ -292,8 +292,12 @@ async def async_with_vectorcall() -> str: async with ctx: return await async_val("vc") +async def async_with_pass() -> None: + async with async_ctx(): + pass + [file driver.py] -from native import async_with, async_with_vectorcall +from native import async_with, async_with_pass, async_with_vectorcall from testutil import run_generator yields, val = run_generator(async_with(), [None, 'x', None]) @@ -304,6 +308,10 @@ yields, val = run_generator(async_with_vectorcall(), [None, 'x', None]) assert yields == ('enter', 'vc', 'exit'), yields assert val == 'x', val +yields, val = run_generator(async_with_pass(), [None, None]) +assert yields == ('enter', 'exit'), yields +assert val is None + [case testAsyncReturn] from testutil import async_val From 5ec68deb8b77442c276d332f8a0d0f6c81979467 Mon Sep 17 00:00:00 2001 From: Jukka Lehtosalo Date: Mon, 14 Sep 2026 18:34:50 +0100 Subject: [PATCH 2/2] [mypyc] Clarify generator exception state handling --- mypyc/irbuild/builder.py | 5 +---- mypyc/irbuild/statement.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/mypyc/irbuild/builder.py b/mypyc/irbuild/builder.py index f17227d55e5e..f9964f936d98 100644 --- a/mypyc/irbuild/builder.py +++ b/mypyc/irbuild/builder.py @@ -1039,10 +1039,7 @@ def pop_loop_stack(self) -> None: self.nonlocal_control.pop() def ensure_register(self, value: Value) -> Register: - """Return an assignable register containing a value. - - Values that may remain live across arbitrary control flow should use a register. - """ + """Return an assignable register containing a value.""" if isinstance(value, Register): return value diff --git a/mypyc/irbuild/statement.py b/mypyc/irbuild/statement.py index a53b39693364..d96ec47a3ffe 100644 --- a/mypyc/irbuild/statement.py +++ b/mypyc/irbuild/statement.py @@ -713,7 +713,7 @@ def transform_try_except( builder.activate_block(except_entry) old_exc = builder.call_c(error_catch_op, [], line) if builder.fn_info.is_generator: - # Let the generator spill pass preserve the saved exception state across a suspension. + # CFG cleanup may remove the CallC's block while resume cleanup paths remain. old_exc = builder.ensure_register(old_exc) # Compile the except blocks with the nonlocal control flow overridden to clear exc_info builder.nonlocal_control.append(ExceptNonlocalControl(builder.nonlocal_control[-1], old_exc))