You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Certain sys.monitoring "not taken" branches in a for loop not showing correctly #123050
With the code from PR 122564, the destinations of the "not taken" branches out of lines 2 and 3 show as going out of scope, rather than the correct line 6. Additionally, the ("taken") branch into if's block also shows as going out of scope:
Conceptually the this is a single instruction: POP_JUMP_IF_TRUE_BACKWARD L1.
We only have one backward jump, JUMP_BACKWARD, so the assembler converts POP_JUMP_IF_TRUE_BACKWARD label
to
POP_JUMP_IF_TRUE over
JUMP_BACKWARD label
over:
We need to account for NOT_TAKEN here (which probably means emitting NOT_TAKEN in the assembler).
Even then, the location of the instruction following NOT_TAKEN is likely not the location of label, but that of POP_JUMP_IF_TRUE.
We don't want to prevent the compiler from generating efficient code, so we needs a means to handle these complex cases, so we'll need to handle this sys.monitoring somehow.
Since this isn't bug until we add the new branch events, I'm marking this as a feature request.
I don't know why the NOP at label L2 is left in, rather than being merged into the following line, but that doesn't make the generated code wrong.
The branch from 22 to L2 (32) jumps to an instruction with no line number, but execution will immediately proceed to 34 (line 4).
The "not taken" branch can likewise be followed from 26 to 28 and back to L1 (offset 6) on line 2.
In other words the "taken" branch goes from line 3 to 4 and the "not taken" branch goes from line 3 to line 2.
which I think we previously said means an exit from the function?
Anyway, I have been increasingly thinking that we may be asking for too much... we could maybe just report on the number of branches taken/existing per line, and give up on filtering out clause branches. @nedbat, what do you think?
This is, as before, using co_positions to map offsets back to source code.
All branches but the 3rd are fairly clear. The 3rd branch, from 24 to 30, is really 4->3... there's no else, so we go back to the next for iteration (if any). Shouldn't the JUMP_BACKWARD in offset 30 really map to somewhere in line 3?
Quite possibly, but I don't see them... If you are referring to the tuples showing the branches, that is simply list(code.co_branches())...
I think we have the information we need. Coverage.py now follows trails of instructions until a new line is reached.
I tried that method for finding out the branch destinations for SlipCover (years ago) and found it unreliable... Essentially, I'd:
read the bytecode
for each conditional jump found, the branch source is the line that bytecode is on
to find the branch destination, track where the bytecode goes, following unconditional jumps
When do we stop tracking the bytecode, though? Stopping when you reach a new line works well for things like skipping over a for's variable initialization and into its block. But it misses same-line cases like
ifx: f()
Also, while tracking, what do we do about conditional jump opcodes? Do we stop there? Do we ignore them? Or do we track both branches and extract the destination somehow from the (potentially various) resulting paths?
I think ideally we wouldn't trace the code at all, but instead rely on a given mapping (co_positions or something else, using the branch destination offset) to tell us where in the source code that branch goes to.
As @nedbat shared, he coded a mechanism that enumerates branches and deduces their destination by stepping through bytecode. I understand that he is satisfied that it works well enough for Coverage.py, and that's great! Seriously.
I just don't think coding something like that should be necessary to fully utilize sys.monitoring's branch feature. Most users only ever look at Python source code; the bytecode is an implementation detail. In my mind, it follows that there should be an obvious, reliable way to map from branch offsets to the source code that originated it.
I don't know if using co_locations should be that; it was designed to allow showing error messages. Perhaps it would be better to simply indicate the source code origin and destination for each branch along with the offsets in co_branches—or something else entirely. But if we don't provide something, the next tool author wanting to use sys.monitoring's branches will likely have to code around the feature like Ned did.
Activity
markshannon commentedon Aug 20, 2024
There's a few things going on here.
First:
The jump in
if v > 0:is miscompiled toConceptually the this is a single instruction:
POP_JUMP_IF_TRUE_BACKWARD L1.We only have one backward jump,
JUMP_BACKWARD, so the assembler convertsPOP_JUMP_IF_TRUE_BACKWARD labelto
We need to account for
NOT_TAKENhere (which probably means emittingNOT_TAKENin the assembler).Even then, the location of the instruction following
NOT_TAKENis likely not the location oflabel, but that ofPOP_JUMP_IF_TRUE.We don't want to prevent the compiler from generating efficient code, so we needs a means to handle these complex cases, so we'll need to handle this
sys.monitoringsomehow.Since this isn't bug until we add the new branch events, I'm marking this as a feature request.
jaltmayerpizzorno commentedon Sep 16, 2024
Updating the disassembly for the current code in pull/122564; the branches around the
ifare still unclear.markshannon commentedon Dec 31, 2024
Latest disassembly (with offsets) on main:
I don't know why the
NOPat labelL2is left in, rather than being merged into the following line, but that doesn't make the generated code wrong.The branch from 22 to L2 (32) jumps to an instruction with no line number, but execution will immediately proceed to 34 (line 4).
The "not taken" branch can likewise be followed from 26 to 28 and back to L1 (offset 6) on line 2.
In other words the "taken" branch goes from line 3 to 4 and the "not taken" branch goes from line 3 to line 2.
markshannon commentedon Jan 7, 2025
On latest main the disassembly now looks like this:
And the branches:
which, to me, looks correct.
@jaltmayerpizzorno does this work for you?
jaltmayerpizzorno commentedon Jan 19, 2025
Hi @markshannon, thank you. That 20->26 branch maps the same location in source and destination,
which I think we previously said means an exit from the function?
Anyway, I have been increasingly thinking that we may be asking for too much... we could maybe just report on the number of branches taken/existing per line, and give up on filtering out clause branches. @nedbat, what do you think?
jaltmayerpizzorno commentedon Jan 19, 2025
BTW, I missed initializing
xin the example:If we continue on this, let's please use this version instead.
nedbat commentedon Jan 20, 2025
I don't know what this means. It feels like we are close to getting the data we want.
jaltmayerpizzorno commentedon Jan 22, 2025
I mean filtering out the branches such as the one between the
AandBinif A and B:.markshannon commentedon Feb 27, 2025
@nedbat @jaltmayerpizzorno
Can I close this, or is there more to do?
jaltmayerpizzorno commentedon Mar 2, 2025
I still am getting a branch whose destination maps back to the same portion of code... Using the code in 'main' (c6513f7), I get:
This is, as before, using
co_positionsto map offsets back to source code.All branches but the 3rd are fairly clear. The 3rd branch, from 24 to 30, is really 4->3... there's no
else, so we go back to the nextforiteration (if any). Shouldn't theJUMP_BACKWARDin offset 30 really map to somewhere in line 3?nedbat commentedon Mar 3, 2025
@jaltmayerpizzorno are there some typos in your description?
I think we have the information we need. Coverage.py now follows trails of instructions until a new line is reached.
Here is what I see for the disassembly, and how the latest coverage.py code interprets it:
The possible branch points are instructions with jump targets that aren't unconditional jumps: offsets 10 and 24.
Final result is line 3 branches to either 4 or 7, and line 4 branches to 3 or 5.
jaltmayerpizzorno commentedon Mar 3, 2025
Quite possibly, but I don't see them... If you are referring to the tuples showing the branches, that is simply
list(code.co_branches())...I tried that method for finding out the branch destinations for SlipCover (years ago) and found it unreliable... Essentially, I'd:
When do we stop tracking the bytecode, though? Stopping when you reach a new line works well for things like skipping over a
for's variable initialization and into its block. But it misses same-line cases likeAlso, while tracking, what do we do about conditional jump opcodes? Do we stop there? Do we ignore them? Or do we track both branches and extract the destination somehow from the (potentially various) resulting paths?
I think ideally we wouldn't trace the code at all, but instead rely on a given mapping (
co_positionsor something else, using the branch destination offset) to tell us where in the source code that branch goes to.jaltmayerpizzorno commentedon Mar 7, 2025
I thought I should clarify what I'm saying.
As @nedbat shared, he coded a mechanism that enumerates branches and deduces their destination by stepping through bytecode. I understand that he is satisfied that it works well enough for Coverage.py, and that's great! Seriously.
I just don't think coding something like that should be necessary to fully utilize
sys.monitoring's branch feature. Most users only ever look at Python source code; the bytecode is an implementation detail. In my mind, it follows that there should be an obvious, reliable way to map from branch offsets to the source code that originated it.I don't know if using
co_locationsshould be that; it was designed to allow showing error messages. Perhaps it would be better to simply indicate the source code origin and destination for each branch along with the offsets inco_branches—or something else entirely. But if we don't provide something, the next tool author wanting to usesys.monitoring's branches will likely have to code around the feature like Ned did.