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