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
57 changes: 57 additions & 0 deletions integration/tests/exception_attributes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Regression test for BaseException str()/args/__traceback__ and for per-assert
# traceback line numbers (message-less asserts must not share a merged block that
# collapses their source locations).


def deepest_lineno(exc):
tb = exc.__traceback__
while tb.tb_next is not None:
tb = tb.tb_next
return tb.tb_lineno


# --- str(exc) is the message; args is the tuple; __traceback__ is exposed ---
try:
raise ValueError("boom")
except ValueError as e:
assert str(e) == "boom", str(e)
assert e.args == ("boom",), e.args
assert isinstance(e, ValueError)
assert e.__traceback__ is not None

try:
raise ValueError("a", "b")
except ValueError as e:
assert e.args == ("a", "b"), e.args

try:
raise KeyError("k")
except KeyError as e:
assert e.args == ("k",), e.args


# --- the traceback line is the raising statement's line ---
def raises_value_error():
raise ValueError("here") # EXC_RAISE_LINE


try:
raises_value_error()
except ValueError as e:
assert deepest_lineno(e) == 35, deepest_lineno(e)


# --- a later message-less assert reports ITS OWN line, not the first assert's
# (regression for the merged-assertion-block traceback bug) ---
def fails_on_third_assert():
assert True
assert True
assert False # EXC_ASSERT_LINE


try:
fails_on_third_assert()
except AssertionError as e:
assert deepest_lineno(e) == 49, deepest_lineno(e)

print("EXCEPTION_ATTRIBUTES_OK")
37 changes: 37 additions & 0 deletions integration/tests/exception_binding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# `except <Type> as <name>:` must bind <name> to the exception INSTANCE,
# not to the matched type. Regression test for the MLIRGenerator handler
# codegen (py.load_exception).

raised = ValueError("boom")
try:
raise raised
except ValueError as e:
assert e is raised, "e must be the raised instance"
assert isinstance(e, ValueError)
assert type(e) is ValueError

# the bound name must be the instance even with multiple candidate handlers
try:
raise KeyError("k")
except ValueError as e:
bound = ("value", e)
except KeyError as e:
bound = ("key", e)
assert bound[0] == "key"
assert isinstance(bound[1], KeyError)
assert type(bound[1]) is KeyError

# nested handlers each bind their own instance
inner_exc = TypeError("inner")
outer_exc = IndexError("outer")
try:
try:
raise inner_exc
except TypeError as e:
assert e is inner_exc
raise outer_exc
except IndexError as e:
assert e is outer_exc
assert e is not inner_exc

print("EXCEPTION_BINDING_OK")
92 changes: 92 additions & 0 deletions integration/tests/exception_chaining.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Regression: exception chaining — __cause__ (explicit, via `raise X from Y`),
# implicit __context__ (the exception being handled when a new one is raised),
# and __suppress_context__. Also covers the exception-stack hygiene that makes
# these reliable: internally-consumed StopIterations no longer linger, so a bare
# `raise` outside a handler is a RuntimeError and __context__ isn't spuriously
# populated.


# `raise X from Y` sets __cause__ to the instance and suppresses context.
try:
try:
raise ValueError("inner")
except ValueError as e:
raise KeyError("outer") from e
except KeyError as k:
assert isinstance(k.__cause__, ValueError), k.__cause__
assert str(k.__cause__) == "inner", str(k.__cause__)
assert k.__suppress_context__ is True, k.__suppress_context__
# __context__ is still set implicitly (suppress only affects display).
assert isinstance(k.__context__, ValueError), k.__context__


# Implicit chaining without `from`: __context__ is the handled exception.
try:
try:
raise ValueError("v1")
except ValueError:
raise KeyError("k1")
except KeyError as k:
assert k.__cause__ is None, k.__cause__
assert isinstance(k.__context__, ValueError), k.__context__
assert str(k.__context__) == "v1", str(k.__context__)
assert k.__suppress_context__ is False, k.__suppress_context__


# `raise X from None` -> cause None, still suppressed.
try:
raise KeyError("k") from None
except KeyError as k:
assert k.__cause__ is None, k.__cause__
assert k.__suppress_context__ is True, k.__suppress_context__


# Plain exception raised outside any handler: no cause, no context.
try:
raise ValueError("plain")
except ValueError as e:
assert e.__cause__ is None, e.__cause__
assert e.__context__ is None, e.__context__
assert e.__suppress_context__ is False, e.__suppress_context__


# A bare `raise` with no active exception is a RuntimeError (not an abort, and
# not a stale leftover exception).
try:
raise
except RuntimeError as e:
assert str(e) == "No active exception to reraise", str(e)


# Iterating a generator / comprehensions while handling an exception must not
# disturb the active exception (exception-stack hygiene).
def gen():
yield 1
yield 2
yield 3


try:
raise ValueError("active")
except ValueError as e:
assert set(gen()) == {1, 2, 3}
assert [x for x in range(4)] == [0, 1, 2, 3]
assert {k: k * k for k in range(3)} == {0: 0, 1: 1, 2: 4}
assert isinstance(e, ValueError) and str(e) == "active"


