Coveragepy: python3.8: unable to detect `break` in while loop as covered

Created on 7 Feb 2019  路  1Comment  路  Source: nedbat/coveragepy

Describe the bug
python has changed how loops work in python3.8. there are no longer explicit opcodes for break / continue / etc. Some of these seem handled in coverage but others are not.

To Reproduce

def f(start):
    end = 3
    while start < end:
        if start == 1:
            start += 1
            continue
        else:
            break  # not covered in py38 :'(

f(1)
f(3)
$ coverage erase && coverage run --branch t.py && coverage report --include t.py --show-missing
Name    Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------
t.py        9      1      4      1    85%   4->8, 8
$ python --version --version
Python 3.8.0a1+ (heads/master:e9bc4172d1, Feb  6 2019, 17:49:51) 
[GCC 7.3.0]

Expected behavior

This is what happens in python3.7:

$ coverage erase && coverage run --branch t.py && coverage report --include t.py
Name    Stmts   Miss Branch BrPart  Cover
-----------------------------------------
t.py        9      0      4      0   100%
$ python3.7 --version --version
Python 3.7.2 (default, Dec 25 2018, 03:50:46) 
[GCC 7.3.0]

Additional context
https://bugs.python.org/issue17611 appears to be the bpo that changed this behaviour

I've verified that the break is covered by adding a print(0) above it, however this causes the break to magically become covered. It only reproduces when there's nothing else in that block.

Here's the disassembly from python3.7:

Disassembly of <code object f at 0x7f4c8460b300, file "t.py", line 1>:
  2           0 LOAD_CONST               1 (3)
              2 STORE_FAST               1 (end)

  3           4 SETUP_LOOP              34 (to 40)
        >>    6 LOAD_FAST                0 (start)
              8 LOAD_FAST                1 (end)
             10 COMPARE_OP               0 (<)
             12 POP_JUMP_IF_FALSE       38

  4          14 LOAD_FAST                0 (start)
             16 LOAD_CONST               2 (1)
             18 COMPARE_OP               2 (==)
             20 POP_JUMP_IF_FALSE       34

  5          22 LOAD_FAST                0 (start)
             24 LOAD_CONST               2 (1)
             26 INPLACE_ADD
             28 STORE_FAST               0 (start)

  6          30 JUMP_ABSOLUTE            6
             32 JUMP_ABSOLUTE            6

  8     >>   34 BREAK_LOOP
             36 JUMP_ABSOLUTE            6
        >>   38 POP_BLOCK
        >>   40 LOAD_CONST               0 (None)
             42 RETURN_VALUE

Here's the disassembly from python3.8

Disassembly of <code object f at 0x7f97cde9eae0, file "t.py", line 1>:
  2           0 LOAD_CONST               1 (3)
              2 STORE_FAST               1 (end)

  3     >>    4 LOAD_FAST                0 (start)
              6 LOAD_FAST                1 (end)
              8 COMPARE_OP               0 (<)
             10 POP_JUMP_IF_FALSE       36

  4          12 LOAD_FAST                0 (start)
             14 LOAD_CONST               2 (1)
             16 COMPARE_OP               2 (==)
             18 POP_JUMP_IF_FALSE       36

  5          20 LOAD_FAST                0 (start)
             22 LOAD_CONST               2 (1)
             24 INPLACE_ADD
             26 STORE_FAST               0 (start)

  6          28 JUMP_ABSOLUTE            4
             30 JUMP_ABSOLUTE            4

  8          32 JUMP_ABSOLUTE           36
             34 JUMP_ABSOLUTE            4
        >>   36 LOAD_CONST               0 (None)
             38 RETURN_VALUE

The main difference here being line 8 which goes from BREAK_LOOP (3.7) to JUMP_ABSOLUTE (3.8)

bug not-our-bug

Most helpful comment

The problem here isn't that the opcodes have changed, but that the peephole optimizer has become cleverer. If you look at the bytecode, you see that if start != 1 on line 4, then the jump is to bytecode 36, not bytecode 32. That is, the optimizer recognizes that the if statement could go to a break, which itself goes to the return, so just make the if go straight to the return.

This is another instance of the problem first reported over 10 years ago in bpo 2506. The debate there ended long ago, but needs someone to implement a way to disable the peephole optimizer for times when you are running code for analysis rather than performance. Perhaps your voice will help?

>All comments

The problem here isn't that the opcodes have changed, but that the peephole optimizer has become cleverer. If you look at the bytecode, you see that if start != 1 on line 4, then the jump is to bytecode 36, not bytecode 32. That is, the optimizer recognizes that the if statement could go to a break, which itself goes to the return, so just make the if go straight to the return.

This is another instance of the problem first reported over 10 years ago in bpo 2506. The debate there ended long ago, but needs someone to implement a way to disable the peephole optimizer for times when you are running code for analysis rather than performance. Perhaps your voice will help?

Was this page helpful?
0 / 5 - 0 ratings