micropython/tests/basics/try_finally_break.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

100 lines
2.0 KiB
Python
Raw Normal View History

py: Fix VM crash with unwinding jump out of a finally block. This patch fixes a bug in the VM when breaking within a try-finally. The bug has to do with executing a break within the finally block of a try-finally statement. For example: def f(): for x in (1,): print('a', x) try: raise Exception finally: print(1) break print('b', x) f() Currently in uPy the above code will print: a 1 1 1 segmentation fault (core dumped) micropython Not only is there a seg fault, but the "1" in the finally block is printed twice. This is because when the VM executes a finally block it doesn't really know if that block was executed due to a fall-through of the try (no exception raised), or because an exception is active. In particular, for nested finallys the VM has no idea which of the nested ones have active exceptions and which are just fall-throughs. So when a break (or continue) is executed it tries to unwind all of the finallys, when in fact only some may be active. It's questionable whether break (or return or continue) should be allowed within a finally block, because they implicitly swallow any active exception, but nevertheless it's allowed by CPython (although almost never used in the standard library). And uPy should at least not crash in such a case. The solution here relies on the fact that exception and finally handlers always appear in the bytecode after the try body. Note: there was a similar bug with a return in a finally block, but that was previously fixed in b735208403a54774f9fd3d966f7c1a194c41870f
2019-01-02 06:48:43 +00:00
# test break within (nested) finally
# basic case with break in finally
def f():
for _ in range(2):
print(1)
try:
pass
finally:
print(2)
break
print(3)
print(4)
print(5)
f()
# where the finally swallows an exception
def f():
lst = [1, 2, 3]
for x in lst:
print('a', x)
try:
raise Exception
finally:
print(1)
break
print('b', x)
f()
# basic nested finally with break in inner finally
def f():
for i in range(2):
print('iter', i)
try:
raise TypeError
finally:
print(1)
try:
raise ValueError
finally:
break
print(f())
# similar to above but more nesting
def f():
for i in range(2):
try:
raise ValueError
finally:
print(1)
try:
raise TypeError
finally:
print(2)
try:
pass
finally:
break
print(f())
# lots of nesting
def f():
for i in range(2):
try:
raise ValueError
finally:
print(1)
try:
raise TypeError
finally:
print(2)
try:
raise Exception
finally:
break
print(f())
# basic case combined with try-else
def f(arg):
for _ in range(2):
print(1)
try:
if arg == 1:
raise ValueError
elif arg == 2:
raise TypeError
except ValueError:
print(2)
else:
print(3)
finally:
print(4)
break
print(5)
print(6)
print(7)
f(0) # no exception, else should execute
f(1) # exception caught, else should be skipped
f(2) # exception not caught, finally swallows exception, else should be skipped