diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 60886b1a55c16f4..215230be7794e14 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1583,6 +1583,28 @@ def f(): self.check_line_numbers(f.__code__, 'JUMP_BACKWARD') + def test_backward_jump_conditional_in_loop_in_try(self): + # gh-130279: The backward jump for the untaken branch of a + # conditional at the end of a loop body must be covered by the + # exception handler of the enclosing try. + def f(items): + try: + for x in items: + if x < 0: + raise ValueError(x) + except KeyboardInterrupt: + return 'handled' + + bytecode = dis.Bytecode(f) + jumps = [instr for instr in bytecode + if instr.opname == 'JUMP_BACKWARD'] + self.assertTrue(jumps) + for instr in jumps: + with self.subTest(instr=instr): + self.assertTrue( + any(entry.start <= instr.offset < entry.end + for entry in bytecode.exception_entries)) + def test_big_dict_literal(self): # The compiler has a flushing point in "compiler_dict" that calls compiles # a portion of the dictionary literal when the loop that iterates over the items diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst new file mode 100644 index 000000000000000..3621b07f18f444f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst @@ -0,0 +1,4 @@ +Fix exceptions raised at the backward jump of a loop (such as +:exc:`KeyboardInterrupt`) skipping the enclosing :keyword:`try` or +:keyword:`with` statement when the loop body ends with an :keyword:`if` +statement whose condition is false. diff --git a/Python/flowgraph.c b/Python/flowgraph.c index a5138d1a1fa2846..bc37c1c35f228e5 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -586,6 +586,13 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { basicblock_addop(backwards_jump, NOT_TAKEN, 0, last->i_loc)); RETURN_IF_ERROR( basicblock_add_jump(backwards_jump, JUMP, target, last->i_loc)); + /* The new instructions are covered by the same exception handler as + * the conditional jump, since the backward jump can raise when it + * checks the eval breaker. + */ + for (int i = 0; i < backwards_jump->b_iused; i++) { + backwards_jump->b_instr[i].i_except = last->i_except; + } backwards_jump->b_startdepth = target->b_startdepth; last->i_opcode = reversed_opcode; last->i_target = b->b_next; @@ -1288,12 +1295,15 @@ jump_thread(basicblock *bb, cfg_instr *inst, cfg_instr *target, int opcode) if (inst->i_target != target->i_target) { /* Change inst to NOP and append a jump to target->i_target. The * NOP will be removed later if it's not needed for the lineno. + * The new jump keeps the exception handler of inst. */ + basicblock *except = inst->i_except; INSTR_SET_OP0(inst, NOP); RETURN_IF_ERROR( basicblock_add_jump( bb, opcode, target->i_target, target->i_loc)); + basicblock_last_instr(bb)->i_except = except; return true; }