Skip to content

Commit dd10eef

Browse files
committed
regalloc: model exception-handler edges in liveness
A try body lowers to blocks whose only explicit CFG successors are the normal-flow ones (e.g. RAISE_VARARGS -> exit); the edge from a faulting op in the try body to the handler is implicit (established by SETUP_EXC_HANDLE in the *predecessor*). mlir::Liveness therefore never sees body-op -> handler, so a value that is live at the handler — or live after it — was considered dead inside the try body, and the register allocator happily reused its register there. When an exception actually unwound, that register had been overwritten. The clearest case: a `for` loop whose body contains `try/except`. The FOR_ITER iterator is read again after the handler (next iteration), but its register was reused for a value inside the try body, so the resumed loop read a clobbered iterator (abort in ForIter / "object is not an iterator"). The same bug corrupted exception args across sequential try/except blocks. Fix: in LiveAnalysis, for every block in a try body (those dominated by the SETUP_EXC_HANDLE / SETUP_WITH try-entry successor), add the handler's live-in set to the block's live-out before computing per-op liveness. Any op in the try body can transfer to the handler, so values live at the handler are live across the whole try body. Dominance keeps it bounded and handles nesting; the pass is skipped entirely when a function has no handlers. This makes the iterator (and any value live past the handler) interfere with try-body temporaries, so they no longer share a register.
1 parent 8d38a64 commit dd10eef

5 files changed

Lines changed: 337 additions & 36 deletions

File tree

integration/tests/exception_types.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,6 @@
22
# crashing — Exception subclasses missing their own __new__ used to inherit
33
# Exception::__new__ (which asserts the exact Exception type), and
44
# ModuleNotFoundError dereferenced a null kwargs.
5-
#
6-
# NB: the per-type check lives in a helper called from the loop body rather than
7-
# inline, to avoid the (separate) FOR_ITER iterator-register clobber bug that a
8-
# heavy loop body triggers.
95

106
builtin_exceptions = [
117
BaseException, Exception, ValueError, KeyError, IndexError, TypeError,
@@ -15,7 +11,7 @@
1511
]
1612

1713

18-
def check(exc_type):
14+
for exc_type in builtin_exceptions:
1915
try:
2016
raise exc_type("msg")
2117
except BaseException as e:
@@ -24,10 +20,6 @@ def check(exc_type):
2420
assert e.args == ("msg",), (exc_type, e.args)
2521

2622

