py/vm: Fix handling of finally-return with complex nested finallys.

Back in 8047340d75 basic support was added in
the VM to handle return statements within a finally block.  But it didn't
cover all cases, in particular when some finally's were active and others
inactive when the "return" was executed.

This patch adds further support for return-within-finally by correctly
managing the currently_in_except_block flag, and should fix all cases.  The
main point is that finally handlers remain on the exception stack even if
they are active (currently being executed), and the unwind return code
should only execute those finally's which are inactive.

New tests are added for the cases which now pass.
This commit is contained in:
Damien George 2018-09-03 13:08:16 +10:00
parent 828f771e32
commit b735208403
3 changed files with 109 additions and 11 deletions

16
py/vm.c
View File

@ -1063,17 +1063,11 @@ unwind_jump:;
ENTRY(MP_BC_RETURN_VALUE): ENTRY(MP_BC_RETURN_VALUE):
MARK_EXC_IP_SELECTIVE(); MARK_EXC_IP_SELECTIVE();
// These next 3 lines pop a try-finally exception handler, if one
// is there on the exception stack. Without this the finally block
// is executed a second time when the return is executed, because
// the try-finally exception handler is still on the stack.
// TODO Possibly find a better way to handle this case.
if (currently_in_except_block) {
POP_EXC_BLOCK();
}
unwind_return: unwind_return:
// Search for and execute finally handlers that aren't already active
while (exc_sp >= exc_stack) { while (exc_sp >= exc_stack) {
if (MP_TAGPTR_TAG1(exc_sp->val_sp)) { if (!currently_in_except_block && MP_TAGPTR_TAG1(exc_sp->val_sp)) {
// Found a finally handler that isn't active.
// Getting here the stack looks like: // Getting here the stack looks like:
// (..., X, [iter0, iter1, ...,] ret_val) // (..., X, [iter0, iter1, ...,] ret_val)
// where X is pointed to by exc_sp->val_sp and in the case // where X is pointed to by exc_sp->val_sp and in the case
@ -1092,10 +1086,10 @@ unwind_return:
// done (when WITH_CLEANUP or END_FINALLY reached). // done (when WITH_CLEANUP or END_FINALLY reached).
PUSH(MP_OBJ_NEW_SMALL_INT(-1)); PUSH(MP_OBJ_NEW_SMALL_INT(-1));
ip = exc_sp->handler; ip = exc_sp->handler;
exc_sp--; POP_EXC_BLOCK();
goto dispatch_loop; goto dispatch_loop;
} }
exc_sp--; POP_EXC_BLOCK();
} }
nlr_pop(); nlr_pop();
code_state->sp = sp; code_state->sp = sp;

View File

@ -0,0 +1,103 @@
# test 'return' within the finally block, with nested finally's
# only inactive finally's should be executed, and only once
# basic nested finally's, the print should only be executed once
def f():
try:
raise TypeError
finally:
print(1)
try:
raise ValueError
finally:
return 42
print(f())
# similar to above but more nesting
def f():
try:
raise ValueError
finally:
print(1)
try:
raise TypeError
finally:
print(2)
try:
pass
finally:
return 42
print(f())
# similar to above but even more nesting
def f():
try:
raise ValueError
finally:
print(1)
try:
raise TypeError
finally:
print(2)
try:
raise Exception
finally:
print(3)
return 42
print(f())
# upon return some try's are active, some finally's are active, some inactive
def f():
try:
try:
pass
finally:
print(2)
return 42
finally:
print(1)
print(f())
# same as above but raise instead of pass
def f():
try:
try:
raise ValueError
finally:
print(2)
return 42
finally:
print(1)
print(f())
# upon return exception stack holds: active finally, inactive finally, active finally
def f():
try:
raise Exception
finally:
print(1)
try:
try:
pass
finally:
print(3)
return 42
finally:
print(2)
print(f())
# same as above but raise instead of pass in innermost try block
def f():
try:
raise Exception
finally:
print(1)
try:
try:
raise Exception
finally:
print(3)
return 42
finally:
print(2)
print(f())

View File

@ -368,6 +368,7 @@ def run_tests(pyb, tests, args, base_path="."):
skip_tests.add('basics/try_finally_loops.py') # requires proper try finally code skip_tests.add('basics/try_finally_loops.py') # requires proper try finally code
skip_tests.add('basics/try_finally_return.py') # requires proper try finally code skip_tests.add('basics/try_finally_return.py') # requires proper try finally code
skip_tests.add('basics/try_finally_return2.py') # requires proper try finally code skip_tests.add('basics/try_finally_return2.py') # requires proper try finally code
skip_tests.add('basics/try_finally_return3.py') # requires proper try finally code
skip_tests.add('basics/unboundlocal.py') # requires checking for unbound local skip_tests.add('basics/unboundlocal.py') # requires checking for unbound local
skip_tests.add('import/gen_context.py') # requires yield_value skip_tests.add('import/gen_context.py') # requires yield_value
skip_tests.add('misc/features.py') # requires raise_varargs skip_tests.add('misc/features.py') # requires raise_varargs