Skip to content

Commit 231f5f3

Browse files
committed
mlir: bind loop-else break/continue transitively
1 parent a486da7 commit 231f5f3

2 files changed

Lines changed: 121 additions & 29 deletions

File tree

‎integration/tests/loop_else_break_binding.py‎

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
producing a cross-region block reference the verifier rejects.
1414
1515
Both are now handled by deferring: a loop refuses to lower while a nested loop
16-
still holds a break/continue in its orelse, so the nested loop is flattened into
17-
the enclosing region first and the branch is same-region by construction. That
18-
handshake is also why both loop patterns share one pass.
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.
1923
"""
2024

2125
# break in a nested for's else breaks the OUTER for.
@@ -107,3 +111,76 @@
107111
log.append(("else", outer))
108112
log.append(("after", outer))
109113
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

‎src/executable/mlir/Conversion/PythonToPythonBytecode/PythonToPythonBytecode.cpp‎

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -56,39 +56,54 @@ namespace py {
5656
return mlir::isa<TryOp, ForLoopOp, WithOp, WhileOp, TryHandlerOp>(op);
5757
}
5858

59-
// True when `yield_op` is a loop-control (break/continue) yield that binds to
60-
// the loop *enclosing* `loop` rather than to `loop` itself — i.e. it sits in
61-
// `loop`'s orelse, which is not part of the loop body.
62-
bool binds_to_enclosing_loop(mlir::py::PyLoopOpInterface loop,
63-
mlir::py::BranchYieldOp yield_op)
59+
// True when `yield_op`, a loop-control (break/continue) yield, binds to the
60+
// loop whose body region is `body`.
61+
//
62+
// Python binds break/continue to the innermost loop whose *body* lexically
63+
// contains it. An else clause is not part of its own loop's body, so a yield
64+
// sitting there keeps searching outwards — and transitively so: an else nested
65+
// inside another else is still lexically part of whatever body encloses the
66+
// pair. Regions that are neither body nor orelse (a try body, a with body) are
67+
// likewise transparent, which is what makes `break` inside a `try` bind to the
68+
// loop around it.
69+
bool binds_to_loop(mlir::Region &body, mlir::py::BranchYieldOp yield_op)
6470
{
65-
return yield_op.getKind().has_value() && loop.isLoopOrelse(yield_op->getParentRegion());
71+
for (mlir::Region *region = yield_op->getParentRegion(); region != nullptr;
72+
region = region->getParentRegion()) {
73+
if (region == &body) { return true; }
74+
auto loop =
75+
mlir::dyn_cast_if_present<mlir::py::PyLoopOpInterface>(region->getParentOp());
76+
// A loop body (or a for's step) stops the search: the yield is that
77+
// loop's, and its own pattern claims it.
78+
if (loop && !loop.isLoopOrelse(region)) { return false; }
79+
}
80+
return false;
6681
}
6782

68-
// True when some loop nested in `region` still holds a break/continue that
69-
// binds to the loop being lowered — i.e. one sitting in that nested loop's
70-
// orelse. Such a yield cannot be rewritten yet: it lives in a region that has
71-
// not been flattened, so branching it to our target block would be a
72-
// cross-region block reference, which is invalid IR.
83+
// True when a break/continue that binds to the loop whose body is `body` is
84+
// somewhere replace_loop_branch_yields cannot reach yet — inside a nested
85+
// region that has not been flattened into ours. Branching it to our target
86+
// block now would be a cross-region block reference, which is invalid IR.
7387
//
74-
// The caller defers (fails the match) until the nested loop lowers and inlines
88+
// The caller defers (fails the match) until the nested op lowers and inlines
7589
// the yield into our region, the same innermost-first trick TryOpLowering uses
76-
// for nested trys. Terminates because the innermost such loop has nothing
77-
// nested to wait on.
78-
bool has_pending_nested_orelse_control(mlir::Region &region)
90+
// for nested trys. Terminates because the innermost such op has nothing nested
91+
// to wait on.
92+
bool has_pending_nested_orelse_control(mlir::Region &body)
7993
{
80-
if (region.empty()) { return false; }
94+
if (body.empty()) { return false; }
8195
bool pending = false;
82-
region.walk<WalkOrder::PreOrder>([&pending](mlir::Operation *op) {
83-
auto loop = mlir::dyn_cast<mlir::py::PyLoopOpInterface>(op);
84-
if (!loop) { return WalkResult::advance(); }
85-
loop.getLoopOrelseRegion().walk<WalkOrder::PreOrder>(
86-
[&pending, loop](mlir::py::BranchYieldOp yield_op) {
87-
if (binds_to_enclosing_loop(loop, yield_op)) { pending = true; }
88-
});
89-
// Only this loop's own orelse matters here; anything deeper is the
90-
// nested loop's problem and it defers on it in turn.
91-
return WalkResult::skip();
96+
body.walk<WalkOrder::PreOrder>([&pending, &body](mlir::Operation *op) {
97+
// Mirror replace_loop_branch_yields: what it walks through it rewrites
98+
// in place, so only what it skips over can be pending.
99+
if (!is_flattened_region_op(op)) { return WalkResult::advance(); }
100+
op->walk([&pending, &body](mlir::py::BranchYieldOp yield_op) {
101+
if (!yield_op.getKind().has_value()) { return WalkResult::advance(); }
102+
if (!binds_to_loop(body, yield_op)) { return WalkResult::advance(); }
103+
pending = true;
104+
return WalkResult::interrupt();
105+
});
106+
return pending ? WalkResult::interrupt() : WalkResult::skip();
92107
});
93108
return pending;
94109
}

0 commit comments

Comments
 (0)