Skip to content

PyCode_Addr2Line and PyFrame_GetLineNumber return -1 #111519

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
notEvil opened this issue Oct 30, 2023 · 10 comments
Closed

PyCode_Addr2Line and PyFrame_GetLineNumber return -1 #111519

notEvil opened this issue Oct 30, 2023 · 10 comments
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error

Comments

@notEvil
Copy link

notEvil commented Oct 30, 2023

Bug report

Bug description:

Hi,

while testing line_profiler on 3.12 I saw PyCode_Addr2Line and PyFrame_GetLineNumber return -1. The following minimal example reproduces the issue for me:

line.py
import pyximport

pyximport.install()

import _line
import asyncio


async def f():
    pass


print(_line.lines(f))


async def function_1(bool):
    if bool:
        async for _ in function_2():
            pass


async def function_2():
    if False:
        yield


_line.start()
asyncio.run(function_1(True))
_line.stop()
_line.pyx
cdef extern from "Python.h":
    ctypedef struct PyObject
    ctypedef struct PyCodeObject
    ctypedef struct PyFrameObject
    ctypedef int (*Py_tracefunc)(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg)
    cdef int PyTrace_CALL
    cdef int PyTrace_LINE
    cdef int PyTrace_RETURN
    cdef int PyCode_Addr2Line(PyCodeObject *co, int byte_offset)
    cdef void PyEval_SetTrace(Py_tracefunc func, PyObject* obj)
    cdef int PyFrame_GetLineNumber(PyFrameObject* frame)


def lines(function):
    code = function.__code__
    return [PyCode_Addr2Line(<PyCodeObject*>code, byte_offset) for byte_offset in range(0, len(code.co_code), 2)]


def start():
    PyEval_SetTrace(trace, NULL)


def stop():
    PyEval_SetTrace(NULL, NULL)


cdef int trace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) noexcept:
    print(f"{PyFrame_GetLineNumber(frame):3}", {PyTrace_CALL: "call", PyTrace_LINE: "line", PyTrace_RETURN: "return"}.get(what, "other"))

On 3.11 it prints

[9, 9, 9, 10, 10]
 28 line
160 call
184 line
189 line
...
691 return
 63 return
189 return
 29 line

and on 3.12

[9, -1, 9, 10, -1, -1]
 28 line
160 call
188 line
193 line
...
 -1 return
...
702 return
 62 return
193 return
 29 line

CPython versions tested on:

3.11, 3.12

Operating systems tested on:

Linux

@notEvil notEvil added the type-bug An unexpected behavior, bug, or error label Oct 30, 2023
@gaogaotiantian
Copy link
Member

Are you able to reproduce this without Cython? Or it only happens on C level tracing function? What about a minimal tracer with C-extension?

@notEvil
Copy link
Author

notEvil commented Oct 30, 2023

Are you able to reproduce this without Cython?

yes

What about a minimal tracer with C-extension?

I've never created a C extension before, so don't expect this to be pretty.

#include <Python.h>


static PyObject* lines(PyObject* self, PyObject* args) {
  PyObject* function = PyTuple_GetItem(args, 0);
  PyObject* code = PyFunction_GetCode(function);
  PyObject* co_code = PyCode_GetCode(code);
  printf("[");
  for (int byte_offset=0; byte_offset < PyObject_Length(co_code); byte_offset += 2) {
    printf("%i, ", PyCode_Addr2Line(code, byte_offset));
  }
  printf("]\n");
  Py_DECREF(co_code);
  return Py_None;
}


static int trace(PyObject* obj, PyFrameObject* frame, int what, PyObject* arg) {
  printf("%i %s\n", PyFrame_GetLineNumber(frame), what == PyTrace_CALL ? "call" : (what == PyTrace_LINE ? "line" : (what == PyTrace_RETURN ? "return" : "other")));
  return 0;
}


static PyObject* start(PyObject* self, PyObject* args) {
  PyEval_SetTrace(trace, NULL);
  return Py_None;
}


static PyObject* stop(PyObject* self, PyObject* args) {
  PyEval_SetTrace(NULL, NULL);
  return Py_None;
}


static PyMethodDef Methods[] = {
    {"lines", lines, METH_VARARGS, ""},
    {"start", start, METH_VARARGS, ""},
    {"stop", stop, METH_VARARGS, ""},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef module = {
    PyModuleDef_HEAD_INIT,
    "cline",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    Methods
};

PyMODINIT_FUNC
PyInit_cline(void)
{
    return PyModule_Create(&module);
}

@notEvil
Copy link
Author

notEvil commented Oct 31, 2023

Backtracked PyCode_Addr2Line to this line

bounds->ar_line = -1;

3.11.6 doesn't reach it and no other path that might lead to -1 is taken in both versions. I'm nowhere close to understand why.

@gaogaotiantian
Copy link
Member

So there's a simple repro:

import dis

async def f():
    pass

print(list(f.__code__.co_lines()))
dis.dis(f)

3.11:

[(0, 2, 3), (2, 4, 3), (4, 6, 3), (6, 8, 4), (8, 10, 4)]
  3           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  4           6 LOAD_CONST               0 (None)
              8 RETURN_VALUE

3.12:

[(0, 2, 3), (2, 4, None), (4, 6, 3), (6, 8, 4), (8, 12, None)]
  3           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  4           6 RETURN_CONST             0 (None)
        >>    8 CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
             10 RERAISE                  1
ExceptionTable:
  4 to 6 -> 8 [0] lasti

3.13:

[(0, 6, 3), (6, 8, 4), (8, 12, None)]
   3           0 RETURN_GENERATOR
               2 POP_TOP
               4 RESUME                   0

   4           6 RETURN_CONST             0 (None)

None     >>    8 CALL_INTRINSIC_1         3 (INTRINSIC_STOPITERATION_ERROR)
              10 RERAISE                  1
ExceptionTable:
  4 to 6 -> 8 [0] lasti

As you can tell, the major difference between 3.12 and 3.11 is the exception table for the instrinsic call. For 3.13 (current main), the missing line number for 2 POP_TOP is filled, but the line number for the exception table is still missing. I'm not sure if this is intentional - I don't know if there is a good "number" that should be there.

I believe the root cause for PyFrame_GetLineNumber is the same. It's not hard to imagine that when you try to get the line number while executing the byte code in the excpetion table, the line number would be -1 (or converted to None at Python level).

I guess the question is to @iritkatriel or @markshannon : is this expected behavior? Should we have the line number assigned to the new-ish bytecode in the exception region?

@notEvil
Copy link
Author

notEvil commented Oct 31, 2023

Also found #107932 (comment)

Assuming these gaps are here to stay, how should line_profiler deal with them? Fill forward maybe?

@iritkatriel
Copy link
Member

iritkatriel commented Nov 3, 2023

These instructions should have line numbers. I think we should fix this.

@iritkatriel iritkatriel self-assigned this Nov 3, 2023
@iritkatriel iritkatriel added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Nov 3, 2023
iritkatriel added a commit to iritkatriel/cpython that referenced this issue Nov 3, 2023
iritkatriel added a commit to iritkatriel/cpython that referenced this issue Nov 3, 2023
iritkatriel added a commit to iritkatriel/cpython that referenced this issue Nov 3, 2023
@iritkatriel
Copy link
Member

On second thought maybe not - it's a "RERAISE 1" so it propagates the line number of the original RAISE, and we don't want it to create another line event.

@iritkatriel iritkatriel removed their assignment Nov 3, 2023
@iritkatriel
Copy link
Member

Picking this up again - what is the actual issue here? Is it that a line number is -1?
I think this should be expected as possible.

@notEvil
Copy link
Author

notEvil commented Mar 24, 2024

I see. line_profiler reuses the last known line number and this didn't cause any trouble so far. Can be closed I guess.

@iritkatriel
Copy link
Member

Thanks.

@iritkatriel iritkatriel closed this as not planned Won't fix, can't repro, duplicate, stale Mar 24, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs) type-bug An unexpected behavior, bug, or error
Projects
None yet
Development

No branches or pull requests

3 participants