27-
for exc_type in builtin_exceptions:
28-
check(exc_type)
29-
30-
3123
# subclass relationships still hold
3224
try:
3325
raise RuntimeError("r")
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Regression: exception-handler edges must be modelled in liveness.
2+
#
3+
# An operation inside a try body can transfer to the handler, but that edge is
4+
# not in the explicit CFG. When liveness ignored it, a value live across the try
5+
# body via the handler path (e.g. a FOR_ITER iterator, or a value used after the
6+
# handler) had its register reused inside the try body and was clobbered when an
7+
# exception actually unwound.
8+
9+
10+
# A for-loop whose body raises and catches: the iterator must survive the try
11+
# body. Previously clobbered (abort in FOR_ITER / "object is not an iterator").
12+
seen = []
13+
for x in [1, 2, 3]:
14+
try:
15+
raise ValueError("m")
16+
except ValueError:
17+
pass
18+
seen.append(x)
19+
assert seen == [1, 2, 3], seen
20+
21+
# Same over range() and over a list of types, with the exception bound.
22+
total = 0
23+
for x in range(4):
24+
try:
25+
raise ValueError("m")
26+
except ValueError as e:
27+
assert str(e) == "m"
28+
total += x
29+
assert total == 6, total
30+
31+
for exc in [ValueError, KeyError, RuntimeError, TypeError, NameError]:
32+
try:
33+
raise exc("msg")
34+
except BaseException as e:
35+
assert isinstance(e, exc), exc
36+
assert e.args == ("msg",), (exc, e.args)
37+
38+
# Sequential try/except in one frame must not leak the prior exception's args.
39+
try:
40+
raise ValueError("hello")
41+
except ValueError as e:
42+
assert e.args == ("hello",), e.args
43+
try:
44+
raise ValueError("a", "b")
45+
except ValueError as e:
46+
assert e.args == ("a", "b"), e.args
47+
48+
# A recursive call whose result must survive a following try/except (the
49+
# original minimal miscompile repro).
50+
def fib(n):
51+
return n if n < 2 else fib(n - 1) + fib(n - 2)
52+
53+
54+
assert fib(10) == 55
55+
try:
56+
raise ValueError("e")
57+
except ValueError as e:
58+
assert str(e) == "e"
59+
60+
# Nested try/except inside a loop.
61+
acc = 0
62+
for x in [1, 2, 3]:
63+
try:
64+
try:
65+
raise ValueError(x)
66+
except KeyError:
67+
pass
68+
except ValueError as e:
69+
acc += e.args[0]
70+
assert acc == 6, acc
71+
72+
print("REGALLOC_EXCEPTION_LIVENESS_OK")
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Regression: the exception-handler-edge liveness fix (a value live across a try
2+
# body via the handler path must keep its register) must hold across try/except,
3+
# try/finally, with, nested try, and except-cascade shapes — not just the simple
4+
# FOR_ITER + try/except case. Each shape loops (register pressure) and keeps a
5+
# value live across a multi-block / faulting try body.
6+
7+
8+
# if/else inside the try body => multi-block body; loop var + accumulator survive
9+
def if_else_body(flag):
10+
acc = 0
11+
for x in [1, 2, 3]:
12+
try:
13+
if flag:
14+
raise ValueError("a")
15+
else:
16+
raise KeyError("b")
17+
except ValueError:
18+
pass
19+
except KeyError:
20+
pass
21+
acc += x
22+
return acc
23+
24+
25+
assert if_else_body(True) == 6, if_else_body(True)
26+
assert if_else_body(False) == 6, if_else_body(False)
27+
28+
29+
# a loop inside the try body; an outer value survives the inner loop + raise
30+
def loop_in_try():
31+
out = []
32+
for x in [1, 2]:
33+
try:
34+
for i in range(3):
35+
pass
36+
raise ValueError(x)
37+
except ValueError:
38+
pass
39+
out.append(x)
40+
return out
41+
42+
43+
assert loop_in_try() == [1, 2], loop_in_try()
44+
45+
46+
# try/except/finally: finally runs on both paths; loop var survives
47+
def try_except_finally():
48+
log = []
49+
for x in [1, 2]:
50+
try:
51+
raise ValueError(x)
52+
except ValueError:
53+
log.append(x)
54+
finally:
55+
log.append(-x)
56+
return log
57+
58+
59+
assert try_except_finally() == [1, -1, 2, -2], try_except_finally()
60+
61+
62+
# try/finally with the exception path actually taken (finally on unwind)
63+
def try_finally_raise():
64+
out = []
65+
for x in [1, 2]:
66+
try:
67+
try:
68+
raise ValueError(x)
69+
finally:
70+
out.append(-x)
71+
except ValueError:
72+
out.append(x)
73+
return out
74+
75+
76+
assert try_finally_raise() == [-1, 1, -2, 2], try_finally_raise()
77+
78+
79+
class CM:
80+
def __enter__(self):
81+
return self
82+
83+
def __exit__(self, *a):
84+
return False
85+
86+
87+
# with-statement: the body may raise; loop var survives the cleanup region
88+
def with_body():
89+
res = []
90+
for x in [1, 2]:
91+
try:
92+
with CM():
93+
raise ValueError(x)
94+
except ValueError:
95+
res.append(x)
96+
return res
97+
98+
99+
assert with_body() == [1, 2], with_body()
100+
101+
102+
# nested try: inner clause does NOT match, exception propagates to outer
103+
def nested_propagate():
104+
res = []
105+
for x in [1, 2]:
106+
try:
107+
try:
108+
raise ValueError(x)
109+
except KeyError:
110+
res.append("k")
111+
except ValueError:
112+
res.append(x)
113+
return res
114+
115+
116+
assert nested_propagate() == [1, 2], nested_propagate()
117+
118+
119+
# multiple except clauses (type cascade); value survives the whole try
120+
def except_cascade(which):
121+
for x in [7]:
122+
try:
123+
if which == 0:
124+
raise ValueError(x)
125+
elif which == 1:
126+
raise KeyError(x)
127+
else:
128+
raise TypeError(x)
129+
except ValueError:
130+
return ("v", x)
131+
except KeyError:
132+
return ("k", x)
133+
except TypeError:
134+
return ("t", x)
135+
136+
137+
assert except_cascade(0) == ("v", 7), except_cascade(0)
138+
assert except_cascade(1) == ("k", 7), except_cascade(1)
139+
assert except_cascade(2) == ("t", 7), except_cascade(2)
140+
141+
142+
# a value defined before the try and used after the handler
143+
def value_after_handler():
144+
out = []
145+
for x in [1, 2, 3]:
146+
keep = x * 10
147+
try:
148+
raise ValueError(x)
149+
except ValueError as e:
150+
got = e.args[0]
151+
out.append(keep + got)
152+
return out
153+
154+
155+
assert value_after_handler() == [11, 22, 33], value_after_handler()
156+
157+
158+
print("REGALLOC_EXCEPTION_LIVENESS_SHAPES_OK")

integration/tests/repr_quoting.py

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,31 +19,21 @@
1919

2020

2121
# exception repr quotes its args; str() stays the bare message
22-
def check_single_arg():
23-
try:
24-
raise ValueError("hello")
25-
except ValueError as e:
26-
assert repr(e) == "ValueError('hello')", repr(e)
27-
assert str(e) == "hello", str(e)
28-
assert e.args == ("hello",), e.args
29-
30-
31-
def check_multi_arg():
32-
try:
33-
raise ValueError("a", "b")
34-
except ValueError as e:
35-
assert repr(e) == "ValueError('a', 'b')", repr(e)
36-
37-
38-
def check_no_arg():
39-
try:
40-
raise ValueError
41-
except ValueError as e:
42-
assert repr(e) == "ValueError()", repr(e)
43-
44-
45-
check_single_arg()
46-
check_multi_arg()
47-
check_no_arg()
22+
try:
23+
raise ValueError("hello")
24+
except ValueError as e:
25+
assert repr(e) == "ValueError('hello')", repr(e)
26+
assert str(e) == "hello", str(e)
27+
assert e.args == ("hello",), e.args
28+
29+
try:
30+
raise ValueError("a", "b")
31+
except ValueError as e:
32+
assert repr(e) == "ValueError('a', 'b')", repr(e)
33+
34+
try:
35+
raise ValueError
36+
except ValueError as e:
37+
assert repr(e) == "ValueError()", repr(e)
4838

4939
print("REPR_QUOTING_OK")

0 commit comments

Comments
 (0)