MatthieuDartiailh / bytecode

Python module to modify bytecode
https://bytecode.readthedocs.io/
MIT License
302 stars 38 forks source link

Stack depth computation based on cfg analysis #8

Closed MatthieuDartiailh closed 7 years ago

MatthieuDartiailh commented 8 years ago

This is a new version of the stack depth computation based on CPython implementation using the control flow graph of the code. It has several advantages compared to the previous implementation:

I made stack_effect a property of Instr and added some argument analysis as dis.stack_effect is quite picky on the argument even if it is not involved in the computation. As a side node, adding a Python 2.7 implementation of stack effect will be, I believe, the only modification required to support Python 2.7

MatthieuDartiailh commented 8 years ago

Note that the extended jump test if failing due to an issue in building the CFG (some label is missing).

MatthieuDartiailh commented 8 years ago

I spent some time looking at the cfg of the failing test and there is one thing that can explain the issue. Looking at the cfg there are two dead blocks that will never be traversed. Those two blocks are 12 and 15 whose neat stack effect is +1 (what we are missing) . They are skipped because 12 follows a RETURN_VALUE. I must say this is quite puzzling to me. Looking a the bytecode the issue is in the last two instructions below label 35 are skipped (POP_EXCEPT, JUMP_FORWARD and the block jumped to label 52). Actually this happens for any except close with a return value but usually does not cause any problem. My guess is that interpreter is dealing with try block in a special fashion. Could you give this a look ?

MatthieuDartiailh commented 8 years ago

