Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions integration/tests/loop_else_break_binding.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
"""`break`/`continue` inside a loop's `else` binds to the *enclosing* loop.

A loop's else clause is not part of its body, so Python binds loop control written
there to whatever loop encloses the whole statement. The lowering used to get this
wrong in two different ways:

* `ForLoopOpLowering` rewrote its orelse's trailing yield without checking the
yield's kind, so the inner loop swallowed a `break` meant for the outer one —
silently running every outer iteration.

* the enclosing loop's walker did claim the yield when the nested loop was a
`while`, but emitted the branch while that `py.while` was still unlowered,
producing a cross-region block reference the verifier rejects.

Both are now handled by deferring: a loop refuses to lower while a nested loop
still holds a break/continue that binds to it, so the nested loop is flattened
into the enclosing region first and the branch is same-region by construction.
That handshake is also why both loop patterns share one pass.

Binding outwards is transitive: an else nested inside another else is still
lexically part of whichever loop body encloses the pair, so the deferral has to
follow the whole chain rather than stop one level in.
"""

# break in a nested for's else breaks the OUTER for.
log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
break
assert log == [1], log

# Same with a while as the inner loop.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
break
assert log == [1], log

# continue in a nested loop's else continues the OUTER loop, skipping the rest
# of the outer body.
log = []
for outer in [1, 2, 3]:
log.append(outer)
while False:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

log = []
for outer in [1, 2, 3]:
log.append(outer)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# A while as the enclosing loop.
log = []
n = 0
while n < 3:
n += 1
log.append(n)
for inner in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# The inner loop's own body break still binds to the inner loop, and the inner
# else is then skipped.
log = []
for outer in [1, 2]:
for inner in [10, 20]:
log.append((outer, inner))
break
else:
log.append("inner-else-must-not-run")
log.append(("after", outer))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log

# Three levels: the break binds to the loop enclosing the loop whose else it is,
# i.e. the middle one, so the outermost keeps iterating.
log = []
for a in [1, 2]:
for b in [10, 20]:
log.append((a, b))
for c in []:
pass
else:
break
log.append(("outer", a))
assert log == [(1, 10), ("outer", 1), (2, 10), ("outer", 2)], log

# An else that neither breaks nor continues still falls through to the exit.
log = []
for outer in [1, 2]:
for inner in []:
pass
else:
log.append(("else", outer))
log.append(("after", outer))
assert log == [("else", 1), ("after", 1), ("else", 2), ("after", 2)], log

# An else nested inside another else: the break binds outwards through *both*, to
# the outermost loop. Checking only the first nested loop's else missed this and
# silently dropped the break.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
break
assert log == [1], log

# Same shape with `continue`, which must skip the rest of the outermost body.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
continue
log.append("after-must-not-run")
assert log == [1, 2, 3], log

# Same shape built from `while`s. Here the mis-binding was not silent: the branch
# was emitted while the inner py.while was still a region of its own, which the
# verifier rejects as a reference to a block in another region.
log = []
for a in [1, 2, 3]:
log.append(a)
while False:
pass
else:
while False:
pass
else:
break
assert log == [1], log

# Three elses deep, to check the walk follows the chain rather than a fixed depth.
log = []
for a in [1, 2, 3]:
log.append(a)
for b in []:
pass
else:
for c in []:
pass
else:
for d in []:
pass
else:
break
assert log == [1], log

# The chain stops at the first loop *body*: this break is in the body of a loop
# that happens to sit in an else, so it binds to that loop and no further.
log = []
for a in [1, 2]:
for b in []:
pass
else:
for c in [10, 20]:
log.append((a, c))
break
log.append(("after", a))
assert log == [(1, 10), ("after", 1), (2, 10), ("after", 2)], log
98 changes: 98 additions & 0 deletions integration/tests/unreachable_code.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
"""Statements following a terminator in the same suite.

`break`, `continue` and `return` leave the builder's insertion point in a block
that already ends in a terminator, so MLIRGenerator used to append whatever came
next in the suite *after* that terminator:

'python.br_yield' op must be the last operation in the parent block

The verifier rejected it, but MLIR's region DCE reached it first and segfaulted
(deleteDeadness reading a null terminator), so the diagnostic never mattered.
MLIRGenerator::codegen_statements now stops at the first statement that terminates the
block, which is also what unreachable code means.

Reduced from sre_parse._parse, which is why `import re` crashed during lowering.
Nothing here asserts on the unreachable statements themselves — they cannot run;
the point is that the module compiles and the reachable behaviour is right.
"""


