From 5e4538ff95ce272b6435ec2cb6d3443c41185b16 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sun, 26 Mar 2023 18:05:03 -0700 Subject: [PATCH 01/10] Support assembly display on pdb --- Lib/pdb.py | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 3a06cd00ad2bf1..925410d4961a3f 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -230,6 +230,7 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, pass self.allow_kbdint = False self.nosigint = nosigint + self.assem_mode = False # Read ~/.pdbrc and ./.pdbrc self.rcLines = [] @@ -1287,6 +1288,19 @@ def do_pp(self, arg): complete_p = _complete_expression complete_pp = _complete_expression + def do_assem(self, arg): + """assem [on | off] + Toggle/Set assembly mode + """ + if not arg: + self.assem_mode = not self.assem_mode + elif arg == "on": + self.assem_mode = True + elif arg == "off": + self.assem_mode = False + else: + self.message("usage: assem [on | off]") + def do_list(self, arg): """l(ist) [first [,last] | .] @@ -1336,7 +1350,8 @@ def do_list(self, arg): try: lines = linecache.getlines(filename, self.curframe.f_globals) self._print_lines(lines[first-1:last], first, breaklist, - self.curframe) + self.curframe, + dis.get_instructions(self.curframe.f_code)) self.lineno = min(last, len(lines)) if len(lines) < last: self.message('[EOF]') @@ -1355,7 +1370,8 @@ def do_longlist(self, arg): except OSError as err: self.error(err) return - self._print_lines(lines, lineno, breaklist, self.curframe) + self._print_lines(lines, lineno, breaklist, self.curframe, + dis.get_instructions(self.curframe.f_code)) do_ll = do_longlist def do_source(self, arg): @@ -1375,13 +1391,15 @@ def do_source(self, arg): complete_source = _complete_expression - def _print_lines(self, lines, start, breaks=(), frame=None): + def _print_lines(self, lines, start, breaks=(), frame=None, instructions=None): """Print a range of lines.""" if frame: current_lineno = frame.f_lineno exc_lineno = self.tb_lineno.get(frame, -1) else: current_lineno = exc_lineno = -1 + if self.assem_mode and instructions: + inst = next(instructions) for lineno, line in enumerate(lines, start): s = str(lineno).rjust(3) if len(s) < 4: @@ -1395,6 +1413,20 @@ def _print_lines(self, lines, start, breaks=(), frame=None): elif lineno == exc_lineno: s += '>>' self.message(s + '\t' + line.rstrip()) + if self.assem_mode and instructions: + while True: + if inst.positions.lineno == lineno: + current_inst = frame and frame.f_lasti == inst.offset + disassem = inst._disassemble(lineno_width=None, + mark_as_current=current_inst) + self.message(f" {disassem}") + elif inst.positions.lineno is not None and inst.positions.lineno > lineno: + break + try: + inst = next(instructions) + except StopIteration: + break + def do_whatis(self, arg): """whatis arg From fc50e5190f45ac0dad5692de549a2f7b440bf369 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Fri, 31 Mar 2023 15:38:41 -0700 Subject: [PATCH 02/10] Implement ni and si, do li and lli instead of assem --- Lib/bdb.py | 72 ++++++++++++++++++++++++-- Lib/pdb.py | 147 ++++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 174 insertions(+), 45 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 7f9b09514ffd00..ab35ef64c8edff 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -32,6 +32,9 @@ def __init__(self, skip=None): self.skip = set(skip) if skip else None self.breaks = {} self.fncache = {} + self._curframe = None + self.lasti = -1 + self.trace_opcodes = False self.frame_returning = None self._load_breaks() @@ -84,6 +87,8 @@ def trace_dispatch(self, frame, event, arg): The arg parameter depends on the previous event. """ + self._curframe = frame + if self.quitting: return # None if event == 'line': @@ -94,6 +99,8 @@ def trace_dispatch(self, frame, event, arg): return self.dispatch_return(frame, arg) if event == 'exception': return self.dispatch_exception(frame, arg) + if event == 'opcode': + return self.dispatch_opcode(frame) if event == 'c_call': return self.trace_dispatch if event == 'c_exception': @@ -115,6 +122,18 @@ def dispatch_line(self, frame): if self.quitting: raise BdbQuit return self.trace_dispatch + def dispatch_opcode(self, frame): + """Invoke user function and return trace function for opcode event. + + If the debugger stops on the current opcode, invoke + self.user_opcode(). Raise BdbQuit if self.quitting is set. + Return self.trace_dispatch to continue tracing in this scope. + """ + if self.stop_here(frame) or self.break_here(frame): + self.user_opcode(frame) + if self.quitting: raise BdbQuit + return self.trace_dispatch + def dispatch_call(self, frame, arg): """Invoke user function and return trace function for call event. @@ -122,6 +141,11 @@ def dispatch_call(self, frame, arg): self.user_call(). Raise BdbQuit if self.quitting is set. Return self.trace_dispatch to continue tracing in this scope. """ + if self.trace_opcodes: + frame.f_trace_opcodes = True + else: + frame.f_trace_opcodes = False + # XXX 'arg' is no longer used if self.botframe is None: # First call of dispatch since reset() @@ -209,9 +233,15 @@ def stop_here(self, frame): if frame is self.stopframe: if self.stoplineno == -1: return False - return frame.f_lineno >= self.stoplineno + if self.trace_opcodes: + return self.lasti != frame.f_lasti + else: + return frame.f_lineno >= self.stoplineno if not self.stopframe: - return True + if self.trace_opcodes: + return self.lasti != frame.f_lasti + else: + return True return False def break_here(self, frame): @@ -272,7 +302,21 @@ def user_exception(self, frame, exc_info): """Called when we stop on an exception.""" pass - def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): + def user_opcode(self, frame): + """Called when we stop or break at a opcode.""" + pass + + def _set_trace_opcodes(self, trace_opcodes): + if trace_opcodes != self.trace_opcodes: + self.trace_opcodes = trace_opcodes + frame = self._curframe + while frame is not None: + frame.f_trace_opcodes = trace_opcodes + if frame is self.botframe: + break + frame = frame.f_back + + def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, lasti=None): """Set the attributes for stopping. If stoplineno is greater than or equal to 0, then stop at line @@ -285,6 +329,12 @@ def _set_stopinfo(self, stopframe, returnframe, stoplineno=0): # stoplineno >= 0 means: stop at line >= the stoplineno # stoplineno -1 means: don't stop at all self.stoplineno = stoplineno + if lasti: + # We are stopping at opcode level + self._set_trace_opcodes(True) + self.lasti = lasti + else: + self._set_trace_opcodes(False) # Derived classes and clients can call the following methods # to affect the stepping state. @@ -309,10 +359,26 @@ def set_step(self): caller_frame.f_trace = self.trace_dispatch self._set_stopinfo(None, None) + def set_stepinst(self, frame): + """Stop after one opcode.""" + # Issue #13183: pdb skips frames after hitting a breakpoint and running + # step commands. + # Restore the trace function in the caller (that may not have been set + # for performance reasons) when returning from the current frame. + if self.frame_returning: + caller_frame = self.frame_returning.f_back + if caller_frame and not caller_frame.f_trace: + caller_frame.f_trace = self.trace_dispatch + self._set_stopinfo(None, None, lasti=frame.f_lasti) + def set_next(self, frame): """Stop on the next line in or below the given frame.""" self._set_stopinfo(frame, None) + def set_nextinst(self, frame): + """Stop on the next line in or below the given frame.""" + self._set_stopinfo(frame, None, lasti=frame.f_lasti) + def set_return(self, frame): """Stop when returning from the given frame.""" if frame.f_code.co_flags & GENERATOR_AND_COROUTINE_FLAGS: diff --git a/Lib/pdb.py b/Lib/pdb.py index 925410d4961a3f..be2ac1d0c46bfb 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -203,6 +203,7 @@ def namespace(self): # command "pdb.line_prefix = '\n% '". # line_prefix = ': ' # Use this to get the old situation back line_prefix = '\n-> ' # Probably a better default +inst_prefix = '\n--> ' # Probably a better default class Pdb(bdb.Bdb, cmd.Cmd): @@ -230,7 +231,6 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None, pass self.allow_kbdint = False self.nosigint = nosigint - self.assem_mode = False # Read ~/.pdbrc and ./.pdbrc self.rcLines = [] @@ -331,6 +331,14 @@ def user_line(self, frame): if self.bp_commands(frame): self.interaction(frame, None) + def user_opcode(self, frame): + if self._wait_for_mainpyfile: + if (self.mainpyfile != self.canonic(frame.f_code.co_filename) + or frame.f_lineno <= 0): + return + self._wait_for_mainpyfile = False + self.interaction(frame, None) + def bp_commands(self, frame): """Call every command that was set for the current active breakpoint (if there is one). @@ -423,6 +431,8 @@ def interaction(self, frame, traceback): self.forget() return self.print_stack_entry(self.stack[self.curindex]) + if self.trace_opcodes: + self.print_current_inst(frame) self._cmdloop() self.forget() @@ -1088,6 +1098,16 @@ def do_step(self, arg): return 1 do_s = do_step + def do_stepinst(self, arg): + """s(tep) + Execute the current line, stop at the first possible occasion + (either in a function that is called or in the current + function). + """ + self.set_stepinst(self.curframe) + return 1 + do_si = do_stepinst + def do_next(self, arg): """n(ext) Continue execution until the next line in the current function @@ -1097,6 +1117,15 @@ def do_next(self, arg): return 1 do_n = do_next + def do_nextinst(self, arg): + """n(ext) + Continue execution until the next line in the current function + is reached or it returns. + """ + self.set_nextinst(self.curframe) + return 1 + do_ni = do_nextinst + def do_run(self, arg): """run [args...] Restart the debugged python program. If a string is supplied @@ -1288,34 +1317,7 @@ def do_pp(self, arg): complete_p = _complete_expression complete_pp = _complete_expression - def do_assem(self, arg): - """assem [on | off] - Toggle/Set assembly mode - """ - if not arg: - self.assem_mode = not self.assem_mode - elif arg == "on": - self.assem_mode = True - elif arg == "off": - self.assem_mode = False - else: - self.message("usage: assem [on | off]") - - def do_list(self, arg): - """l(ist) [first [,last] | .] - - List source code for the current file. Without arguments, - list 11 lines around the current line or continue the previous - listing. With . as argument, list 11 lines around the current - line. With one argument, list 11 lines starting at that line. - With two arguments, list the given range; if the second - argument is less than the first, it is a count. - - The current line in the current frame is indicated by "->". - If an exception is being debugged, the line where the - exception was originally raised or propagated is indicated by - ">>", if it differs from the current line. - """ + def _do_list(self, arg, show_instructions=False): self.lastcmd = 'list' last = None if arg and arg != '.': @@ -1349,20 +1351,57 @@ def do_list(self, arg): breaklist = self.get_file_breaks(filename) try: lines = linecache.getlines(filename, self.curframe.f_globals) + instructions = dis.get_instructions(self.curframe.f_code) \ + if show_instructions else None self._print_lines(lines[first-1:last], first, breaklist, self.curframe, - dis.get_instructions(self.curframe.f_code)) + instructions) self.lineno = min(last, len(lines)) if len(lines) < last: self.message('[EOF]') except KeyboardInterrupt: pass + + def do_list(self, arg): + """l(ist) [first [,last] | .] + + List source code for the current file. Without arguments, + list 11 lines around the current line or continue the previous + listing. With . as argument, list 11 lines around the current + line. With one argument, list 11 lines starting at that line. + With two arguments, list the given range; if the second + argument is less than the first, it is a count. + + The current line in the current frame is indicated by "->". + If an exception is being debugged, the line where the + exception was originally raised or propagated is indicated by + ">>", if it differs from the current line. + """ + self._do_list(arg, False) do_l = do_list - def do_longlist(self, arg): - """longlist | ll - List the whole source code for the current function or frame. + def do_listinst(self, arg): + """listinst | li [first[, last] | .] + + List source code for the current file with instructions. + + Without arguments, list 11 lines around the current line or + continue the previous listing. With . as argument, list 11 + lines around the current line. With one argument, list 11 + lines starting at that line. With two arguments, list the + given range; if the second argument is less than the first, + it is a count. + + The current line in the current frame is indicated by "->". + The current instruction is indicated by "-->" + If an exception is being debugged, the line where the + exception was originally raised or propagated is indicated by + ">>", if it differs from the current line. """ + self._do_list(arg, True) + do_li = do_listinst + + def _do_longlist(self, arg, show_instructions=False): filename = self.curframe.f_code.co_filename breaklist = self.get_file_breaks(filename) try: @@ -1370,10 +1409,27 @@ def do_longlist(self, arg): except OSError as err: self.error(err) return + instructions = dis.get_instructions(self.curframe.f_code) \ + if show_instructions else None self._print_lines(lines, lineno, breaklist, self.curframe, - dis.get_instructions(self.curframe.f_code)) + instructions) + + def do_longlist(self, arg): + """longlist | ll + List the whole source code for the current function or frame. + """ + self._do_longlist(arg, False) do_ll = do_longlist + def do_longlistinst(self, arg): + """longlistinst | lli + + List the whole source code with instructions for the current + function or frame. + """ + self._do_longlist(arg, True) + do_lli = do_longlistinst + def do_source(self, arg): """source expression Try to get source code for the given object and display it. @@ -1398,7 +1454,7 @@ def _print_lines(self, lines, start, breaks=(), frame=None, instructions=None): exc_lineno = self.tb_lineno.get(frame, -1) else: current_lineno = exc_lineno = -1 - if self.assem_mode and instructions: + if instructions: inst = next(instructions) for lineno, line in enumerate(lines, start): s = str(lineno).rjust(3) @@ -1413,21 +1469,21 @@ def _print_lines(self, lines, start, breaks=(), frame=None, instructions=None): elif lineno == exc_lineno: s += '>>' self.message(s + '\t' + line.rstrip()) - if self.assem_mode and instructions: + if instructions: while True: if inst.positions.lineno == lineno: current_inst = frame and frame.f_lasti == inst.offset disassem = inst._disassemble(lineno_width=None, mark_as_current=current_inst) self.message(f" {disassem}") - elif inst.positions.lineno is not None and inst.positions.lineno > lineno: + elif inst.positions.lineno is not None and \ + inst.positions.lineno > lineno: break try: inst = next(instructions) except StopIteration: break - def do_whatis(self, arg): """whatis arg Print the type of the argument. @@ -1591,6 +1647,13 @@ def print_stack_entry(self, frame_lineno, prompt_prefix=line_prefix): self.message(prefix + self.format_stack_entry(frame_lineno, prompt_prefix)) + def print_current_inst(self, frame): + for inst in dis.get_instructions(frame.f_code): + if inst.offset == frame.f_lasti: + self.message(inst._disassemble(lineno_width=None, + mark_as_current=True)) + return + # Provide help def do_help(self, arg): @@ -1700,10 +1763,10 @@ def _compile_error_message(self, expr): # unfortunately we can't guess this order from the class definition _help_order = [ 'help', 'where', 'down', 'up', 'break', 'tbreak', 'clear', 'disable', - 'enable', 'ignore', 'condition', 'commands', 'step', 'next', 'until', - 'jump', 'return', 'retval', 'run', 'continue', 'list', 'longlist', - 'args', 'p', 'pp', 'whatis', 'source', 'display', 'undisplay', - 'interact', 'alias', 'unalias', 'debug', 'quit', + 'enable', 'ignore', 'condition', 'commands', 'step', 'stepinst', + 'next', 'nextinst', 'until', 'jump', 'return', 'retval', 'run', + 'continue', 'list', 'longlist', 'args', 'p', 'pp', 'whatis', 'source', + 'display', 'undisplay', 'interact', 'alias', 'unalias', 'debug', 'quit', ] for _command in _help_order: From c3af63c198b46606e8853a27a38e5ac72343ac9d Mon Sep 17 00:00:00 2001 From: gaogaotiantian Date: Tue, 4 Apr 2023 20:51:41 -0700 Subject: [PATCH 03/10] Fixed a typo and use adaptive for instructions Co-authored-by: Artem Mukhin --- Lib/bdb.py | 2 +- Lib/pdb.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index ab35ef64c8edff..6aa1e2b7ea3e20 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -303,7 +303,7 @@ def user_exception(self, frame, exc_info): pass def user_opcode(self, frame): - """Called when we stop or break at a opcode.""" + """Called when we stop or break at an opcode.""" pass def _set_trace_opcodes(self, trace_opcodes): diff --git a/Lib/pdb.py b/Lib/pdb.py index be2ac1d0c46bfb..39a2eccbf48bee 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1351,7 +1351,7 @@ def _do_list(self, arg, show_instructions=False): breaklist = self.get_file_breaks(filename) try: lines = linecache.getlines(filename, self.curframe.f_globals) - instructions = dis.get_instructions(self.curframe.f_code) \ + instructions = dis.get_instructions(self.curframe.f_code, adaptive=True) \ if show_instructions else None self._print_lines(lines[first-1:last], first, breaklist, self.curframe, @@ -1409,7 +1409,7 @@ def _do_longlist(self, arg, show_instructions=False): except OSError as err: self.error(err) return - instructions = dis.get_instructions(self.curframe.f_code) \ + instructions = dis.get_instructions(self.curframe.f_code, adaptive=True) \ if show_instructions else None self._print_lines(lines, lineno, breaklist, self.curframe, instructions) From 4016fd44f09e4615e892e8bb7fc7dafd165c6ecf Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Tue, 4 Apr 2023 21:10:54 -0700 Subject: [PATCH 04/10] Fix docstrings and commands Used stepi instead of stepinst(other commands too) Abstracted a function to restore caller trace function --- Lib/bdb.py | 35 ++++++++++++++++------------------- Lib/pdb.py | 48 ++++++++++++++++++++++++++---------------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/Lib/bdb.py b/Lib/bdb.py index 6aa1e2b7ea3e20..429771ded4c281 100644 --- a/Lib/bdb.py +++ b/Lib/bdb.py @@ -78,6 +78,7 @@ def trace_dispatch(self, frame, event, arg): is entered. return: A function or other code block is about to return. exception: An exception has occurred. + opcode: An opcode is going to be executed. c_call: A C function is about to be called. c_return: A C function has returned. c_exception: A C function has raised an exception. @@ -303,7 +304,7 @@ def user_exception(self, frame, exc_info): pass def user_opcode(self, frame): - """Called when we stop or break at an opcode.""" + """Called when we are about to execute an opcode.""" pass def _set_trace_opcodes(self, trace_opcodes): @@ -336,6 +337,16 @@ def _set_stopinfo(self, stopframe, returnframe, stoplineno=0, lasti=None): else: self._set_trace_opcodes(False) + def _set_caller_tracefunc(self): + # Issue #13183: pdb skips frames after hitting a breakpoint and running + # step commands. + # Restore the trace function in the caller (that may not have been set + # for performance reasons) when returning from the current frame. + if self.frame_returning: + caller_frame = self.frame_returning.f_back + if caller_frame and not caller_frame.f_trace: + caller_frame.f_trace = self.trace_dispatch + # Derived classes and clients can call the following methods # to affect the stepping state. @@ -349,33 +360,19 @@ def set_until(self, frame, lineno=None): def set_step(self): """Stop after one line of code.""" - # Issue #13183: pdb skips frames after hitting a breakpoint and running - # step commands. - # Restore the trace function in the caller (that may not have been set - # for performance reasons) when returning from the current frame. - if self.frame_returning: - caller_frame = self.frame_returning.f_back - if caller_frame and not caller_frame.f_trace: - caller_frame.f_trace = self.trace_dispatch + self._set_caller_tracefunc() self._set_stopinfo(None, None) - def set_stepinst(self, frame): + def set_stepi(self, frame): """Stop after one opcode.""" - # Issue #13183: pdb skips frames after hitting a breakpoint and running - # step commands. - # Restore the trace function in the caller (that may not have been set - # for performance reasons) when returning from the current frame. - if self.frame_returning: - caller_frame = self.frame_returning.f_back - if caller_frame and not caller_frame.f_trace: - caller_frame.f_trace = self.trace_dispatch + self._set_caller_tracefunc() self._set_stopinfo(None, None, lasti=frame.f_lasti) def set_next(self, frame): """Stop on the next line in or below the given frame.""" self._set_stopinfo(frame, None) - def set_nextinst(self, frame): + def set_nexti(self, frame): """Stop on the next line in or below the given frame.""" self._set_stopinfo(frame, None, lasti=frame.f_lasti) diff --git a/Lib/pdb.py b/Lib/pdb.py index 39a2eccbf48bee..0017219e7243e0 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -332,6 +332,7 @@ def user_line(self, frame): self.interaction(frame, None) def user_opcode(self, frame): + """This function is called when we are about to execute an opcode.""" if self._wait_for_mainpyfile: if (self.mainpyfile != self.canonic(frame.f_code.co_filename) or frame.f_lineno <= 0): @@ -1098,15 +1099,15 @@ def do_step(self, arg): return 1 do_s = do_step - def do_stepinst(self, arg): + def do_stepi(self, arg): """s(tep) Execute the current line, stop at the first possible occasion (either in a function that is called or in the current function). """ - self.set_stepinst(self.curframe) + self.set_stepi(self.curframe) return 1 - do_si = do_stepinst + do_si = do_stepi def do_next(self, arg): """n(ext) @@ -1117,14 +1118,14 @@ def do_next(self, arg): return 1 do_n = do_next - def do_nextinst(self, arg): + def do_nexti(self, arg): """n(ext) Continue execution until the next line in the current function is reached or it returns. """ - self.set_nextinst(self.curframe) + self.set_nexti(self.curframe) return 1 - do_ni = do_nextinst + do_ni = do_nexti def do_run(self, arg): """run [args...] @@ -1380,17 +1381,19 @@ def do_list(self, arg): self._do_list(arg, False) do_l = do_list - def do_listinst(self, arg): - """listinst | li [first[, last] | .] + def do_listi(self, arg): + """listi | li [first[, last] | .] List source code for the current file with instructions. - Without arguments, list 11 lines around the current line or - continue the previous listing. With . as argument, list 11 - lines around the current line. With one argument, list 11 - lines starting at that line. With two arguments, list the - given range; if the second argument is less than the first, - it is a count. + Without arguments, list 11 lines with their corresponding + instructions around the current line or continue the + previous listing. With . as argument, list 11 lines with + their corresponding instructions around the current line. + With one argument, list 11 lines with their corresponding + instructions starting at that line. With two arguments, + list the given range; if the second argument is less than + the first, it is a count. The current line in the current frame is indicated by "->". The current instruction is indicated by "-->" @@ -1399,7 +1402,7 @@ def do_listinst(self, arg): ">>", if it differs from the current line. """ self._do_list(arg, True) - do_li = do_listinst + do_li = do_listi def _do_longlist(self, arg, show_instructions=False): filename = self.curframe.f_code.co_filename @@ -1421,14 +1424,14 @@ def do_longlist(self, arg): self._do_longlist(arg, False) do_ll = do_longlist - def do_longlistinst(self, arg): - """longlistinst | lli + def do_longlisti(self, arg): + """longlisti | lli List the whole source code with instructions for the current function or frame. """ self._do_longlist(arg, True) - do_lli = do_longlistinst + do_lli = do_longlisti def do_source(self, arg): """source expression @@ -1763,10 +1766,11 @@ def _compile_error_message(self, expr): # unfortunately we can't guess this order from the class definition _help_order = [ 'help', 'where', 'down', 'up', 'break', 'tbreak', 'clear', 'disable', - 'enable', 'ignore', 'condition', 'commands', 'step', 'stepinst', - 'next', 'nextinst', 'until', 'jump', 'return', 'retval', 'run', - 'continue', 'list', 'longlist', 'args', 'p', 'pp', 'whatis', 'source', - 'display', 'undisplay', 'interact', 'alias', 'unalias', 'debug', 'quit', + 'enable', 'ignore', 'condition', 'commands', 'step', 'stepi', + 'next', 'nexti', 'until', 'jump', 'return', 'retval', 'run', + 'continue', 'list', 'listi', 'longlist', 'longlisti', 'args', 'p', + 'pp', 'whatis', 'source', 'display', 'undisplay', 'interact', 'alias', + 'unalias', 'debug', 'quit', ] for _command in _help_order: From 11d13958dda05f45238f767c82b9be5a0361814f Mon Sep 17 00:00:00 2001 From: gaogaotiantian Date: Wed, 5 Apr 2023 16:30:51 -0700 Subject: [PATCH 05/10] Fix docstring for stepi Co-authored-by: Artem Mukhin --- Lib/pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pdb.py b/Lib/pdb.py index 0017219e7243e0..d48a415ffa7f39 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1101,7 +1101,7 @@ def do_step(self, arg): def do_stepi(self, arg): """s(tep) - Execute the current line, stop at the first possible occasion + Execute the current instruction, stop at the first possible occasion (either in a function that is called or in the current function). """ From a42a420e8f8c2c9541e4735ed9459f684cd94810 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sat, 8 Apr 2023 16:02:47 -0700 Subject: [PATCH 06/10] Add tests for instruction level commands --- Lib/test/test_pdb.py | 96 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index de2bab46495729..458cef02ac3ca2 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -1,5 +1,6 @@ # A test suite for pdb; not very comprehensive at the moment. +import dis import doctest import os import pdb @@ -2351,6 +2352,101 @@ def _create_fake_frozen_module(): # verify that pdb found the source of the "frozen" function self.assertIn('x = "Sentinel string for gh-93696"', stdout, "Sentinel statement not found") + def get_func_opnames(self, func_def, func_name): + extract_code = f""" + import dis + for inst in dis.get_instructions({func_name}): + print(inst.opname) + """ + + with redirect_stdout(StringIO()) as s: + exec(textwrap.dedent(func_def) + textwrap.dedent(extract_code)) + + return s.getvalue().splitlines() + + def test_list_instruction(self): + func_def = """ + def f(): + a = [1, 2, 3] + return a[0] + """ + func_exec = """ + f() + """ + script = func_def + func_exec + + commands_li = """ + break f + c + li + """ + + commands_lli = """ + break f + c + lli + """ + + # Make sure all the opcodes are listed + stdout, stderr = self.run_pdb_module(script, commands_li) + for opname in self.get_func_opnames(func_def, "f"): + self.assertIn(opname, stdout) + + stdout, stderr = self.run_pdb_module(script, commands_lli) + for opname in self.get_func_opnames(func_def, "f"): + self.assertIn(opname, stdout) + + def test_instruction_level_control(self): + func_def = """ + def f(): + a = [1, 2, 3] + return a[0] + """ + func_exec = """ + f() + """ + script = func_def + func_exec + + commands = """ + ni + li + """ + + # Check that after ni, current instruction is displayed + stdout, stderr = self.run_pdb_module(script, commands) + lines = [line.strip() for line in stdout.splitlines()] + for idx, line in enumerate(lines): + if "-->" in line: + # Found the current instruction indicator after ni + # Make sure that is listed in li + self.assertIn(line, lines[idx+1:]) + break + + commands = """ + ni + ni + ni + c + """ + + stdout, stderr = self.run_pdb_module(script, commands) + curr_instr_lines = [line.strip() for line in stdout.splitlines() if "-->" in line] + self.assertEqual(len(curr_instr_lines), 3) + for line in curr_instr_lines: + # Make sure ni is moving forward, not stopping at the same instrunction + self.assertEqual(curr_instr_lines.count(line), 1) + + # this test is under the assumption that within 10 instructions the function + # f should be called + commands = "si\n" * 10 + "c\n" + + stdout, stderr = self.run_pdb_module(script, commands) + curr_instr_lines = [line.strip() for line in stdout.splitlines()] + # Make sure si stepped into the function so the users can see the source + # code of the function + self.assertTrue(any("-> a = [1, 2, 3]" in line for line in curr_instr_lines)) + + class ChecklineTests(unittest.TestCase): def setUp(self): linecache.clearcache() # Pdb.checkline() uses linecache.getline() From 116ec5d4ea3665b3f524fc871766ebfa3f7b32d1 Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sat, 8 Apr 2023 16:18:15 -0700 Subject: [PATCH 07/10] Add docs for instruction commands --- Doc/library/pdb.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index d80c5eebbf27a7..ea2e5b614bf631 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -379,6 +379,13 @@ can be overridden by the local file. Execute the current line, stop at the first possible occasion (either in a function that is called or on the next line in the current function). +.. pdbcommand:: si | stepi + + Execute the current instruction, stop at the first possible occasion (either in a + function that is called or on the next instruction in the current function). + + .. versionadded:: 3.12 + .. pdbcommand:: n(ext) Continue execution until the next line in the current function is reached or @@ -387,6 +394,13 @@ can be overridden by the local file. executes called functions at (nearly) full speed, only stopping at the next line in the current function.) +.. pdbcommand:: ni | nexti + + Continue execution until the next instruction in the current function is reached or + it returns. + + .. versionadded:: 3.12 + .. pdbcommand:: unt(il) [lineno] Without argument, continue execution until the line with a number greater @@ -433,6 +447,12 @@ can be overridden by the local file. .. versionadded:: 3.2 The ``>>`` marker. +.. pdbcommand:: li | listi [first[, last]] + + Similar to :pdbcmd:`list`, but also display instructions with source code + + .. versionadded:: 3.12 + .. pdbcommand:: ll | longlist List all source code for the current function or frame. Interesting lines @@ -440,6 +460,12 @@ can be overridden by the local file. .. versionadded:: 3.2 +.. pdbcommand:: lli | longlisti + + Similar to :pdbcmd:`ll`, but also display instructions with source code + + .. versionadded:: 3.12 + .. pdbcommand:: a(rgs) Print the argument list of the current function. From 89b6899ec63bca01187c86750163543f435771ba Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Sat, 8 Apr 2023 23:20:35 +0000 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst diff --git a/Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst b/Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst new file mode 100644 index 00000000000000..f20de87713ba6f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-08-23-20-34.gh-issue-103049.PY_2cD.rst @@ -0,0 +1 @@ +Add instruction commands support for :mod:`pdb` From faabf32511df5f6f29e5d168023dea1ad51beb8d Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sun, 9 Apr 2023 11:06:05 -0700 Subject: [PATCH 09/10] Fixed a typo in comment Co-authored-by: Artem Mukhin --- Lib/test/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_pdb.py b/Lib/test/test_pdb.py index 458cef02ac3ca2..735092f6d7ab22 100644 --- a/Lib/test/test_pdb.py +++ b/Lib/test/test_pdb.py @@ -2433,7 +2433,7 @@ def f(): curr_instr_lines = [line.strip() for line in stdout.splitlines() if "-->" in line] self.assertEqual(len(curr_instr_lines), 3) for line in curr_instr_lines: - # Make sure ni is moving forward, not stopping at the same instrunction + # Make sure ni is moving forward, not stopping at the same instruction self.assertEqual(curr_instr_lines.count(line), 1) # this test is under the assumption that within 10 instructions the function From e258bf525448d8ba072dfc99c8b347427277174a Mon Sep 17 00:00:00 2001 From: Tian Gao Date: Sun, 9 Apr 2023 11:20:55 -0700 Subject: [PATCH 10/10] Update some docs and comments --- Doc/library/pdb.rst | 15 +++++++++------ Lib/pdb.py | 23 ++++++++++++++--------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/Doc/library/pdb.rst b/Doc/library/pdb.rst index ea2e5b614bf631..00caa97d212a2c 100644 --- a/Doc/library/pdb.rst +++ b/Doc/library/pdb.rst @@ -381,8 +381,9 @@ can be overridden by the local file. .. pdbcommand:: si | stepi - Execute the current instruction, stop at the first possible occasion (either in a - function that is called or on the next instruction in the current function). + Execute the current bytecode instruction, stop at the first possible occasion + (either in a function that is called or on the next instruction in the + current function). .. versionadded:: 3.12 @@ -396,8 +397,8 @@ can be overridden by the local file. .. pdbcommand:: ni | nexti - Continue execution until the next instruction in the current function is reached or - it returns. + Continue execution until the next bytecode instruction in the current function + is reached or it returns. .. versionadded:: 3.12 @@ -449,7 +450,8 @@ can be overridden by the local file. .. pdbcommand:: li | listi [first[, last]] - Similar to :pdbcmd:`list`, but also display instructions with source code + Similar to :pdbcmd:`list`, but also display bytecode instructions with + the source code .. versionadded:: 3.12 @@ -462,7 +464,8 @@ can be overridden by the local file. .. pdbcommand:: lli | longlisti - Similar to :pdbcmd:`ll`, but also display instructions with source code + Similar to :pdbcmd:`ll`, but also display bytecode instructions with + the source code .. versionadded:: 3.12 diff --git a/Lib/pdb.py b/Lib/pdb.py index d48a415ffa7f39..3ff9eb1dc9adb7 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1101,9 +1101,9 @@ def do_step(self, arg): def do_stepi(self, arg): """s(tep) - Execute the current instruction, stop at the first possible occasion - (either in a function that is called or in the current - function). + Execute the current bytecode instruction, stop at the first + possible occasion (either in a function that is called or in + the current function). """ self.set_stepi(self.curframe) return 1 @@ -1112,7 +1112,7 @@ def do_stepi(self, arg): def do_next(self, arg): """n(ext) Continue execution until the next line in the current function - is reached or it returns. + is reached or the current function returns. """ self.set_next(self.curframe) return 1 @@ -1120,8 +1120,8 @@ def do_next(self, arg): def do_nexti(self, arg): """n(ext) - Continue execution until the next line in the current function - is reached or it returns. + Continue execution until the next bytecode instruction in the + current function is reached or the current function returns. """ self.set_nexti(self.curframe) return 1 @@ -1384,7 +1384,8 @@ def do_list(self, arg): def do_listi(self, arg): """listi | li [first[, last] | .] - List source code for the current file with instructions. + List source code for the current file with bytecode + instructions. Without arguments, list 11 lines with their corresponding instructions around the current line or continue the @@ -1427,8 +1428,8 @@ def do_longlist(self, arg): def do_longlisti(self, arg): """longlisti | lli - List the whole source code with instructions for the current - function or frame. + List the whole source code with bytecode instructions for + the current function or frame. """ self._do_longlist(arg, True) do_lli = do_longlisti @@ -1473,6 +1474,10 @@ def _print_lines(self, lines, start, breaks=(), frame=None, instructions=None): s += '>>' self.message(s + '\t' + line.rstrip()) if instructions: + # For the current line of the source code, get all the + # instructions belong to it. We keep a single iterator + # `instructions` for all the instructions compiled from + # the source and try to only go through the iterator once while True: if inst.positions.lineno == lineno: current_inst = frame and frame.f_lasti == inst.offset