forked from microsoft/debugpy
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support step into target. Fixes microsoft#288
- Loading branch information
Showing
24 changed files
with
5,401 additions
and
3,924 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
297 changes: 297 additions & 0 deletions
297
src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_bytecode_utils.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,297 @@ | ||
"""Bytecode analysing utils. Originally added for using in smart step into.""" | ||
import dis | ||
import inspect | ||
from collections import namedtuple | ||
|
||
from _pydevd_bundle.pydevd_constants import IS_PY3K, KeyifyList | ||
from bisect import bisect | ||
|
||
_LOAD_OPNAMES = { | ||
'LOAD_BUILD_CLASS', | ||
'LOAD_CONST', | ||
'LOAD_NAME', | ||
'LOAD_ATTR', | ||
'LOAD_GLOBAL', | ||
'LOAD_FAST', | ||
'LOAD_CLOSURE', | ||
'LOAD_DEREF', | ||
} | ||
|
||
_CALL_OPNAMES = { | ||
'CALL_FUNCTION', | ||
'CALL_FUNCTION_KW', | ||
} | ||
|
||
if IS_PY3K: | ||
for opname in ('LOAD_CLASSDEREF', 'LOAD_METHOD'): | ||
_LOAD_OPNAMES.add(opname) | ||
for opname in ('CALL_FUNCTION_EX', 'CALL_METHOD'): | ||
_CALL_OPNAMES.add(opname) | ||
else: | ||
_LOAD_OPNAMES.add('LOAD_LOCALS') | ||
for opname in ('CALL_FUNCTION_VAR', 'CALL_FUNCTION_VAR_KW'): | ||
_CALL_OPNAMES.add(opname) | ||
|
||
_BINARY_OPS = set([opname for opname in dis.opname if opname.startswith('BINARY_')]) | ||
|
||
_BINARY_OP_MAP = { | ||
'BINARY_POWER': '__pow__', | ||
'BINARY_MULTIPLY': '__mul__', | ||
'BINARY_MATRIX_MULTIPLY': '__matmul__', | ||
'BINARY_FLOOR_DIVIDE': '__floordiv__', | ||
'BINARY_TRUE_DIVIDE': '__div__', | ||
'BINARY_MODULO': '__mod__', | ||
'BINARY_ADD': '__add__', | ||
'BINARY_SUBTRACT': '__sub__', | ||
'BINARY_LSHIFT': '__lshift__', | ||
'BINARY_RSHIFT': '__rshift__', | ||
'BINARY_AND': '__and__', | ||
'BINARY_OR': '__or__', | ||
'BINARY_XOR': '__xor__', | ||
'BINARY_SUBSCR': '__getitem__', | ||
} | ||
|
||
if not IS_PY3K: | ||
_BINARY_OP_MAP['BINARY_DIVIDE'] = '__div__' | ||
|
||
_UNARY_OPS = set([opname for opname in dis.opname if opname.startswith('UNARY_') and opname != 'UNARY_NOT']) | ||
|
||
_UNARY_OP_MAP = { | ||
'UNARY_POSITIVE': '__pos__', | ||
'UNARY_NEGATIVE': '__neg__', | ||
'UNARY_INVERT': '__invert__', | ||
} | ||
|
||
_MAKE_OPS = set([opname for opname in dis.opname if opname.startswith('MAKE_')]) | ||
|
||
_COMP_OP_MAP = { | ||
'<': '__lt__', | ||
'<=': '__le__', | ||
'==': '__eq__', | ||
'!=': '__ne__', | ||
'>': '__gt__', | ||
'>=': '__ge__', | ||
'in': '__contains__', | ||
'not in': '__contains__', | ||
} | ||
|
||
|
||
def _is_load_opname(opname): | ||
return opname in _LOAD_OPNAMES | ||
|
||
|
||
def _is_call_opname(opname): | ||
return opname in _CALL_OPNAMES | ||
|
||
|
||
def _is_binary_opname(opname): | ||
return opname in _BINARY_OPS | ||
|
||
|
||
def _is_unary_opname(opname): | ||
return opname in _UNARY_OPS | ||
|
||
|
||
def _is_make_opname(opname): | ||
return opname in _MAKE_OPS | ||
|
||
|
||
# Similar to :py:class:`dis._Instruction` but without fields we don't use. Also :py:class:`dis._Instruction` | ||
# is not available in Python 2. | ||
Instruction = namedtuple("Instruction", ["opname", "opcode", "arg", "argval", "lineno", "offset"]) | ||
|
||
if IS_PY3K: | ||
long = int | ||
|
||
try: | ||
_unpack_opargs = dis._unpack_opargs | ||
except AttributeError: | ||
|
||
def _unpack_opargs(code): | ||
n = len(code) | ||
i = 0 | ||
extended_arg = 0 | ||
while i < n: | ||
c = code[i] | ||
op = ord(c) | ||
offset = i | ||
arg = None | ||
i += 1 | ||
if op >= dis.HAVE_ARGUMENT: | ||
arg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg | ||
extended_arg = 0 | ||
i += 2 | ||
if op == dis.EXTENDED_ARG: | ||
extended_arg = arg * long(65536) | ||
yield (offset, op, arg) | ||
|
||
|
||
def _code_to_name(inst): | ||
"""If thw instruction's ``argval`` is :py:class:`types.CodeType`, replace it with the name and return the updated instruction. | ||
:type inst: :py:class:`Instruction` | ||
:rtype: :py:class:`Instruction` | ||
""" | ||
if inspect.iscode(inst.argval): | ||
return inst._replace(argval=inst.argval.co_name) | ||
return inst | ||
|
||
|
||
def _get_smart_step_into_candidates(code): | ||
"""Iterate through the bytecode and return a list of instructions which can be smart step into candidates. | ||
:param code: A code object where we searching for calls. | ||
:type code: :py:class:`types.CodeType` | ||
:return: list of :py:class:`~Instruction` that represents the objects that were called | ||
by one of the Python call instructions. | ||
:raise: :py:class:`RuntimeError` if failed to parse the bytecode or if dis cannot be used. | ||
""" | ||
try: | ||
linestarts = dict(dis.findlinestarts(code)) | ||
except Exception: | ||
raise RuntimeError("Unable to get smart step into candidates because dis.findlinestarts is not available.") | ||
|
||
varnames = code.co_varnames | ||
names = code.co_names | ||
constants = code.co_consts | ||
freevars = code.co_freevars | ||
lineno = None | ||
stk = [] # only the instructions related to calls are pushed in the stack | ||
result = [] | ||
|
||
for offset, op, arg in _unpack_opargs(code.co_code): | ||
try: | ||
if linestarts is not None: | ||
lineno = linestarts.get(offset, None) or lineno | ||
opname = dis.opname[op] | ||
argval = None | ||
if arg is None: | ||
if _is_binary_opname(opname): | ||
stk.pop() | ||
result.append(Instruction(opname, op, arg, _BINARY_OP_MAP[opname], lineno, offset)) | ||
elif _is_unary_opname(opname): | ||
result.append(Instruction(opname, op, arg, _UNARY_OP_MAP[opname], lineno, offset)) | ||
if opname == 'COMPARE_OP': | ||
stk.pop() | ||
cmp_op = dis.cmp_op[arg] | ||
if cmp_op not in ('exception match', 'BAD'): | ||
result.append(Instruction(opname, op, arg, _COMP_OP_MAP.get(cmp_op, cmp_op), lineno, offset)) | ||
if _is_load_opname(opname): | ||
if opname == 'LOAD_CONST': | ||
argval = constants[arg] | ||
elif opname == 'LOAD_NAME' or opname == 'LOAD_GLOBAL': | ||
argval = names[arg] | ||
elif opname == 'LOAD_ATTR': | ||
stk.pop() | ||
argval = names[arg] | ||
elif opname == 'LOAD_FAST': | ||
argval = varnames[arg] | ||
elif IS_PY3K and opname == 'LOAD_METHOD': | ||
stk.pop() | ||
argval = names[arg] | ||
elif opname == 'LOAD_DEREF': | ||
argval = freevars[arg] | ||
stk.append(Instruction(opname, op, arg, argval, lineno, offset)) | ||
elif _is_make_opname(opname): | ||
tos = stk.pop() # qualified name of the function or function code in Python 2 | ||
argc = 0 | ||
if IS_PY3K: | ||
stk.pop() # function code | ||
for flag in (0x01, 0x02, 0x04, 0x08): | ||
if arg & flag: | ||
argc += 1 # each flag means one extra element to pop | ||
else: | ||
argc = arg | ||
tos = _code_to_name(tos) | ||
while argc > 0: | ||
stk.pop() | ||
argc -= 1 | ||
stk.append(tos) | ||
elif _is_call_opname(opname): | ||
argc = arg # the number of the function or method arguments | ||
if opname == 'CALL_FUNCTION_KW' or not IS_PY3K and opname == 'CALL_FUNCTION_VAR': | ||
stk.pop() # pop the mapping or iterable with arguments or parameters | ||
elif not IS_PY3K and opname == 'CALL_FUNCTION_VAR_KW': | ||
stk.pop() # pop the mapping with arguments | ||
stk.pop() # pop the iterable with parameters | ||
elif not IS_PY3K and opname == 'CALL_FUNCTION': | ||
argc = arg & 0xff # positional args | ||
argc += ((arg >> 8) * 2) # keyword args | ||
elif opname == 'CALL_FUNCTION_EX': | ||
has_keyword_args = arg & 0x01 | ||
if has_keyword_args: | ||
stk.pop() | ||
stk.pop() # positional args | ||
argc = 0 | ||
while argc > 0: | ||
stk.pop() # popping args from the stack | ||
argc -= 1 | ||
tos = _code_to_name(stk[-1]) | ||
if tos.opname == 'LOAD_BUILD_CLASS': | ||
# an internal `CALL_FUNCTION` for building a class | ||
continue | ||
result.append(tos._replace(offset=offset)) # the actual offset is not when a function was loaded but when it was called | ||
except: | ||
err_msg = "Bytecode parsing error at: offset(%d), opname(%s), arg(%d)" % (offset, dis.opname[op], arg) | ||
raise RuntimeError(err_msg) | ||
return result | ||
|
||
|
||
# Note that the offset is unique within the frame (so, we can use it as the target id). | ||
# Also, as the offset is the instruction offset within the frame, it's possible to | ||
# to inspect the parent frame for frame.f_lasti to know where we actually are (as the | ||
# caller name may not always match the new frame name). | ||
Variant = namedtuple('Variant', ['name', 'is_visited', 'line', 'offset', 'call_order']) | ||
|
||
|
||
def calculate_smart_step_into_variants(frame, start_line, end_line, base=0): | ||
""" | ||
Calculate smart step into variants for the given line range. | ||
:param frame: | ||
:type frame: :py:class:`types.FrameType` | ||
:param start_line: | ||
:param end_line: | ||
:return: A list of call names from the first to the last. | ||
:note: it's guaranteed that the offsets appear in order. | ||
:raise: :py:class:`RuntimeError` if failed to parse the bytecode or if dis cannot be used. | ||
""" | ||
variants = [] | ||
is_context_reached = False | ||
code = frame.f_code | ||
lasti = frame.f_lasti | ||
|
||
call_order_cache = {} | ||
|
||
for inst in _get_smart_step_into_candidates(code): | ||
if inst.lineno and inst.lineno > end_line: | ||
break | ||
if not is_context_reached and inst.lineno is not None and inst.lineno >= start_line: | ||
is_context_reached = True | ||
if not is_context_reached: | ||
continue | ||
|
||
call_order = call_order_cache.get(inst.argval, 0) + 1 | ||
call_order_cache[inst.argval] = call_order | ||
variants.append( | ||
Variant( | ||
inst.argval, inst.offset <= lasti, inst.lineno - base, inst.offset, call_order)) | ||
return variants | ||
|
||
|
||
def get_smart_step_into_variant_from_frame_offset(frame_f_lasti, variants): | ||
""" | ||
Given the frame.f_lasti, return the related `Variant`. | ||
:note: if the offset is found before any variant available or no variants are | ||
available, None is returned. | ||
:rtype: Variant|NoneType | ||
""" | ||
if not variants: | ||
return None | ||
|
||
i = bisect(KeyifyList(variants, lambda entry:entry.offset), frame_f_lasti) | ||
|
||
if i == 0: | ||
return None | ||
|
||
else: | ||
return variants[i - 1] |
Oops, something went wrong.