From 0505d637b76b31e00c5a089924982f214be2613a Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:54:35 +0100 Subject: [PATCH 1/8] gh-130279: Fix missing exception handlers for jumps in loops --- Lib/test/test_compile.py | 22 +++++++++++++++++++ ...-10-15-12-36-09.gh-issue-130279.N9N-sw.rst | 4 ++++ Python/flowgraph.c | 10 +++++++++ 3 files changed, 36 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst 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; } From 4bdb7ef78ad3a1b2efde90bf32dcbe10e37ce45f Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:23:24 +0100 Subject: [PATCH 2/8] add except arg --- Python/flowgraph.c | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index bc37c1c35f228e5..2a90def05d2f94e 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -188,7 +188,8 @@ cfg_builder_new_block(cfg_builder *g) } static int -basicblock_addop(basicblock *b, int opcode, int oparg, location loc) +basicblock_addop(basicblock *b, int opcode, int oparg, location loc, + basicblock *except) { assert(IS_WITHIN_OPCODE_RANGE(opcode)); assert(!IS_ASSEMBLER_OPCODE(opcode)); @@ -203,15 +204,16 @@ basicblock_addop(basicblock *b, int opcode, int oparg, location loc) i->i_opcode = opcode; i->i_oparg = oparg; i->i_loc = loc; + i->i_except = except; // memory is already zero initialized assert(i->i_target == NULL); - assert(i->i_except == NULL); return SUCCESS; } static int -basicblock_add_jump(basicblock *b, int opcode, basicblock *target, location loc) +basicblock_add_jump(basicblock *b, int opcode, basicblock *target, location loc, + basicblock *except) { cfg_instr *last = basicblock_last_instr(b); if (last && is_jump(last)) { @@ -219,7 +221,7 @@ basicblock_add_jump(basicblock *b, int opcode, basicblock *target, location loc) } RETURN_IF_ERROR( - basicblock_addop(b, opcode, target->b_label.id, loc)); + basicblock_addop(b, opcode, target->b_label.id, loc, except)); last = basicblock_last_instr(b); assert(last && last->i_opcode == opcode); last->i_target = target; @@ -493,7 +495,7 @@ int _PyCfgBuilder_Addop(cfg_builder *g, int opcode, int oparg, location loc) { RETURN_IF_ERROR(cfg_builder_maybe_start_new_block(g)); - return basicblock_addop(g->g_curblock, opcode, oparg, loc); + return basicblock_addop(g->g_curblock, opcode, oparg, loc, NULL); } @@ -554,7 +556,7 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { bool is_forward = last->i_target->b_visited == 0; if (is_forward) { RETURN_IF_ERROR( - basicblock_addop(b, NOT_TAKEN, 0, last->i_loc)); + basicblock_addop(b, NOT_TAKEN, 0, last->i_loc, NULL)); return SUCCESS; } @@ -582,17 +584,16 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { if (backwards_jump == NULL) { return ERROR; } - RETURN_IF_ERROR( - 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; - } + RETURN_IF_ERROR( + basicblock_addop(backwards_jump, NOT_TAKEN, 0, last->i_loc, + last->i_except)); + RETURN_IF_ERROR( + basicblock_add_jump(backwards_jump, JUMP, target, last->i_loc, + last->i_except)); backwards_jump->b_startdepth = target->b_startdepth; last->i_opcode = reversed_opcode; last->i_target = b->b_next; @@ -1295,15 +1296,13 @@ 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; + bb, opcode, target->i_target, target->i_loc, + inst->i_except)); return true; } @@ -3557,7 +3556,7 @@ push_cold_blocks_to_end(cfg_builder *g) { b->b_next->b_label.id = next_lbl++; } basicblock_addop(explicit_jump, JUMP_NO_INTERRUPT, b->b_next->b_label.id, - NO_LOCATION); + NO_LOCATION, NULL); explicit_jump->b_cold = 1; explicit_jump->b_next = b->b_next; explicit_jump->b_predecessors = 1; From 1b6f00bf979559f7decef9a0dc105f0b518ab69c Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:01:56 +0100 Subject: [PATCH 3/8] fix for normalize_jumps_in_block --- Lib/test/test_monitoring.py | 31 +++++++++++++++++++++++++++++++ Python/flowgraph.c | 6 +----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_monitoring.py b/Lib/test/test_monitoring.py index 5c2d69934b02ea6..d1b2a4380be806d 100644 --- a/Lib/test/test_monitoring.py +++ b/Lib/test/test_monitoring.py @@ -2529,6 +2529,37 @@ def foo(): foo() sys.monitoring.set_local_events(0, foo.__code__, 0) + def test_130279(self): + # gh-130279 + # An exception raised by the BRANCH_LEFT callback of a forward + # branch must be caught by the enclosing try + class Boom(Exception): + pass + + raised = False + + def callback(code, src, dst): + nonlocal raised + if not raised: + raised = True + raise Boom + + def foo(x): + try: + if x: + pass + except Boom: + return "caught" + return "not raised" + + sys.monitoring.register_callback(TEST_TOOL, E.BRANCH_LEFT, callback) + sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, E.BRANCH_LEFT) + try: + self.assertEqual(foo(True), "caught") + finally: + sys.monitoring.set_local_events(TEST_TOOL, foo.__code__, 0) + sys.monitoring.register_callback(TEST_TOOL, E.BRANCH_LEFT, None) + class TestOptimizer(MonitoringTestBase, unittest.TestCase): diff --git a/Python/flowgraph.c b/Python/flowgraph.c index 2a90def05d2f94e..b0c8f52289c6c37 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -556,7 +556,7 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { bool is_forward = last->i_target->b_visited == 0; if (is_forward) { RETURN_IF_ERROR( - basicblock_addop(b, NOT_TAKEN, 0, last->i_loc, NULL)); + basicblock_addop(b, NOT_TAKEN, 0, last->i_loc, last->i_except)); return SUCCESS; } @@ -584,10 +584,6 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) { if (backwards_jump == NULL) { return ERROR; } - /* 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. - */ RETURN_IF_ERROR( basicblock_addop(backwards_jump, NOT_TAKEN, 0, last->i_loc, last->i_except)); From 424624df1631c5cf10c14c2a5dd856f57c1391c6 Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:29:57 +0100 Subject: [PATCH 4/8] Use dis.hasjump in test --- Lib/test/test_compile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py index 215230be7794e14..ff791d1b1d68bc5 100644 --- a/Lib/test/test_compile.py +++ b/Lib/test/test_compile.py @@ -1584,9 +1584,9 @@ 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. + # gh-130279: The jumps in a loop body, including the backward jump + # for the untaken branch of a conditional at the end of the body, + # must be covered by the exception handler of the enclosing try. def f(items): try: for x in items: @@ -1597,7 +1597,7 @@ def f(items): bytecode = dis.Bytecode(f) jumps = [instr for instr in bytecode - if instr.opname == 'JUMP_BACKWARD'] + if instr.opcode in dis.hasjump] self.assertTrue(jumps) for instr in jumps: with self.subTest(instr=instr): From f9b8e4285e595d3643c52620d88333c060d96224 Mon Sep 17 00:00:00 2001 From: Irit Katriel <1055913+iritkatriel@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:37:09 +0100 Subject: [PATCH 5/8] Apply suggestion from @iritkatriel --- .../2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 index 3621b07f18f444f..996eb7020f53449 100644 --- 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 @@ -1,4 +1,2 @@ -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. +Fix bug where compiler did not set the right exception handler for some jump +instructions. From e401950f1c0b2ea95a8ddeb88d3ba2a4c20ef25f Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:56:26 +0100 Subject: [PATCH 6/8] change to target --- Python/flowgraph.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Python/flowgraph.c b/Python/flowgraph.c index b0c8f52289c6c37..17489ae5bfb5252 100644 --- a/Python/flowgraph.c +++ b/Python/flowgraph.c @@ -1298,7 +1298,7 @@ jump_thread(basicblock *bb, cfg_instr *inst, cfg_instr *target, int opcode) RETURN_IF_ERROR( basicblock_add_jump( bb, opcode, target->i_target, target->i_loc, - inst->i_except)); + target->i_except)); return true; } From 31d2b486e94bc0b136de85c107236326414b29ae Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Sun, 18 Oct 2026 18:31:20 +0100 Subject: [PATCH 7/8] Add myself to Misc/ACKS --- Misc/ACKS | 1 + 1 file changed, 1 insertion(+) diff --git a/Misc/ACKS b/Misc/ACKS index ad1b1119aa243a8..00ed03ff51c98fb 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1144,6 +1144,7 @@ Ivan Levkivskyi Ben Lewis William Lewis Akira Li +Han Li Jiahao Li Robert Li Xuanji Li From 3c03289f2c21f56cd3b8e201c7d3c18a772641dd Mon Sep 17 00:00:00 2001 From: LH-and-FPGA <180024497+LH-and-FPGA@users.noreply.github.com> Date: Sun, 18 Oct 2026 22:54:21 +0100 Subject: [PATCH 8/8] Update test_dis for NOT_TAKEN exception handler --- Lib/test/test_dis.py | 115 +++++++++++++++++++++---------------------- 1 file changed, 56 insertions(+), 59 deletions(-) diff --git a/Lib/test/test_dis.py b/Lib/test/test_dis.py index 3e562a26ad586aa..9879a665bc05000 100644 --- a/Lib/test/test_dis.py +++ b/Lib/test/test_dis.py @@ -454,53 +454,52 @@ def foo(a: int, b: str) -> str: """ dis_traceback = """\ -%4d RESUME 0 +%4d RESUME 0 -%4d NOP +%4d NOP -%4d L1: LOAD_SMALL_INT 1 - LOAD_SMALL_INT 0 - --> BINARY_OP 11 (/) - POP_TOP +%4d L1: LOAD_SMALL_INT 1 + LOAD_SMALL_INT 0 + --> BINARY_OP 11 (/) + POP_TOP -%4d L2: LOAD_FAST_CHECK 1 (tb) - RETURN_VALUE +%4d L2: LOAD_FAST_CHECK 1 (tb) + RETURN_VALUE - -- L3: PUSH_EXC_INFO + -- L3: PUSH_EXC_INFO -%4d LOAD_GLOBAL 0 (Exception) - CHECK_EXC_MATCH - POP_JUMP_IF_FALSE 24 (to L9) - L4: NOT_TAKEN - L5: STORE_FAST 0 (e) +%4d LOAD_GLOBAL 0 (Exception) + CHECK_EXC_MATCH + POP_JUMP_IF_FALSE 24 (to L7) + NOT_TAKEN + STORE_FAST 0 (e) -%4d L6: LOAD_FAST 0 (e) - LOAD_ATTR 2 (__traceback__) - STORE_FAST 1 (tb) - L7: POP_EXCEPT - LOAD_COMMON_CONSTANT 7 (None) - STORE_FAST 0 (e) - DELETE_FAST 0 (e) +%4d L4: LOAD_FAST 0 (e) + LOAD_ATTR 2 (__traceback__) + STORE_FAST 1 (tb) + L5: POP_EXCEPT + LOAD_COMMON_CONSTANT 7 (None) + STORE_FAST 0 (e) + DELETE_FAST 0 (e) -%4d LOAD_FAST 1 (tb) - RETURN_VALUE +%4d LOAD_FAST 1 (tb) + RETURN_VALUE - -- L8: LOAD_COMMON_CONSTANT 7 (None) - STORE_FAST 0 (e) - DELETE_FAST 0 (e) - RERAISE 1 + -- L6: LOAD_COMMON_CONSTANT 7 (None) + STORE_FAST 0 (e) + DELETE_FAST 0 (e) + RERAISE 1 -%4d L9: RERAISE 0 +%4d L7: RERAISE 0 - -- L10: COPY 3 - POP_EXCEPT - RERAISE 1 + -- L8: COPY 3 + POP_EXCEPT + RERAISE 1 ExceptionTable: L1 to L2 -> L3 [0] - L3 to L4 -> L10 [1] lasti - L5 to L6 -> L10 [1] lasti - L6 to L7 -> L8 [1] lasti - L8 to L10 -> L10 [1] lasti + L3 to L4 -> L8 [1] lasti + L4 to L5 -> L6 [1] lasti + L6 to L8 -> L8 [1] lasti """ % (TRACEBACK_CODE.co_firstlineno, TRACEBACK_CODE.co_firstlineno + 1, TRACEBACK_CODE.co_firstlineno + 2, @@ -569,11 +568,11 @@ def _with(c): %4d L3: PUSH_EXC_INFO WITH_EXCEPT_START TO_BOOL - POP_JUMP_IF_TRUE 2 (to L6) - L4: NOT_TAKEN - L5: RERAISE 2 - L6: POP_TOP - L7: POP_EXCEPT + POP_JUMP_IF_TRUE 2 (to L4) + NOT_TAKEN + RERAISE 2 + L4: POP_TOP + L5: POP_EXCEPT POP_TOP POP_TOP POP_TOP @@ -583,13 +582,12 @@ def _with(c): LOAD_COMMON_CONSTANT 7 (None) RETURN_VALUE - -- L8: COPY 3 + -- L6: COPY 3 POP_EXCEPT RERAISE 1 ExceptionTable: L1 to L2 -> L3 [2] lasti - L3 to L4 -> L8 [4] lasti - L5 to L7 -> L8 [4] lasti + L3 to L5 -> L6 [4] lasti """ % (_with.__code__.co_firstlineno, _with.__code__.co_firstlineno + 1, _with.__code__.co_firstlineno + 2, @@ -664,11 +662,11 @@ async def _asyncwith(c): L20: CLEANUP_THROW L21: END_SEND TO_BOOL - POP_JUMP_IF_TRUE 2 (to L24) - L22: NOT_TAKEN - L23: RERAISE 2 - L24: POP_TOP - L25: POP_EXCEPT + POP_JUMP_IF_TRUE 2 (to L22) + NOT_TAKEN + RERAISE 2 + L22: POP_TOP + L23: POP_EXCEPT POP_TOP POP_TOP POP_TOP @@ -678,25 +676,24 @@ async def _asyncwith(c): LOAD_COMMON_CONSTANT 7 (None) RETURN_VALUE - -- L26: COPY 3 + -- L24: COPY 3 POP_EXCEPT RERAISE 1 - L27: CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR) + L25: CALL_INTRINSIC_1 3 (INTRINSIC_STOPITERATION_ERROR) RERAISE 1 ExceptionTable: - L1 to L3 -> L27 [0] lasti + L1 to L3 -> L25 [0] lasti L3 to L4 -> L12 [5] - L4 to L6 -> L27 [0] lasti + L4 to L6 -> L25 [0] lasti L6 to L7 -> L16 [2] lasti - L7 to L9 -> L27 [0] lasti + L7 to L9 -> L25 [0] lasti L9 to L10 -> L14 [3] - L10 to L13 -> L27 [0] lasti - L14 to L15 -> L27 [0] lasti - L16 to L18 -> L26 [4] lasti + L10 to L13 -> L25 [0] lasti + L14 to L15 -> L25 [0] lasti + L16 to L18 -> L24 [4] lasti L18 to L19 -> L20 [8] - L19 to L22 -> L26 [4] lasti - L23 to L25 -> L26 [4] lasti - L25 to L27 -> L27 [0] lasti + L19 to L23 -> L24 [4] lasti + L23 to L25 -> L25 [0] lasti """ % (_asyncwith.__code__.co_firstlineno, _asyncwith.__code__.co_firstlineno + 1, _asyncwith.__code__.co_firstlineno + 2,