Skip to content

Commit af25589

Browse files
authored
Merge pull request #23 from gf712/fix/generator-resume-stack-rebase
Various fixes to codegen and generator runtime
2 parents 17fd37c + 5a410ef commit af25589

57 files changed

Lines changed: 3441 additions & 2393 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Regression test for BaseException str()/args/__traceback__ and for per-assert
2+
# traceback line numbers (message-less asserts must not share a merged block that
3+
# collapses their source locations).
4+
5+
6+
def deepest_lineno(exc):
7+
tb = exc.__traceback__
8+
while tb.tb_next is not None:
9+
tb = tb.tb_next
10+
return tb.tb_lineno
11+
12+
13+
# --- str(exc) is the message; args is the tuple; __traceback__ is exposed ---
14+
try:
15+
raise ValueError("boom")
16+
except ValueError as e:
17+
assert str(e) == "boom", str(e)
18+
assert e.args == ("boom",), e.args
19+
assert isinstance(e, ValueError)
20+
assert e.__traceback__ is not None
21+
22+
try:
23+
raise ValueError("a", "b")
24+
except ValueError as e:
25+
assert e.args == ("a", "b"), e.args
26+
27+
try:
28+
raise KeyError("k")
29+
except KeyError as e:
30+
assert e.args == ("k",), e.args
31+
32+
33+
# --- the traceback line is the raising statement's line ---
34+
def raises_value_error():
35+
raise ValueError("here") # EXC_RAISE_LINE
36+
37+
38+
try:
39+
raises_value_error()
40+
except ValueError as e:
41+
assert deepest_lineno(e) == 35, deepest_lineno(e)
42+
43+
44+
# --- a later message-less assert reports ITS OWN line, not the first assert's
45+
# (regression for the merged-assertion-block traceback bug) ---
46+
def fails_on_third_assert():
47+
assert True
48+
assert True
49+
assert False # EXC_ASSERT_LINE
50+
51+
52+
try:
53+
fails_on_third_assert()
54+
except AssertionError as e:
55+
assert deepest_lineno(e) == 49, deepest_lineno(e)
56+
57+
print("EXCEPTION_ATTRIBUTES_OK")
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# `except <Type> as <name>:` must bind <name> to the exception INSTANCE,
2+
# not to the matched type. Regression test for the MLIRGenerator handler
3+
# codegen (py.load_exception).
4+
5+
raised = ValueError("boom")
6+
try:
7+
raise raised
8+
except ValueError as e:
9+
assert e is raised, "e must be the raised instance"
10+
assert isinstance(e, ValueError)
11+
assert type(e) is ValueError
12+
13+
# the bound name must be the instance even with multiple candidate handlers
14+
try:
15+
raise KeyError("k")
16+
except ValueError as e:
17+
bound = ("value", e)
18+
except KeyError as e:
19+
bound = ("key", e)
20+
assert bound[0] == "key"
21+
assert isinstance(bound[1], KeyError)
22+
assert type(bound[1]) is KeyError
23+
24+
# nested handlers each bind their own instance
25+
inner_exc = TypeError("inner")
26+
outer_exc = IndexError("outer")
27+
try:
28+
try:
29+
raise inner_exc
30+
except TypeError as e:
31+
assert e is inner_exc
32+
raise outer_exc
33+
except IndexError as e:
34+
assert e is outer_exc
35+
assert e is not inner_exc
36+
37+
print("EXCEPTION_BINDING_OK")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Regression: exception chaining — __cause__ (explicit, via `raise X from Y`),
2+
# implicit __context__ (the exception being handled when a new one is raised),
3+
# and __suppress_context__. Also covers the exception-stack hygiene that makes
4+
# these reliable: internally-consumed StopIterations no longer linger, so a bare
5+
# `raise` outside a handler is a RuntimeError and __context__ isn't spuriously
6+
# populated.
7+
8+
9+
# `raise X from Y` sets __cause__ to the instance and suppresses context.
10+
try:
11+
try:
12+
raise ValueError("inner")
13+
except ValueError as e:
14+
raise KeyError("outer") from e
15+
except KeyError as k:
16+
assert isinstance(k.__cause__, ValueError), k.__cause__
17+
assert str(k.__cause__) == "inner", str(k.__cause__)
18+
assert k.__suppress_context__ is True, k.__suppress_context__
19+
# __context__ is still set implicitly (suppress only affects display).
20+
assert isinstance(k.__context__, ValueError), k.__context__
21+
22+
23+
# Implicit chaining without `from`: __context__ is the handled exception.
24+
try:
25+
try:
26+
raise ValueError("v1")
27+
except ValueError:
28+
raise KeyError("k1")
29+
except KeyError as k:
30+
assert k.__cause__ is None, k.__cause__
31+
assert isinstance(k.__context__, ValueError), k.__context__
32+
assert str(k.__context__) == "v1", str(k.__context__)
33+
assert k.__suppress_context__ is False, k.__suppress_context__
34+
35+
36+
# `raise X from None` -> cause None, still suppressed.
37+
try:
38+
raise KeyError("k") from None
39+
except KeyError as k:
40+
assert k.__cause__ is None, k.__cause__
41+
assert k.__suppress_context__ is True, k.__suppress_context__
42+
43+
44+
# Plain exception raised outside any handler: no cause, no context.
45+
try:
46+
raise ValueError("plain")
47+
except ValueError as e:
48+
assert e.__cause__ is None, e.__cause__
49+
assert e.__context__ is None, e.__context__
50+
assert e.__suppress_context__ is False, e.__suppress_context__
51+
52+
53+
# A bare `raise` with no active exception is a RuntimeError (not an abort, and
54+
# not a stale leftover exception).
55+
try:
56+
raise
57+
except RuntimeError as e:
58+
assert str(e) == "No active exception to reraise", str(e)
59+
60+
61+
# Iterating a generator / comprehensions while handling an exception must not
62+
# disturb the active exception (exception-stack hygiene).
63+
def gen():
64+
yield 1
65+
yield 2
66+
yield 3
67+
68+
69+
try:
70+
raise ValueError("active")
71+
except ValueError as e:
72+
assert set(gen()) == {1, 2, 3}
73+
assert [x for x in range(4)] == [0, 1, 2, 3]
74+
assert {k: k * k for k in range(3)} == {0: 0, 1: 1, 2: 4}
75+
assert isinstance(e, ValueError) and str(e) == "active"
76+
77+
78+
# Chaining attributes are writable; setting __cause__ also suppresses context.
79+
try:
80+
raise ValueError("x")
81+
except ValueError as e:
82+
ctx = RuntimeError("ctx")
83+
e.__context__ = ctx
84+
assert e.__context__ is ctx
85+
e.__cause__ = ctx
86+
assert e.__cause__ is ctx
87+
assert e.__suppress_context__ is True
88+
e.__suppress_context__ = False
89+
assert e.__suppress_context__ is False
90+
91+
92+
print("EXCEPTION_CHAINING_OK")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Regression: every builtin exception type must construct (raise X(...)) without
2+
# crashing — Exception subclasses missing their own __new__ used to inherit
3+
# Exception::__new__ (which asserts the exact Exception type), and
4+
# ModuleNotFoundError dereferenced a null kwargs.
5+
6+
builtin_exceptions = [
7+
BaseException, Exception, ValueError, KeyError, IndexError, TypeError,
8+
NameError, AttributeError, RuntimeError, NotImplementedError, ImportError,
9+
ModuleNotFoundError, OSError, LookupError, MemoryError, StopIteration,
10+
UnboundLocalError, AssertionError,
11+
]
12+
13+
14+
for exc_type in builtin_exceptions:
15+
try:
16+
raise exc_type("msg")
17+
except BaseException as e:
18+
assert isinstance(e, exc_type), exc_type
19+
assert type(e) is exc_type, (type(e), exc_type)
20+
assert e.args == ("msg",), (exc_type, e.args)
21+
22+
23+
# subclass relationships still hold
24+
try:
25+
raise RuntimeError("r")
26+
except Exception as e:
27+
assert isinstance(e, RuntimeError)
28+
assert isinstance(e, Exception)
29+
assert isinstance(e, BaseException)
30+
31+
32+
# constructed with no args
33+
try:
34+
raise ValueError
35+
except ValueError as e:
36+
assert e.args == ()
37+
38+
39+
print("EXCEPTION_TYPES_OK")
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# Regression test for generator resumption when consumed outside a `for` loop.
2+
3+
def gen_simple():
4+
yield 1
5+
yield 2
6+
yield 3
7+
8+
9+
# list()/tuple() resume from inside the constructor call (a deeper frame).
10+
assert list(gen_simple()) == [1, 2, 3]
11+
assert tuple(gen_simple()) == (1, 2, 3)
12+
13+
# Top-level next() across several resumes.
14+
it = gen_simple()
15+
assert next(it) == 1
16+
assert next(it) == 2
17+
assert next(it) == 3
18+
19+
20+
# Generator with parameters and locals carried across yields: exercises a
21+
# non-zero locals_count when the frame is rebased.
22+
def running_total(n):
23+
total = 0
24+
for i in range(n):
25+
total += i
26+
yield total
27+
28+
29+
assert list(running_total(5)) == [0, 1, 3, 6, 10]
30+
31+
32+
# Nested `yield from` consumed by list().
33+
def inner():
34+
yield from [1, 2, 3]
35+
36+
37+
def outer():
38+
yield from inner()
39+
yield 4
40+
41+
42+
assert list(outer()) == [1, 2, 3, 4]
43+
44+
45+
# Two generators alive at once, advanced in interleaved order.
46+
def tagged(tag):
47+
yield tag
48+
yield tag + 10
49+
50+
51+
a = tagged(1)
52+
b = tagged(2)
53+
assert next(a) == 1
54+
assert next(b) == 2
55+
assert next(a) == 11
56+
assert next(b) == 12
57+
58+
59+
# The `for` path (which already worked) must keep working.
60+
collected = []
61+
for value in gen_simple():
62+
collected.append(value)
63+
assert collected == [1, 2, 3]
64+
65+
66+
# A generator consumed from inside another function-call frame.
67+
def consume_first(iterator):
68+
return next(iterator)
69+
70+
71+
assert consume_first(gen_simple()) == 1
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Regression: exception-handler edges must be modelled in liveness.
2+
#
3+
# An operation inside a try body can transfer to the handler, but that edge is
4+
# not in the explicit CFG. When liveness ignored it, a value live across the try
5+
# body via the handler path (e.g. a FOR_ITER iterator, or a value used after the
6+
# handler) had its register reused inside the try body and was clobbered when an
7+
# exception actually unwound.
8+
9+
10+
# A for-loop whose body raises and catches: the iterator must survive the try
11+
# body. Previously clobbered (abort in FOR_ITER / "object is not an iterator").
12+
seen = []
13+
for x in [1, 2, 3]:
14+
try:
15+
raise ValueError("m")
16+
except ValueError:
17+
pass
18+
seen.append(x)
19+
assert seen == [1, 2, 3], seen
20+
21+
# Same over range() and over a list of types, with the exception bound.
22+
total = 0
23+
for x in range(4):
24+
try:
25+
raise ValueError("m")
26+
except ValueError as e:
27+
assert str(e) == "m"
28+
total += x
29+
assert total == 6, total
30+
31+
for exc in [ValueError, KeyError, RuntimeError, TypeError, NameError]:
32+
try:
33+
raise exc("msg")
34+
except BaseException as e:
35+
assert isinstance(e, exc), exc
36+
assert e.args == ("msg",), (exc, e.args)
37+
38+
# Sequential try/except in one frame must not leak the prior exception's args.
39+
try:
40+
raise ValueError("hello")
41+
except ValueError as e:
42+
assert e.args == ("hello",), e.args
43+
try:
44+
raise ValueError("a", "b")
45+
except ValueError as e:
46+
assert e.args == ("a", "b"), e.args
47+
48+
# A recursive call whose result must survive a following try/except (the
49+
# original minimal miscompile repro).
50+
def fib(n):
51+
return n if n < 2 else fib(n - 1) + fib(n - 2)
52+
53+
54+
assert fib(10) == 55
55+
try:
56+
raise ValueError("e")
57+
except ValueError as e:
58+
assert str(e) == "e"
59+
60+
# Nested try/except inside a loop.
61+
acc = 0
62+
for x in [1, 2, 3]:
63+
try:
64+
try:
65+
raise ValueError(x)
66+
except KeyError:
67+
pass
68+
except ValueError as e:
69+
acc += e.args[0]
70+
assert acc == 6, acc
71+
72+
print("REGALLOC_EXCEPTION_LIVENESS_OK")

0 commit comments

Comments
 (0)