Looking at the python compiler (https://hg.python.org/cpython/file/tip/Python/compile.c#l2516), it does appear that some instructions are appended in some cases when a block is a TRY_FINALLY block no matter what appears before. So maybe we need to rework the cfg building to distinguish between the different kinds of blocks, and apply different rules for different block for when to move to the next block.

MatthieuDartiailh commented 8 years ago

@haypo what do you think about adding an attribute to BasicBlock (or creating subclasses) for LOOP, EXCEPT, FINALLY_TRY and FINALLY_END blocks to better reproduce Python CFG. I may have time to work a bit on it but not much. It is, I think, the one thing that would allow to get the stack computation to work properly in all cases.

vstinner commented 8 years ago

@haypo what do you think about adding an attribute to BasicBlock (or creating subclasses) for LOOP, EXCEPT, FINALLY_TRY and FINALLY_END blocks to better reproduce Python CFG.

Sorry, which kind of attribute?

BasicBlock is mutable, so it should be a method or a property, not an attribute.

MatthieuDartiailh commented 8 years ago

A property would indeed make more sense on BasicBlock

MatthieuDartiailh commented 8 years ago

@haypo I would like some advice on the CFG building. I spent quite some time reading compile.c in the python trying to understand how the block stack works for LOOP, EXCEPT, FINALLY_TRY and FINALLY_END blocks, and I must say that some things do not make sense to me. For example this function:

def t(a):
    try:
         return 1
         a += 1
    finally:
         return a

Produces the following code (from dis) (by the way do you know is dis 'blocks' maps to real blocks ? I am assuming yes but I am not sure):

  2           0 SETUP_FINALLY           18 (to 21)

  3           3 LOAD_CONST               1 (1)
              6 RETURN_VALUE

  4           7 LOAD_FAST                0 (a)
             10 LOAD_CONST               1 (1)
             13 INPLACE_ADD
             14 STORE_FAST               0 (a)
             17 POP_BLOCK
             18 LOAD_CONST               0 (None)

  6     >>   21 LOAD_FAST                0 (a)
             24 RETURN_VALUE
             25 END_FINALLY

When called it with 1 as argument it returns 1 as expected which means that a was not incremented. My issue is that the POP_BLOCK and following LOAD_CONST introduce the finally clause and should always be executed, however here they appear in a dead block ! Doing the same analysis without 'a += 1' shows the POP_BLOCK belonging to the same block as the RETURN_VALUE. Writing the same code with two returns inside the try block optimizes out the second and leaves the POP_BLOCK in its own block but I cannot know if it is considered to be the next block of the return or not. I know this function does not make much sense but it basically describe the issue we have in stack depth computation with nested try except else finally. Based on the above remarks, I am not sure how to reliably construct a round tripable CFG giving us the right stack depth.

PS : adding a property to block does not really make sense as the block type is used only internally.

vstinner commented 8 years ago

Sadly, yes, CPython emits regulary dead code :-/ It's obvious that the "a += 1" basic clock has no entry point and so is dead code.

MatthieuDartiailh commented 8 years ago

Ok but what happens of the POP_BLOCK then ? is it also dead code ?

vstinner commented 8 years ago
  2           0 SETUP_FINALLY           18 (to 21)
  3           3 LOAD_CONST               1 (1)
              6 RETURN_VALUE

  4           7 LOAD_FAST                0 (a)   # DEAD CODE
             10 LOAD_CONST               1 (1)   # DEAD CODE
             13 INPLACE_ADD   # DEAD CODE
             14 STORE_FAST               0 (a)   # DEAD CODE
             17 POP_BLOCK   # DEAD CODE
             18 LOAD_CONST               0 (None)   # DEAD CODE

  6     >>   21 LOAD_FAST                0 (a)
             24 RETURN_VALUE
             25 END_FINALLY   # DEAD CODE

In fact, only the following code is executed...

  2           0 SETUP_FINALLY           18 (to 21)
  3           3 LOAD_CONST               1 (1)
              6 RETURN_VALUE
  6     >>   21 LOAD_FAST                0 (a)
             24 RETURN_VALUE

By the way, it's not easy that it can be simplified even more...

  6     >>   21 LOAD_FAST                0 (a)
             24 RETURN_VALUE

Python bytecode compiler is kind of inefficient...

MatthieuDartiailh commented 8 years ago

Ok thanks for the clarification ! I looked briefly at ceval.c and though it may indeed be so (but it would have taken me quite some time to be sure). This means I was looking in the wrong direction when trying to understand the issue in the stackdepth calculation. I will revert the recent (and ugly) changes to the CFG building and keep looking.

MatthieuDartiailh commented 8 years ago

@haypo I think this time everything is good to go. It turns out that the stackdepth issue is related to the fact that CPython emits a lot of dead code and go through it when doing the stack depth calculation. I made the test so that all branches can be tested and I check that there is no issue there. I also updated the docs. Hopefully I have not forgotten anything.

MatthieuDartiailh commented 8 years ago

ping @haypo this is ready for review (all the docs should be up to date).

MatthieuDartiailh commented 8 years ago

@haypo I think I addressed most of your comments. Some comments:

MatthieuDartiailh commented 7 years ago

ping @haypo I addressed all the comments I know how to address. For the two tests you are not fully happy with, I am open to suggestions. I justified my approach for stack_effect in my previous message and I do believe there is no need to change the current implementation.

MatthieuDartiailh commented 7 years ago

ping @haypo

vstinner commented 7 years ago

@MatthieuDartiailh: I sent you an invitation to become a contributor to the project. So you will be directly able to merge your pull request ;-)

MatthieuDartiailh commented 7 years ago

Thanks a lot for this ! I will double check everything and try to figure out the values for the LOAD_CONST+RETURN_VALUE. Then I will merge.

MatthieuDartiailh commented 7 years ago

I completed the partial test in test_concrete. Before I merge, could you just let me know if you agree with my argument for stack_effect ?

MatthieuDartiailh commented 7 years ago

Also to keep the history clean I will squash when merging.

codecov-io commented 7 years ago

Current coverage is 92.94% (diff: 99.20%)

Merging #8 into master will increase coverage by 0.36%

@@             master         #8   diff @@
==========================================
  Files            15         15          
  Lines          2265       2382   +117   
  Methods           0          0          
  Messages          0          0          
  Branches          0          0          
==========================================
+ Hits           2097       2214   +117   
  Misses          168        168          
  Partials          0          0          

Powered by Codecov. Last update 00fb1dc...0cd2503

MatthieuDartiailh commented 7 years ago

I am finally happy with the state of the tests. I will merge tomorrow save if you have something else to add.