Skip to content
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

Closed
wants to merge 20 commits into from

Conversation

pitrou
Copy link
Member

@pitrou pitrou commented Jul 23, 2017

@pitrou pitrou added the type-feature A feature request or enhancement label Jul 23, 2017
@mention-bot
Copy link

@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.

@serhiy-storchaka serhiy-storchaka self-requested a review July 23, 2017 19:27
@pitrou
Copy link
Member Author

pitrou commented Jul 23, 2017

The test_dis failures are expected, as I haven't updated that file yet.

@pitrou
Copy link
Member Author

pitrou commented Jul 23, 2017

However, I don't understand the additional test failures under Windows.

@pitrou
Copy link
Member Author

pitrou commented Jul 23, 2017

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);
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, removed.

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a 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.

@@ -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
Copy link
Member

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
Copy link
Member

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.

@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RERAISE instead of RETRY.

/* 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--;
Copy link
Member

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

in_finally[blockstack_top-1] = 1;
}
else {
blockstack_top--;
}
break;

case END_FINALLY:
/* Ignore END_FINALLYs for SETUP_EXCEPTs - they exist
Copy link
Member

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:
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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_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.

Copy link
Member Author

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?

Copy link
Member

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).

Copy link
Member

@serhiy-storchaka serhiy-storchaka left a 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) {
Copy link
Member

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?

Copy link
Member Author

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);
Copy link
Member

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.

Copy link
Member Author

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;
Copy link
Member

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.

Copy link
Member Author

@pitrou pitrou Jul 29, 2017

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];
Copy link
Member

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;
Copy link
Member

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)).

Copy link
Member Author

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.

Copy link
Member

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:
Copy link
Member

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_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.

@pitrou
Copy link
Member Author

pitrou commented Jul 29, 2017

Serhiy, do you want to take over this PR? I am not much motivated by the remaining cleanup / dealing with existing cruft.

@serhiy-storchaka
Copy link
Member

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.

@pitrou
Copy link
Member Author

pitrou commented Jul 31, 2017

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 };

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?

@nascheme
Copy link
Member

nascheme commented Nov 6, 2017

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:

  • use exc_info->exc_* instead of tstate->exc_*
  • remove the block under fast_yield that calls restore_and_clear_exc_state() or swap_exc_state()

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.

@pitrou
Copy link
Member Author

pitrou commented Dec 28, 2017

Closing this PR as outdated. See PR #4682 and PR #5006 for more recent implementations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting core review type-feature A feature request or enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants