Update opcode, dis from 3.14.2 by youknowone · Pull Request #6869 · RustPython/RustPython
🏁 Script executed:
# First, let's look at the file structure and the specific lines mentioned cd crates/codegen/src && wc -l ir.rs
Repository: RustPython/RustPython
Length of output: 75
🏁 Script executed:
# Examine the code at lines 226-229 sed -n '220,235p' ir.rs
Repository: RustPython/RustPython
Length of output: 113
🏁 Script executed:
# Check the other locations mentioned: 335-345, 360-368, 850-901 echo "=== Lines 335-345 ===" sed -n '330,350p' ir.rs echo "=== Lines 360-368 ===" sed -n '355,373p' ir.rs echo "=== Lines 850-901 ===" sed -n '845,905p' ir.rs
Repository: RustPython/RustPython
Length of output: 294
🏁 Script executed:
# Search for lineno_override and related structures to understand the context rg -B5 -A5 "lineno_override" ir.rs
Repository: RustPython/RustPython
Length of output: 110
🏁 Script executed:
# Search for LineTableLocation struct definition rg "struct LineTableLocation" ir.rs -A10
Repository: RustPython/RustPython
Length of output: 110
🌐 Web query:
CPython 3.13 3.14 line-0 RESUME instruction linetable encoding
💡 Result:
In CPython 3.13 and 3.14, the source-location info for each bytecode instruction (including the initial RESUME) is stored in code.co_linetable as a stream of location entries. Each entry starts with a header byte 1 cccc lll (MSB set), where cccc is the location kind (0–15) and lll is (length_in_code_units - 1); “code units” are 2-byte units (i.e., one wordcode instruction is length 1). The rest of the entry is kind-specific, using 6-bit-chunk varint/svarint encodings. [1][2]
Why RESUME can show line 0
The start line stored in the first entry is encoded as a delta from co_firstlineno (and subsequent entries delta from the previous start line). So if a code object has co_firstlineno == 0, then RESUME can legitimately decode to line 0 via a delta of 0. [1][2]
(Separately, PEP 626 notes that frames for modules start with f_lineno = 0, and that some bytecodes may be “artificial” and have no meaningful line. Tools should prefer co_lines() / co_positions() rather than assuming the first executable line is co_firstlineno + 1. [3])
The actual encoding you’ll typically see for “line 0 RESUME”
Most commonly, RESUME’s entry uses Kind 13 (“No column info”): it stores start/end line deltas but sets columns to None. [1][2]
For a single-instruction RESUME whose decoded line is 0 (i.e., first entry, start-line delta = 0, end-line delta = 0):
- header byte: kind=13, length=1 →
1 1101 000=0xE8 - start-line delta (svarint 0 → varint 0) →
0x00 - end-line delta (Δ from start line, 0) →
0x00
So the entry bytes are:
E8 00 00 (hex) [1][2]
If instead CPython marks that instruction range as having no location at all (Kind 15 “No location”), then for length=1 the entry is just:
F8 (hex) [1][2]
Note on “RESUME has the def-line number”
For functions, co_firstlineno is the line with the def statement, so the first entry’s delta-0 convention makes RESUME map to the function definition line, which is a behavior change that has affected some tools. [2][4]
Sources: CPython 3.13 Objects/locations.md (format reference) [1]; CPython 3.14 InternalDocs/code_objects.md (same format, moved docs) [2]; PEP 626 (line-number model, module line 0, “artificial” bytecodes) [3]; python.org discussion about RESUME line numbering changes [4].
🌐 Web query:
CPython linetable format line 0 no-line instructions bytecode
💡 Result:
In modern CPython (3.10+), line/position info is no longer “every instruction has a real source line”. The bytecode→source mapping is stored in the code object’s line table (co_linetable) and exposed via code.co_lines(), which can yield ranges whose line is None (“no line number”) for artificial / bookkeeping bytecode. This is explicitly part of the PEP 626 model: some bytecodes “must be marked as artificial, and not have a meaningful line number”, and the iterator’s line can be None. [1]
Internally, when CPython decodes the line table, it represents “no line number” as a negative line (range.line < 0). [1] In the older (3.10-era) internal notes for the delta-encoded line table, there’s also an explicit sentinel for “no line number” (a special line-delta value). [2]
So if you’re seeing line 0 attached to certain instructions, that’s almost always a tooling/formatting artifact where “no line” (None / <0) got coerced to 0 for display or storage. Typical examples of bytecode that may have “no-line” (or otherwise surprising) locations are interpreter/setup opcodes like RESUME (and related prologue opcodes around generators/coroutines), which has been discussed as a behavior change users notice when enumerating covered lines. [3]
Practical guidance:
- Treat “line 0” as “no line info”, not a real source line.
- Prefer
code.co_lines()(line can beNone) per PEP 626, rather than reverse-engineeringco_linetable. [1] - If you need per-instruction positions, use
dis.get_instructions()/dis.dis()(these are built to follow the current semantics). [4]
Sources:
[1] PEP 626 (Precise line numbers; co_lines() yields line or None, and internal decode treats <0 as no-line) https://peps.python.org/pep-0626/
[2] CPython Objects/lnotab_notes.txt (sentinel encoding for “no line number”) https://chromium.googlesource.com/external/github.com/python/cpython/%2B/refs/tags/v3.10.0b3/Objects/lnotab_notes.txt
[3] Discussion of RESUME line-number behavior in 3.11 https://discuss.python.org/t/the-resume-opcode-has-the-same-line-number-as-the-function-definition-and-breaks-previous-behaviour/21972
[4] dis module docs (3.11+) https://docs.python.org/3.11/library/dis.html
🏁 Script executed:
git ls-files | grep ir.rsRepository: RustPython/RustPython
Length of output: 89
🏁 Script executed:
# Once we have the path, read the file at specified lines git ls-files | grep ir.rs | head -1 | xargs -I {} sh -c 'echo "Lines 220-235:"; sed -n "220,235p" {}; echo -e "\nLines 330-350:"; sed -n "330,350p" {}; echo -e "\nLines 355-373:"; sed -n "355,373p" {}; echo -e "\nLines 845-905:"; sed -n "845,905p" {}'
Repository: RustPython/RustPython
Length of output: 4652
🏁 Script executed:
# Search for all LineTableLocation creations in the file rg "LineTableLocation" crates/codegen/src/ir.rs -B3 -A3
Repository: RustPython/RustPython
Length of output: 1069
🏁 Script executed:
# Look for the struct definition rg "struct LineTableLocation" crates/codegen/src/ -A5
Repository: RustPython/RustPython
Length of output: 309
🏁 Script executed:
# Search for lineno_override usage to understand the full context rg "lineno_override" crates/codegen/src/ir.rs -B10 -A5
Repository: RustPython/RustPython
Length of output: 1512
🏁 Script executed:
# Check lines 360-368 more carefully sed -n '355,375p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 696
🏁 Script executed:
# Check if there are other LineTableLocation creations rg "LineTableLocation {" crates/codegen/src/ir.rs -B1 -A5
Repository: RustPython/RustPython
Length of output: 166
🏁 Script executed:
# Verify the generate_linetable function handles these values sed -n '850,920p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 2870
🏁 Script executed:
# Search for all LineTableLocation creations without regex special chars grep -n "let lt_loc = LineTableLocation" crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 121
🏁 Script executed:
# Also search for direct LineTableLocation struct instantiations grep -n "LineTableLocation {" crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 151
🏁 Script executed:
# Let's see what's at lines 360-368 to understand "Also applies to" sed -n '355,375p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 696
🏁 Script executed:
# Check if there are multiple code blocks creating LineTableLocation with same pattern sed -n '300,330p' crates/codegen/src/ir.rs sed -n '340,370p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 2783
🏁 Script executed:
# Let me verify there's only one LineTableLocation creation by searching more carefully grep -n "LineTableLocation {" crates/codegen/src/ir.rs -A6
Repository: RustPython/RustPython
Length of output: 677
🏁 Script executed:
# Check if the code at 226-229 is actually the right location by looking at context sed -n '220,245p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 1066
🏁 Script executed:
# Verify the lineno_override handling one more time sed -n '330,350p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 1302
🏁 Script executed:
# Count total LineTableLocation creations grep -c "let lt_loc = LineTableLocation" crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 66
🏁 Script executed:
# Verify the line numbers in the "Also applies to" section - check 360-368 sed -n '360,375p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 583
🏁 Script executed:
# Let me check around line 850-901 to understand what that refers to sed -n '848,855p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 310
🏁 Script executed:
# Verify the issue: when lineno_override = Some(0), what happens in generate_linetable? # Check if line_delta could be 0 but end_line_delta could be non-zero sed -n '870,920p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 2265
🏁 Script executed:
# Let me understand the "Also applies to" references more carefully # Check what's at the line ranges mentioned echo "=== Checking 335-345 range ===" sed -n '335,345p' crates/codegen/src/ir.rs echo "=== Checking 360-368 range ===" sed -n '360,368p' crates/codegen/src/ir.rs echo "=== Checking 850-901 context - is this generate_linetable? ===" sed -n '845,860p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 1621
🏁 Script executed:
# Verify the fix would work correctly by checking generate_linetable logic # When col=0, end_col=0, what happens? sed -n '895,950p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 3063
🏁 Script executed:
# Let me check the actual line-0 RESUME context by searching for where lineno_override is set rg "lineno_override\s*=" crates/codegen/src/ -B2 -A2
Repository: RustPython/RustPython
Length of output: 392
🏁 Script executed:
# Verify the concern about end_line_delta when lineno_override=Some(0) # If lineno_override=Some(0) but end_location.line is non-zero, end_line_delta would be non-zero # This would not fall into the "line_delta == 0 && end_line_delta == 0" path # Let's check what happens in the else path sed -n '910,950p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 2306
🏁 Script executed:
# Let me check the complete flow for line-0 case # First, verify module scope sets lineno_override sed -n '1,50p' crates/codegen/src/compile.rs | grep -A5 "Module scope"
Repository: RustPython/RustPython
Length of output: 47
🏁 Script executed:
# Check the line-0 flow: what happens when prev_line=first_line and line=0 sed -n '930,960p' crates/codegen/src/ir.rs
Repository: RustPython/RustPython
Length of output: 1703
🏁 Script executed:
# Let me verify one more time that the fix would be appropriate by checking # if there are tests or comments about line-0 RESUME semantics rg "RESUME\|line.*0\|line-0" crates/codegen/src/ir.rs | head -20
Repository: RustPython/RustPython
Length of output: 47
Override end_line and columns when lineno_override is set to 0.
When lineno_override = Some(0) is set for module-scope RESUME, the code currently overrides only line to 0 while keeping end_line and columns from the actual source location. Per CPython 3.13+ semantics, line 0 represents "no line" (artificial instructions), and such entries should have no meaningful column information. The current code creates a 0→first_line span with real columns, which violates CPython's linetable encoding (Kind 13 "NoColumns" expects no column data for line-0 entries).