def after_break(values):
seen = []
for v in values:
seen.append(v)
if v == 2:
break
seen.append("unreachable")
raise ValueError("unreachable")
return seen


assert after_break([1, 2, 3]) == [1, 2], after_break([1, 2, 3])


def after_continue(values):
seen = []
for v in values:
if v == 2:
continue
seen.append("unreachable")
seen.append(v)
return seen


assert after_continue([1, 2, 3]) == [1, 3], after_continue([1, 2, 3])


def after_return(a):
return a + 1
b = a * 2
raise ValueError("unreachable")


assert after_return(1) == 2, after_return(1)


def after_break_in_while(a):
n = 0
while True:
n += 1
if n >= a:
break
n = 999
raise ValueError("unreachable")
return n


assert after_break_in_while(3) == 3, after_break_in_while(3)


def after_break_in_try(values):
seen = []
for v in values:
try:
seen.append(v)
if v == 2:
break
raise ValueError("unreachable")
except ValueError:
seen.append("caught")
return seen


assert after_break_in_try([1, 2, 3]) == [1, 2], after_break_in_try([1, 2, 3])


def after_raise(a):
if a:
raise ValueError("boom")
a = 999
return a


try:
after_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert after_raise(False) is False
93 changes: 93 additions & 0 deletions integration/tests/while_condition_cse.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
"""A while condition whose value is defined outside the condition region.

WhileOpLowering built the loop's test and cf.cond_br at the *condition value's*
definition site. That is usually inside the condition region, but not always: CSE
merges the constant behind `while True:` with an identical constant in the
enclosing function, after which py.condition tests a value defined in the
function's entry block. Inserting there put the cf.cond_br in the middle of that
block, as a second terminator, and MLIR's region DCE then segfaulted on the block
whose last operation was no longer a terminator.

py.condition is by construction the terminator of the condition region's last
block, and the value it tests necessarily dominates it, so that is where the
branch belongs.

`b = True` before the loop is what creates the constant CSE merges with — without
it the loop's `True` is unique and the bug does not appear. Reduced from
sre_parse._parse; the same fault was the long-standing `import weakref` crash.
"""


def only_exit_is_raise(a):
b = True
if a:
while True:
raise ValueError("boom")
return b


try:
only_exit_is_raise(True)
raise AssertionError("should have raised")
except ValueError as e:
assert str(e) == "boom", str(e)
assert only_exit_is_raise(False) is True


def shared_true_constant(limit):
flag = True
n = 0
while True:
n += 1
if n >= limit:
break
return (n, flag)


assert shared_true_constant(3) == (3, True), shared_true_constant(3)


def shared_false_constant(a):
flag = False
n = 0
while not flag:
n += 1
if n >= a:
flag = True
return n


assert shared_false_constant(2) == 2, shared_false_constant(2)


def condition_is_a_parameter(cond, limit):
# The condition value is a block argument rather than an op result, the other
# branch of the insertion-point choice that used to exist.
n = 0
while cond:
n += 1
if n >= limit:
cond = False
return n


assert condition_is_a_parameter(True, 2) == 2, condition_is_a_parameter(True, 2)
assert condition_is_a_parameter(False, 2) == 0, condition_is_a_parameter(False, 2)


def nested_loops_sharing_true(limit):
t = True
outer = 0
while True:
outer += 1
inner = 0
while True:
inner += 1
if inner >= 2:
break
if outer >= limit:
break
return (outer, inner, t)


assert nested_loops_sharing_true(2) == (2, 2, True), nested_loops_sharing_true(2)
Loading
Loading