diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index ac8837359c1445..1f437f0afeb326 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1633,6 +1633,56 @@ def test_remove_redundant_nop_edge_case(self): def f(): a if (1 if b else c) else d + def test_while_continue_try_except_exception_table(self): + # The try region's exception table must include the backward jump emitted + # for 'continue', so pending exceptions (e.g. KeyboardInterrupt) and + # tracing behave like other try-body instructions. + src = textwrap.dedent('''\ + def f(): + while True: + try: + continue + except KeyboardInterrupt: + break + ''') + f_code = compile(src, '', 'exec').co_consts[0] + jump_offsets = [ + i.offset for i in dis.get_instructions(f_code) + if i.opname == 'JUMP_BACKWARD' + ] + self.assertEqual(len(jump_offsets), 1) + joff = jump_offsets[0] + entries = dis._parse_exception_table(f_code) + self.assertTrue( + any(e.start <= joff < e.end for e in entries), + f'offset {joff} not covered by {entries!r}', + ) + + def test_try_literal_stmt_exception_table(self): + # Like try + continue, a try body that is only a literal statement must + # not leave the "invisible" result-discard outside the exception table. + src = textwrap.dedent('''\ + def f(): + try: + 42 + except: + pass + ''') + f_code = compile(src, '', 'exec').co_consts[0] + body_offsets = [ + i.offset for i in dis.get_instructions(f_code) + if i.positions is not None + and i.positions.lineno == 3 + and i.opname not in ('RESUME', 'NOP') + ] + self.assertNotEqual(body_offsets, []) + entries = dis._parse_exception_table(f_code) + for off in body_offsets: + self.assertTrue( + any(e.start <= off < e.end for e in entries), + f'offset {off} not covered by {entries!r}', + ) + def test_lineno_propagation_empty_blocks(self): # Smoke test. See gh-138714. def f(): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-10-12-00-00.gh-issue-148278.4Kp9mN.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-10-12-00-00.gh-issue-148278.4Kp9mN.rst new file mode 100644 index 00000000000000..5ab04021684637 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-10-12-00-00.gh-issue-148278.4Kp9mN.rst @@ -0,0 +1,4 @@ +Fix exception table coverage for ``try`` blocks: pseudo ``SETUP_FINALLY`` and +``POP_BLOCK`` instructions are now labeled with the active handler, so +``continue`` inside ``try``/``except`` and other minimal try bodies handle +pending exceptions and tracing like the rest of the protected region. diff --git a/Python/flowgraph.c b/Python/flowgraph.c index e988f4451007fb..fe884943484eec 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -932,8 +932,17 @@ label_exception_targets(basicblock *entryblock) { todo++; } handler = push_except_block(except_stack, instr); + /* Exception coverage for this instruction must match the try + * region it opens, so tracing and pending exceptions while + * executing it are handled like the following protected + * instructions. */ + instr->i_except = handler; } else if (instr->i_opcode == POP_BLOCK) { + /* POP_BLOCK ends a protected region but must still be covered by + * that region's handler until the pop completes (e.g. continue + * inside try). */ + instr->i_except = except_stack_top(except_stack); handler = pop_except_block(except_stack); INSTR_SET_OP0(instr, NOP); }