-
-
Notifications
You must be signed in to change notification settings - Fork 30.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] bpo-17611: Move unwinding of stack from interpreter to compiler #2827
Conversation
@pitrou, thanks for your PR! By analyzing the history of the files in this pull request, we identified @1st1, @serhiy-storchaka and @benjaminp to be potential reviewers. |
The test_dis failures are expected, as I haven't updated that file yet. |
However, I don't understand the additional test failures under Windows. |
Ok, it seems the Windows failures were due to a tracing bug without computed gotos. |
Python/compile.c
Outdated
ADDOP_O(c, LOAD_CONST, Py_None, consts); | ||
compiler_nameop(c, handler->v.ExceptHandler.name, Store); | ||
compiler_nameop(c, handler->v.ExceptHandler.name, Del); | ||
ADDOP_JREL(c, JUMP_FORWARD, end); | ||
|
||
/* finally: */ | ||
ADDOP_O(c, LOAD_CONST, Py_None, consts); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems this is a dead code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Published the part of review comments since the PR is changed.
Objects/frameobject.c
Outdated
@@ -52,7 +52,7 @@ frame_getlineno(PyFrameObject *f, void *closure) | |||
* o Lines with an 'except' statement on them can't be jumped to, because | |||
* they expect an exception to be on the top of the stack. | |||
* o Lines that live in a 'finally' block can't be jumped from or to, since | |||
* the END_FINALLY expects to clean up the stack after the 'try' block. | |||
* the RERAISE expects to clean up the stack after the 'try' block. | |||
* o 'try'/'for'/'while' blocks can't be jumped into because the blockstack |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the 'while' block should be excluded from this and the 'with' and 'async with' blocks should be added.
@@ -52,7 +52,7 @@ frame_getlineno(PyFrameObject *f, void *closure) | |||
* o Lines with an 'except' statement on them can't be jumped to, because | |||
* they expect an exception to be on the top of the stack. | |||
* o Lines that live in a 'finally' block can't be jumped from or to, since |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now there is a different reason for this. Since a 'finally' block is duplicated, different instruction offsets correspond to the same line.
Objects/frameobject.c
Outdated
@@ -176,14 +178,18 @@ frame_setlineno(PyFrameObject *f, PyObject* p_new_lineno) | |||
} | |||
|
|||
/* You can't jump into or out of a 'finally' block because the 'try' | |||
* block leaves something on the stack for the END_FINALLY to clean | |||
* up. So we walk the bytecode, maintaining a simulated blockstack. | |||
* block leaves something on the stack for the RETRY to clean up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
RERAISE instead of RETRY.
Objects/frameobject.c
Outdated
/* This is the start of the 'finally' block. | ||
* It will end with RERAISE (if a 'try..finally' block) | ||
* or WITH_EXCEPT_FINISH (if a 'with' block). | ||
*/ | ||
in_finally[blockstack_top-1] = 1; | ||
} | ||
else { | ||
blockstack_top--; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There may be multiple POP_BLOCK instructions following SETUP_EXCEPT. For example see the disassemble of
for i in a:
try:
if i:
break
except:
b
c
Objects/frameobject.c
Outdated
in_finally[blockstack_top-1] = 1; | ||
} | ||
else { | ||
blockstack_top--; | ||
} | ||
break; | ||
|
||
case END_FINALLY: | ||
/* Ignore END_FINALLYs for SETUP_EXCEPTs - they exist |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this comment is removed? If it no longer valid perhaps some following checks can be replaced with asserts.
delta_iblock++; | ||
break; | ||
|
||
case POP_BLOCK: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Due to code duplication several POP_BLOCK instructions can correspond to the single SETUP_EXCEPT or SETUP_FINALLY instruction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've now removed all code duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I was not clear. Every break
, continue
and return
in try
...except
block adds a POP_BLOCK
if it leaves the block. The number of POP_BLOCK
s may be larger than the number of SETUP_EXCEPT
s and SETUP_FINALLY
s. blockstack_top
may become negative. See my comment at https://github.com/python/cpython/pull/2827/files/693b9398b5fd202fa5864f9cc76fa1bc7f84f62e#r128971050.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Would you like to suggest another approach?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a ready solution. May be we can use target addresses of SETUP_EXCEPT and SETUP_FINALLY instructions (similarly to FOR_ITER). Or add special never executed stop-code at the ends of blocks. Or use additional data structure for starts and ends of all blocks (actually this would allow to get rid of instructions SETUP_EXCEPT, SETUP_FINALLY and POP_BLOCK and speed up stack unrolling; I'm going to try this approach in separate issue).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why JUMP_FINALLY
should be a jumping instruction.
Python/ceval.c
Outdated
@@ -2844,6 +2854,18 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag) | |||
FAST_DISPATCH(); | |||
} | |||
|
|||
TARGET(JUMP_FINALLY) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we push 3 NULLs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes.
Python/ceval.c
Outdated
PUSH(exc); | ||
PUSH(exc); | ||
PUSH(exc); | ||
JUMPBY(oparg); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it needed? It seems to me that JUMP_FINALLY always jumps to the next instruction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That might be, I'll give it a try.
case FOR_ITER: | ||
/* Store the loop end in forstack[] */ | ||
oparg = code[addr + 1]; | ||
target_addr = addr + oparg + 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use sizeof(_Py_CODEUNIT)
instead of 2.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, the entire frameobject code deals with char *
code objects? I admit I had glossed a bit over it when doing this PR. I am surprised this hasn't been migrated to more modern spellings...
switch (op) { | ||
case FOR_ITER: | ||
/* Store the loop end in forstack[] */ | ||
oparg = code[addr + 1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget about EXTENDED_ARG
. See get_arg()
in peephole.c
.
oparg = code[addr + 1]; | ||
target_addr = addr + oparg + 2; | ||
assert(target_addr < code_len); | ||
forstack[forstack_top++] = target_addr; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forstack
is not needed for determining whether the jump is allowed.
The jump is forbidden if (addr < first_addr && first_addr < target_addr) != (addr < second_addr && second_addr < target_addr)
for any 'for' block.
If assume first_addr < second_addr
and iterate while addr < second_addr
this may be optimized to (first_addr < target_addr) && ((addr < first_addr) != (second_addr < target_addr))
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The jump is forbidden if (addr < first_addr && first_addr < target_addr) != (addr < second_addr && second_addr < target_addr) for any 'for' block.
I don't think this is true. It is actually allowed to jump out of a for
loop, just not into.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then following pseudocode:
first_in = addr < first_addr && first_addr < target_addr;
second_in = addr < second_addr && second_addr < target_addr;
if (!first_in && second_in) goto forbidden; /* or first_in < second_in */
for_loop_delta += second_in - first_in; /* non-positive */
delta_iblock++; | ||
break; | ||
|
||
case POP_BLOCK: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I was not clear. Every break
, continue
and return
in try
...except
block adds a POP_BLOCK
if it leaves the block. The number of POP_BLOCK
s may be larger than the number of SETUP_EXCEPT
s and SETUP_FINALLY
s. blockstack_top
may become negative. See my comment at https://github.com/python/cpython/pull/2827/files/693b9398b5fd202fa5864f9cc76fa1bc7f84f62e#r128971050.
Serhiy, do you want to take over this PR? I am not much motivated by the remaining cleanup / dealing with existing cruft. |
I take over this PR. Thank you Antoine, you have done hard work for which I did not dare to undertake. I have fetched the branch from your repository. |
Thank you Serhiy :-) |
enum fblocktype { LOOP, EXCEPT, FINALLY_TRY, FINALLY_END }; | ||
enum fblocktype { LOOP, EXCEPT, FINALLY_TRY, FINALLY_END, ASYNC_WITH, | ||
HANDLER_CLEANUP, WITH, NO_TYPE }; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Is there a reason WITH doesn't come before ASYNC_WITH? Also, should HANDLER_CLEANUP be directly after the FINALLY_END?
It would be quite good to get this change "to the finish line", so to speak. This is an ugly little corner of CPython and cleaning it up is difficult. That's why it has hung around all these years. This patch looks like it does the job, even though it is complicated. It seems to me that 95% of the hard work has been done by Antoine and Mark already. I have rebased it on the current "master" branch, see nascheme:unwind_stack. The rebase was not totally painless. I had to re-generate the importlib stuff (not surprising). In ceval, I had to make the following changes:
The unit test test_dist and test_importlib fail for me. It looks like test_dis hasn't been updated to match the latest changes made to the bytecode. Not sure what's wrong with importlib. |
https://bugs.python.org/issue17611