https://github.com/python/cpython/commit/036d309cf542986a39eeba5ca805b2ec7619731d
commit: 036d309cf542986a39eeba5ca805b2ec7619731d
branch: main
author: Han <[email protected]>
committer: iritkatriel <[email protected]>
date: 2026-10-19T09:10:15+01:00
summary:

gh-130279: Fix missing exception handlers for jumps in loops (#159460)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-10-15-12-36-09.gh-issue-130279.N9N-sw.rst
M Lib/test/test_compile.py
M Lib/test/test_dis.py
M Lib/test/test_monitoring.py
M Misc/ACKS
M Python/flowgraph.c

diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index 60886b1a55c16f4..ff791d1b1d68bc5 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 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:
+                    if x < 0:
+                        raise ValueError(x)
+            except KeyboardInterrupt:
+                return 'handled'
+
+        bytecode = dis.Bytecode(f)
+        jumps = [instr for instr in bytecode
+                 if instr.opcode in dis.hasjump]
+        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/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,
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/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
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..996eb7020f53449
--- /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,2 @@
+Fix bug where compiler did not set the right exception handler for some jump
+instructions.
diff --git a/Python/flowgraph.c b/Python/flowgraph.c
index a5138d1a1fa2846..17489ae5bfb5252 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, last->i_except));
         return SUCCESS;
     }
 
@@ -583,9 +585,11 @@ normalize_jumps_in_block(cfg_builder *g, basicblock *b) {
         return ERROR;
     }
     RETURN_IF_ERROR(
-        basicblock_addop(backwards_jump, NOT_TAKEN, 0, last->i_loc));
+        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));
+        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;
@@ -1293,7 +1297,8 @@ 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));
+                bb, opcode, target->i_target, target->i_loc,
+                target->i_except));
 
         return true;
     }
@@ -3547,7 +3552,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;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to