|
18 | 18 | Label, |
19 | 19 | SetLineno, |
20 | 20 | TryBegin, |
| 21 | + TryEnd, |
21 | 22 | dump_bytecode, |
22 | 23 | ) |
23 | 24 | from bytecode.utils import PY312, PY313, PY314 |
@@ -903,6 +904,78 @@ def test(*args, **kwargs): |
903 | 904 | self.assertEqual(test([], name=None), -1) |
904 | 905 | self.assertEqual(stdout.getvalue(), "second finally\nfirst finally\n") |
905 | 906 |
|
| 907 | + def test_stack_size_computation_shared_handler_split_regions(self): |
| 908 | + # Regression test for an assertion failure in the stack-size |
| 909 | + # computation. A bytecode-rewriting transform (e.g. an instrumentation |
| 910 | + # tool) may wrap a whole function body in a single exception handler |
| 911 | + # whose coverage is split around the body's own try blocks, so that the |
| 912 | + # regions never nest but all share the *same* handler block. A single |
| 913 | + # logical region then maps to several TryBegin instances. |
| 914 | + # |
| 915 | + # When such a region is exited through a forward jump (here the branch |
| 916 | + # inside the first handler body), the jump target block is reached both |
| 917 | + # by fall-through and by the jump, and ends up carrying one TryBegin |
| 918 | + # copy as ``pending_try_begin`` while its own leading TryEnd references |
| 919 | + # a *different* copy of the same region. Matching TryEnd against the |
| 920 | + # current TryBegin by identity then failed to close the region, leaving |
| 921 | + # it spuriously open and tripping ``assert self._current_try_begin is |
| 922 | + # None`` at the following TryBegin. Matching must be done on the handler |
| 923 | + # (the TryBegin target block) instead. |
| 924 | + if sys.version_info < (3, 11): |
| 925 | + self.skipTest("exception tables (TryBegin/TryEnd) require 3.11+") |
| 926 | + |
| 927 | + def trigger(d, x): # pragma: no cover |
| 928 | + try: |
| 929 | + d["a"] |
| 930 | + except KeyError: |
| 931 | + if x: # a branch *inside* the handler body |
| 932 | + pass |
| 933 | + else: |
| 934 | + d["b"] = 1 |
| 935 | + try: # ... followed by another try block |
| 936 | + del d["c"] |
| 937 | + except KeyError: |
| 938 | + pass |
| 939 | + return 99 |
| 940 | + |
| 941 | + bc = Bytecode.from_code(trigger.__code__) |
| 942 | + |
| 943 | + # Wrap the whole body in one shared handler, splitting the covering |
| 944 | + # region around the body's existing try blocks. |
| 945 | + except_label = Label() |
| 946 | + first_tb = last_tb = TryBegin(except_label, push_lasti=True) |
| 947 | + i = 0 |
| 948 | + while i < len(bc): |
| 949 | + instr = bc[i] |
| 950 | + if isinstance(instr, TryBegin) and last_tb is not None: |
| 951 | + bc.insert(i, TryEnd(last_tb)) |
| 952 | + last_tb = None |
| 953 | + i += 1 |
| 954 | + elif isinstance(instr, TryEnd): |
| 955 | + j = i + 1 |
| 956 | + while j < len(bc) and not isinstance(bc[j], TryBegin): |
| 957 | + if isinstance(bc[j], Instr): |
| 958 | + last_tb = TryBegin(except_label, push_lasti=True) |
| 959 | + bc.insert(i + 1, last_tb) |
| 960 | + break |
| 961 | + j += 1 |
| 962 | + i += 1 |
| 963 | + i += 1 |
| 964 | + bc.insert(0, first_tb) |
| 965 | + bc.append(TryEnd(last_tb)) |
| 966 | + bc.append(except_label) |
| 967 | + bc.append(Instr("PUSH_EXC_INFO")) |
| 968 | + bc.append(Instr("RERAISE", 2)) |
| 969 | + |
| 970 | + cfg = ControlFlowGraph.from_bytecode(bc) |
| 971 | + # Used to raise AssertionError on 3.12. |
| 972 | + cfg.compute_stacksize() |
| 973 | + |
| 974 | + # The rewritten code object is valid and still runs correctly. |
| 975 | + trigger.__code__ = cfg.to_code() |
| 976 | + self.assertEqual(trigger({}, True), 99) |
| 977 | + self.assertEqual(trigger({"c": 1}, False), 99) |
| 978 | + |
906 | 979 | def test_stack_size_with_dead_code(self): |
907 | 980 | # Simply demonstrate more directly the previously mentioned issue. |
908 | 981 | def test(*args): # pragma: no cover |
|
0 commit comments