[WIP] bpo-17611: Move unwinding of stack from interpreter to compiler#2827
[WIP] bpo-17611: Move unwinding of stack from interpreter to compiler#2827pitrou wants to merge 20 commits intopython:masterfrom
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_JREL(c, JUMP_FORWARD, end); | ||
|
|
||
| /* finally: */ | ||
| ADDOP_O(c, LOAD_CONST, Py_None, consts); |
There was a problem hiding this comment.
Seems this is a dead code.
serhiy-storchaka
left a comment
There was a problem hiding this comment.
Published the part of review comments since the PR is changed.
Objects/frameobject.c
Outdated
| * 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.
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.
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
| /* 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.
RERAISE instead of RETRY.
Objects/frameobject.c
Outdated
| in_finally[blockstack_top-1] = 1; | ||
| } | ||
| else { | ||
| blockstack_top--; |
There was a problem hiding this comment.
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
| break; | ||
|
|
||
| case END_FINALLY: | ||
| /* Ignore END_FINALLYs for SETUP_EXCEPTs - they exist |
There was a problem hiding this comment.
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.
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.
I've now removed all code duplication.
There was a problem hiding this comment.
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_BLOCKs may be larger than the number of SETUP_EXCEPTs and SETUP_FINALLYs. 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.
I see. Would you like to suggest another approach?
There was a problem hiding this comment.
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).
serhiy-storchaka
left a comment
There was a problem hiding this comment.
I don't understand why JUMP_FINALLY should be a jumping instruction.
Python/ceval.c
Outdated
| FAST_DISPATCH(); | ||
| } | ||
|
|
||
| TARGET(JUMP_FINALLY) { |
Python/ceval.c
Outdated
| PUSH(exc); | ||
| PUSH(exc); | ||
| PUSH(exc); | ||
| JUMPBY(oparg); |
There was a problem hiding this comment.
Is it needed? It seems to me that JUMP_FINALLY always jumps to the next instruction.
There was a problem hiding this comment.
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.
Use sizeof(_Py_CODEUNIT) instead of 2.
There was a problem hiding this comment.
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.
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.
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.
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.
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.
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_BLOCKs may be larger than the number of SETUP_EXCEPTs and SETUP_FINALLYs. 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.
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