# Chaining attributes are writable; setting __cause__ also suppresses context.
try:
raise ValueError("x")
except ValueError as e:
ctx = RuntimeError("ctx")
e.__context__ = ctx
assert e.__context__ is ctx
e.__cause__ = ctx
assert e.__cause__ is ctx
assert e.__suppress_context__ is True
e.__suppress_context__ = False
assert e.__suppress_context__ is False


print("EXCEPTION_CHAINING_OK")
39 changes: 39 additions & 0 deletions integration/tests/exception_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Regression: every builtin exception type must construct (raise X(...)) without
# crashing — Exception subclasses missing their own __new__ used to inherit
# Exception::__new__ (which asserts the exact Exception type), and
# ModuleNotFoundError dereferenced a null kwargs.

builtin_exceptions = [
BaseException, Exception, ValueError, KeyError, IndexError, TypeError,
NameError, AttributeError, RuntimeError, NotImplementedError, ImportError,
ModuleNotFoundError, OSError, LookupError, MemoryError, StopIteration,
UnboundLocalError, AssertionError,
]


for exc_type in builtin_exceptions:
try:
raise exc_type("msg")
except BaseException as e:
assert isinstance(e, exc_type), exc_type
assert type(e) is exc_type, (type(e), exc_type)
assert e.args == ("msg",), (exc_type, e.args)


# subclass relationships still hold
try:
raise RuntimeError("r")
except Exception as e:
assert isinstance(e, RuntimeError)
assert isinstance(e, Exception)
assert isinstance(e, BaseException)


# constructed with no args
try:
raise ValueError
except ValueError as e:
assert e.args == ()


print("EXCEPTION_TYPES_OK")
71 changes: 71 additions & 0 deletions integration/tests/generator_consume.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Regression test for generator resumption when consumed outside a `for` loop.

def gen_simple():
yield 1
yield 2
yield 3


# list()/tuple() resume from inside the constructor call (a deeper frame).
assert list(gen_simple()) == [1, 2, 3]
assert tuple(gen_simple()) == (1, 2, 3)

# Top-level next() across several resumes.
it = gen_simple()
assert next(it) == 1
assert next(it) == 2
assert next(it) == 3


# Generator with parameters and locals carried across yields: exercises a
# non-zero locals_count when the frame is rebased.
def running_total(n):
total = 0
for i in range(n):
total += i
yield total


assert list(running_total(5)) == [0, 1, 3, 6, 10]


# Nested `yield from` consumed by list().
def inner():
yield from [1, 2, 3]


def outer():
yield from inner()
yield 4


assert list(outer()) == [1, 2, 3, 4]


# Two generators alive at once, advanced in interleaved order.
def tagged(tag):
yield tag
yield tag + 10


a = tagged(1)
b = tagged(2)
assert next(a) == 1
assert next(b) == 2
assert next(a) == 11
assert next(b) == 12


# The `for` path (which already worked) must keep working.
collected = []
for value in gen_simple():
collected.append(value)
assert collected == [1, 2, 3]


# A generator consumed from inside another function-call frame.
def consume_first(iterator):
return next(iterator)


assert consume_first(gen_simple()) == 1
72 changes: 72 additions & 0 deletions integration/tests/regalloc_exception_liveness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Regression: exception-handler edges must be modelled in liveness.
#
# An operation inside a try body can transfer to the handler, but that edge is
# not in the explicit CFG. When liveness ignored it, a value live across the try
# body via the handler path (e.g. a FOR_ITER iterator, or a value used after the
# handler) had its register reused inside the try body and was clobbered when an
# exception actually unwound.


# A for-loop whose body raises and catches: the iterator must survive the try
# body. Previously clobbered (abort in FOR_ITER / "object is not an iterator").
seen = []
for x in [1, 2, 3]:
try:
raise ValueError("m")
except ValueError:
pass
seen.append(x)
assert seen == [1, 2, 3], seen

# Same over range() and over a list of types, with the exception bound.
total = 0
for x in range(4):
try:
raise ValueError("m")
except ValueError as e:
assert str(e) == "m"
total += x
assert total == 6, total

for exc in [ValueError, KeyError, RuntimeError, TypeError, NameError]:
try:
raise exc("msg")
except BaseException as e:
assert isinstance(e, exc), exc
assert e.args == ("msg",), (exc, e.args)

# Sequential try/except in one frame must not leak the prior exception's args.
try:
raise ValueError("hello")
except ValueError as e:
assert e.args == ("hello",), e.args
try:
raise ValueError("a", "b")
except ValueError as e:
assert e.args == ("a", "b"), e.args

# A recursive call whose result must survive a following try/except (the
# original minimal miscompile repro).
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)


assert fib(10) == 55
try:
raise ValueError("e")
except ValueError as e:
assert str(e) == "e"

# Nested try/except inside a loop.
acc = 0
for x in [1, 2, 3]:
try:
try:
raise ValueError(x)
except KeyError:
pass
except ValueError as e:
acc += e.args[0]
assert acc == 6, acc

print("REGALLOC_EXCEPTION_LIVENESS_OK")
Loading
Loading