Skip to content

Commit aa17453

Browse files
authored
Various improvements to enable importing stdlib module (#41)
2 parents 7c6fa12 + f3cea1e commit aa17453

24 files changed

Lines changed: 1006 additions & 214 deletions
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.
2+
3+
A loop's else clause is not part of its body, so Python binds loop control written
4+
there to whatever loop encloses the whole statement. The lowering used to get this
5+
wrong in two different ways:
6+
7+
* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
8+
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
9+
silently running every outer iteration.
10+
11+
* the enclosing loop's walker did claim the yield when the nested loop was a
12+
`while`, but emitted the branch while that `py.while` was still unlowered,
13+
producing a cross-region block reference the verifier rejects.
14+
15+
Both are now handled by deferring: a loop refuses to lower while a nested loop
16+
still holds a break/continue that binds to it, so the nested loop is flattened
17+
into the enclosing region first and the branch is same-region by construction.
18+
That handshake is also why both loop patterns share one pass.
19+
20+
Binding outwards is transitive: an else nested inside another else is still
21+
lexically part of whichever loop body encloses the pair, so the deferral has to
22+
follow the whole chain rather than stop one level in.
23+
"""
24+
25+
# break in a nested for's else breaks the OUTER for.
26+
log = []
27+
for outer in [1, 2, 3]:
28+
log.append(outer)
29+
for inner in []:
30+
pass
31+
else:
32+
break
33+
assert log == [1], log
34+
35+
# Same with a while as the inner loop.
36+
log = []
37+
for outer in [1, 2, 3]:
38+
log.append(outer)
39+
while False:
40+
pass
41+
else:
42+
break
43+
assert log == [1], log
44+
45+
# continue in a nested loop's else continues the OUTER loop, skipping the rest
46+
# of the outer body.
47+
log = []
48+
for outer in [1, 2, 3]:
49+
log.append(outer)
50+
while False:
51+
pass
52+
else:
53+
continue
54+
log.append("after-must-not-run")
55+
assert log == [1, 2, 3], log
56+
57+
log = []
58+
for outer in [1, 2, 3]:
59+
log.append(outer)
60+
for inner in []:
61+
pass
62+
else:
63+
continue
64+
log.append("after-must-not-run")
65+
assert log == [1, 2, 3], log
66+
67+
# A while as the enclosing loop.
68+
log = []
69+
n = 0
70+
while n < 3:
71+
n += 1
72+
log.append(n)
73+
for inner in []:
74+
pass
75+
else:
76+
continue
77+
log.append("after-must-not-run")
78+
assert log == [1, 2, 3], log
79+
80+
# The inner loop's own body break still binds to the inner loop, and the inner
81+
# else is then skipped.
82+
log = []
83+
for outer in [1, 2]:
84+
for inner in [10, 20]:
85+
log.append((outer, inner))
86+
break
87+
else:
88+
log.append("inner-else-must-not-run")
89+
log.append(("after", outer))
90+
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
91+
92+
# Three levels: the break binds to the loop enclosing the loop whose else it is,
93+
# i.e. the middle one, so the outermost keeps iterating.
94+
log = []
95+
for a in [1, 2]:
96+
for b in [10, 20]:
97+
log.append((a, b))
98+
for c in []:
99+
pass
100+
else:
101+
break
102+
log.append(("outer", a))
103+
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log
104+
105+
# An else that neither breaks nor continues still falls through to the exit.
106+
log = []
107+
for outer in [1, 2]:
108+
for inner in []:
109+
pass
110+
else:
111+
log.append(("else", outer))
112+
log.append(("after", outer))
113+
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log
114+
115+
# An else nested inside another else: the break binds outwards through *both*, to
116+
# the outermost loop. Checking only the first nested loop's else missed this and
117+
# silently dropped the break.
118+
log = []
119+
for a in [1, 2, 3]:
120+
log.append(a)
121+
for b in []:
122+
pass
123+
else:
124+
for c in []:
125+
pass
126+
else:
127+
break
128+
assert log == [1], log
129+
130+
# Same shape with `continue`, which must skip the rest of the outermost body.
131+
log = []
132+
for a in [1, 2, 3]:
133+
log.append(a)
134+
for b in []:
135+
pass
136+
else:
137+
for c in []:
138+
pass
139+
else:
140+
continue
141+
log.append("after-must-not-run")
142+
assert log == [1, 2, 3], log
143+
144+
# Same shape built from `while`s. Here the mis-binding was not silent: the branch
145+
# was emitted while the inner py.while was still a region of its own, which the
146+
# verifier rejects as a reference to a block in another region.
147+
log = []
148+
for a in [1, 2, 3]:
149+
log.append(a)
150+
while False:
151+
pass
152+
else:
153+
while False:
154+
pass
155+
else:
156+
break
157+
assert log == [1], log
158+
159+
# Three elses deep, to check the walk follows the chain rather than a fixed depth.
160+
log = []
161+
for a in [1, 2, 3]:
162+
log.append(a)
163+
for b in []:
164+
pass
165+
else:
166+
for c in []:
167+
pass
168+
else:
169+
for d in []:
170+
pass
171+
else:
172+
break
173+
assert log == [1], log
174+
175+
# The chain stops at the first loop *body*: this break is in the body of a loop
176+
# that happens to sit in an else, so it binds to that loop and no further.
177+
log = []
178+
for a in [1, 2]:
179+
for b in []:
180+
pass
181+
else:
182+
for c in [10, 20]:
183+
log.append((a, c))
184+
break
185+
log.append(("after", a))
186+
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Statements following a terminator in the same suite.
2+
3+
`break`, `continue` and `return` leave the builder's insertion point in a block
4+
that already ends in a terminator, so MLIRGenerator used to append whatever came
5+
next in the suite *after* that terminator:
6+
7+
'python.br_yield' op must be the last operation in the parent block
8+
9+
The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
10+
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
11+
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
12+
block, which is also what unreachable code means.
13+
14+
Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
15+
Nothing here asserts on the unreachable statements themselves — they cannot run;
16+
the point is that the module compiles and the reachable behaviour is right.
17+
"""
18+
19+
20+
def after_break(values):
21+
seen = []
22+
for v in values:
23+
seen.append(v)
24+
if v == 2:
25+
break
26+
seen.append("unreachable")
27+
raise ValueError("unreachable")
28+
return seen
29+
30+
31+
assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])
32+
33+
34+
def after_continue(values):
35+
seen = []
36+
for v in values:
37+
if v == 2:
38+
continue
39+
seen.append("unreachable")
40+
seen.append(v)
41+
return seen
42+
43+
44+
assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])
45+
46+
47+
def after_return(a):
48+
return a + 1
49+
b = a * 2
50+
raise ValueError("unreachable")
51+
52+
53+
assert after_return(1) == 2, after_return(1)
54+
55+
56+
def after_break_in_while(a):
57+
n = 0
58+
while True:
59+
n += 1
60+
if n >= a:
61+
break
62+
n = 999
63+
raise ValueError("unreachable")
64+
return n
65+
66+
67+
assert after_break_in_while(3) == 3, after_break_in_while(3)
68+
69+
70+
def after_break_in_try(values):
71+
seen = []
72+
for v in values:
73+
try:
74+
seen.append(v)
75+
if v == 2:
76+
break
77+
raise ValueError("unreachable")
78+
except ValueError:
79+
seen.append("caught")
80+
return seen
81+
82+
83+
assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])
84+
85+
86+
def after_raise(a):
87+
if a:
88+
raise ValueError("boom")
89+
a = 999
90+
return a
91+
92+
93+
try:
94+
after_raise(True)
95+
raise AssertionError("should have raised")
96+
except ValueError as e:
97+
assert str(e) == "boom", str(e)
98+
assert after_raise(False) is False
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
"""A while condition whose value is defined outside the condition region.
2+
3+
WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
4+
definition site. That is usually inside the condition region, but not always: CSE
5+
merges the constant behind `while True:` with an identical constant in the
6+
enclosing function, after which py.condition tests a value defined in the
7+
function's entry block. Inserting there put the cf.cond_br in the middle of that
8+
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
9+
whose last operation was no longer a terminator.
10+
11+
py.condition is by construction the terminator of the condition region's last
12+
block, and the value it tests necessarily dominates it, so that is where the
13+
branch belongs.
14+
15+
`b = True` before the loop is what creates the constant CSE merges with — without
16+
it the loop's `True` is unique and the bug does not appear. Reduced from
17+
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
18+
"""
19+
20+
21+
def only_exit_is_raise(a):
22+
b = True
23+
if a:
24+
while True:
25+
raise ValueError("boom")
26+
return b
27+
28+
29+
try:
30+
only_exit_is_raise(True)
31+
raise AssertionError("should have raised")
32+
except ValueError as e:
33+
assert str(e) == "boom", str(e)
34+
assert only_exit_is_raise(False) is True
35+
36+
37+
def shared_true_constant(limit):
38+
flag = True
39+
n = 0
40+
while True:
41+
n += 1
42+
if n >= limit:
43+
break
44+
return (n, flag)
45+
46+
47+
assert shared_true_constant(3) == (3, True), shared_true_constant(3)
48+
49+
50+
def shared_false_constant(a):
51+
flag = False
52+
n = 0
53+
while not flag:
54+
n += 1
55+
if n >= a:
56+
flag = True
57+
return n
58+
59+
60+
assert shared_false_constant(2) == 2, shared_false_constant(2)
61+
62+
63+
def condition_is_a_parameter(cond, limit):
64+
# The condition value is a block argument rather than an op result, the other
65+
# branch of the insertion-point choice that used to exist.
66+
n = 0
67+
while cond:
68+
n += 1
69+
if n >= limit:
70+
cond = False
71+
return n
72+
73+
74+
assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
75+
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)
76+
77+
78+
def nested_loops_sharing_true(limit):
79+
t = True
80+
outer = 0
81+
while True:
82+
outer += 1
83+
inner = 0
84+
while True:
85+
inner += 1
86+
if inner >= 2:
87+
break
88+
if outer >= limit:
89+
break
90+
return (outer, inner, t)
91+
92+
93+
assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)

0 commit comments

Comments
 (0)