diff options
author | Mehdi Amini <joker.eph@gmail.com> | 2023-02-15 15:56:08 -0800 |
---|---|---|
committer | Mehdi Amini <joker.eph@gmail.com> | 2023-04-24 14:34:15 -0700 |
commit | 1020150e7a6f6d6f833c232125c5ab817c03c76b (patch) | |
tree | 7e74387e13d346152dcfcf3577f241726f187b32 /mlir/utils | |
parent | 1869a9c225c7ed411a15592d21b277716b65a374 (diff) | |
download | llvm-1020150e7a6f6d6f833c232125c5ab817c03c76b.tar.gz |
Add a GDB/LLDB interface for interactive debugging of MLIR Actions
This includes a small runtime acting as callback for the ExecutionEngine
and a C API that makes it possible to control from the debugger.
A python script for LLDB is included that hook a new `mlir` subcommand
and allows to set breakpoints and inspect the current action, the context
and the stack.
Differential Revision: https://reviews.llvm.org/D144817
Diffstat (limited to 'mlir/utils')
-rw-r--r-- | mlir/utils/lldb-scripts/action_debugging.py | 568 |
1 files changed, 568 insertions, 0 deletions
diff --git a/mlir/utils/lldb-scripts/action_debugging.py b/mlir/utils/lldb-scripts/action_debugging.py new file mode 100644 index 000000000000..2371967bf72a --- /dev/null +++ b/mlir/utils/lldb-scripts/action_debugging.py @@ -0,0 +1,568 @@ +#!/usr/bin/env python + +# --------------------------------------------------------------------- +# Be sure to add the python path that points to the LLDB shared library. +# +# # To use this in the embedded python interpreter using "lldb" just +# import it with the full path using the "command script import" +# command +# (lldb) command script import /path/to/cmdtemplate.py +# --------------------------------------------------------------------- + +import inspect +import lldb +import argparse +import shlex +import sys + +# Each new breakpoint gets a unique ID starting from 1. +nextid = 1 +# List of breakpoint set from python, the key is the ID and the value the +# actual breakpoint. These are NOT LLDB SBBreakpoint objects. +breakpoints = dict() + +exprOptions = lldb.SBExpressionOptions() +exprOptions.SetIgnoreBreakpoints() +exprOptions.SetLanguage(lldb.eLanguageTypeC) + + +class MlirDebug: + """MLIR debugger commands + This is the class that hooks into LLDB and registers the `mlir` command. + Other providers can register subcommands below this one. + """ + + lldb_command = "mlir" + parser = None + + def __init__(self, debugger, unused): + super().__init__() + self.create_options() + self.help_string = MlirDebug.parser.format_help() + + @classmethod + def create_options(cls): + if MlirDebug.parser: + return MlirDebug.parser + usage = "usage: %s [options]" % (cls.lldb_command) + description = "TODO." + + # Pass add_help_option = False, since this keeps the command in line + # with lldb commands, and we wire up "help command" to work by + # providing the long & short help methods below. + MlirDebug.parser = argparse.ArgumentParser( + prog=cls.lldb_command, usage=usage, description=description, add_help=False + ) + MlirDebug.subparsers = MlirDebug.parser.add_subparsers(dest="command") + return MlirDebug.parser + + def get_short_help(self): + return "MLIR debugger commands" + + def get_long_help(self): + return self.help_string + + def __call__(self, debugger, command, exe_ctx, result): + # Use the Shell Lexer to properly parse up command options just like a + # shell would + command_args = shlex.split(command) + + try: + args = MlirDebug.parser.parse_args(command_args) + except: + result.SetError("option parsing failed") + raise + args.func(args, debugger, command, exe_ctx, result) + + @classmethod + def on_process_start(frame, bp_loc, dict): + print("Process started") + + +class SetControl: + # Define the subcommands that controls what to do when a breakpoint is hit. + # The key is the subcommand name, the value is a tuple of the command ID to + # pass to MLIR and the help string. + commands = { + "apply": (1, "Apply the current action and continue the execution"), + "skip": (2, "Skip the current action and continue the execution"), + "step": (3, "Step into the current action"), + "next": (4, "Step over the current action"), + "finish": (5, "Step out of the current action"), + } + + @classmethod + def register_mlir_subparser(cls): + for cmd, (cmdInt, help) in cls.commands.items(): + parser = MlirDebug.subparsers.add_parser( + cmd, + help=help, + ) + parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError("No valid frame (program not running?)") + return + cmdInt = cls.commands.get(options.command, None) + if not cmdInt: + result.SetError("Invalid command: %s" % (options.command)) + return + + result = frame.EvaluateExpression( + "((bool (*)(int))mlirDebuggerSetControl)(%d)" % (cmdInt[0]), + exprOptions, + ) + if not result.error.Success(): + print("Error setting up command: %s" % (result.error)) + return + debugger.SetAsync(True) + result = exe_ctx.GetProcess().Continue() + debugger.SetAsync(False) + + +class PrintContext: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "context", help="Print the current context" + ) + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError("Can't print context without a valid frame") + return + result = frame.EvaluateExpression( + "((bool (*)())&mlirDebuggerPrintContext)()", exprOptions + ) + if not result.error.Success(): + print("Error printing context: %s" % (result.error)) + return + + +class Backtrace: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "backtrace", aliases=["bt"], help="Print the current backtrace" + ) + cls.parser.set_defaults(func=cls.process_options) + cls.parser.add_argument("--context", default=False, action="store_true") + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't backtrace without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)(bool))mlirDebuggerPrintActionBacktrace)(%d)" % (options.context), + exprOptions, + ) + if not result.error.Success(): + print("Error printing breakpoints: %s" % (result.error)) + return + + +############################################################################### +# Cursor manipulation +############################################################################### + + +class PrintCursor: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "cursor-print", aliases=["cursor-p"], help="Print the current cursor" + ) + cls.parser.add_argument( + "--print-region", "--regions", "-r", default=False, action="store_true" + ) + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't print cursor without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)(bool))mlirDebuggerCursorPrint)(%d)" % (options.print_region), + exprOptions, + ) + if not result.error.Success(): + print("Error printing cursor: %s" % (result.error)) + return + + +class SelectCursorFromContext: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "cursor-select-from-context", + aliases=["cursor-s"], + help="Select the cursor from the current context", + ) + cls.parser.add_argument("index", type=int, help="Index in the context") + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't manipulate cursor without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)(int))mlirDebuggerCursorSelectIRUnitFromContext)(%d)" + % options.index, + exprOptions, + ) + if not result.error.Success(): + print("Error manipulating cursor: %s" % (result.error)) + return + + +class CursorSelectParent: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "cursor-parent", aliases=["cursor-up"], help="Select the cursor parent" + ) + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't manipulate cursor without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)())mlirDebuggerCursorSelectParentIRUnit)()", + exprOptions, + ) + if not result.error.Success(): + print("Error manipulating cursor: %s" % (result.error)) + return + + +class SelectCursorChild: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "cursor-child", aliases=["cursor-c"], help="Select the nth child" + ) + cls.parser.add_argument("index", type=int, help="Index of the child to select") + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't manipulate cursor without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)(int))mlirDebuggerCursorSelectChildIRUnit)(%d)" % options.index, + exprOptions, + ) + if not result.error.Success(): + print("Error manipulating cursor: %s" % (result.error)) + return + + +class CursorSelecPrevious: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "cursor-previous", + aliases=["cursor-prev"], + help="Select the cursor previous element", + ) + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't manipulate cursor without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)())mlirDebuggerCursorSelectPreviousIRUnit)()", + exprOptions, + ) + if not result.error.Success(): + print("Error manipulating cursor: %s" % (result.error)) + return + + +class CursorSelecNext: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "cursor-next", aliases=["cursor-n"], help="Select the cursor next element" + ) + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + frame = exe_ctx.GetFrame() + if not frame.IsValid(): + result.SetError( + "Can't manipulate cursor without a valid frame (program not running?)" + ) + result = frame.EvaluateExpression( + "((bool(*)())mlirDebuggerCursorSelectNextIRUnit)()", + exprOptions, + ) + if not result.error.Success(): + print("Error manipulating cursor: %s" % (result.error)) + return + + +############################################################################### +# Breakpoints +############################################################################### + + +class EnableBreakpoint: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "enable", help="Enable a single breakpoint (given its ID)" + ) + cls.parser.add_argument("id", help="ID of the breakpoint to enable") + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + bp = breakpoints.get(int(options.id), None) + if not bp: + result.SetError("No breakpoint with ID %d" % int(options.id)) + return + bp.enable(exe_ctx.GetFrame()) + + +class DisableBreakpoint: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "disable", help="Disable a single breakpoint (given its ID)" + ) + cls.parser.add_argument("id", help="ID of the breakpoint to disable") + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + bp = breakpoints.get(int(options.id), None) + if not bp: + result.SetError("No breakpoint with ID %s" % options.id) + return + bp.disable(exe_ctx.GetFrame()) + + +class ListBreakpoints: + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + "list", help="List all current breakpoints" + ) + cls.parser.set_defaults(func=cls.process_options) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + for id, bp in sorted(breakpoints.items()): + print(id, type(id), str(bp), "enabled" if bp.isEnabled else "disabled") + + +class Breakpoint: + def __init__(self): + global nextid + self.id = nextid + nextid += 1 + breakpoints[self.id] = self + self.isEnabled = True + + def enable(self, frame=None): + self.isEnabled = True + if not frame or not frame.IsValid(): + return + # use a C cast to force the type of the breakpoint handle to be void * so + # that we don't rely on DWARF. Also add a fake bool return value otherwise + # LLDB can't signal any error with the expression evaluation (at least I don't know how). + cmd = ( + "((bool (*)(void *))mlirDebuggerEnableBreakpoint)((void *)%s)" % self.handle + ) + result = frame.EvaluateExpression(cmd, exprOptions) + if not result.error.Success(): + print("Error enabling breakpoint: %s" % (result.error)) + return + + def disable(self, frame=None): + self.isEnabled = False + if not frame or not frame.IsValid(): + return + # use a C cast to force the type of the breakpoint handle to be void * so + # that we don't rely on DWARF. Also add a fake bool return value otherwise + # LLDB can't signal any error with the expression evaluation (at least I don't know how). + cmd = ( + "((bool (*)(void *)) mlirDebuggerDisableBreakpoint)((void *)%s)" + % self.handle + ) + result = frame.EvaluateExpression(cmd, exprOptions) + if not result.error.Success(): + print("Error disabling breakpoint: %s" % (result.error)) + return + + +class TagBreakpoint(Breakpoint): + mlir_subcommand = "break-on-tag" + + def __init__(self, tag): + super().__init__() + self.tag = tag + + def __str__(self): + return "[%d] TagBreakpoint(%s)" % (self.id, self.tag) + + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + cls.mlir_subcommand, help="add a breakpoint on actions' tag matching" + ) + cls.parser.set_defaults(func=cls.process_options) + cls.parser.add_argument("tag", help="tag to match") + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + breakpoint = TagBreakpoint(options.tag) + print("Added breakpoint %s" % str(breakpoint)) + + frame = exe_ctx.GetFrame() + if frame.IsValid(): + breakpoint.install(frame) + + def install(self, frame): + result = frame.EvaluateExpression( + '((void *(*)(const char *))mlirDebuggerAddTagBreakpoint)("%s")' + % (self.tag), + exprOptions, + ) + if not result.error.Success(): + print("Error installing breakpoint: %s" % (result.error)) + return + # Save the handle, this is necessary to implement enable/disable. + self.handle = result.GetValue() + + +class FileLineBreakpoint(Breakpoint): + mlir_subcommand = "break-on-file" + + def __init__(self, file, line, col): + super().__init__() + self.file = file + self.line = line + self.col = col + + def __str__(self): + return "[%d] FileLineBreakpoint(%s, %d, %d)" % ( + self.id, + self.file, + self.line, + self.col, + ) + + @classmethod + def register_mlir_subparser(cls): + cls.parser = MlirDebug.subparsers.add_parser( + cls.mlir_subcommand, + help="add a breakpoint that filters on location of the IR affected by an action. The syntax is file:line:col where file and col are optional", + ) + cls.parser.set_defaults(func=cls.process_options) + cls.parser.add_argument("location", type=str) + + @classmethod + def process_options(cls, options, debugger, command, exe_ctx, result): + split_loc = options.location.split(":") + file = split_loc[0] + line = int(split_loc[1]) if len(split_loc) > 1 else -1 + col = int(split_loc[2]) if len(split_loc) > 2 else -1 + breakpoint = FileLineBreakpoint(file, line, col) + print("Added breakpoint %s" % str(breakpoint)) + + frame = exe_ctx.GetFrame() + if frame.IsValid(): + breakpoint.install(frame) + + def install(self, frame): + result = frame.EvaluateExpression( + '((void *(*)(const char *, int, int))mlirDebuggerAddFileLineColLocBreakpoint)("%s", %d, %d)' + % (self.file, self.line, self.col), + exprOptions, + ) + if not result.error.Success(): + print("Error installing breakpoint: %s" % (result.error)) + return + # Save the handle, this is necessary to implement enable/disable. + self.handle = result.GetValue() + + +def on_start(frame, bpno, err): + print("MLIR debugger attaching...") + for _, bp in sorted(breakpoints.items()): + if bp.isEnabled: + print("Installing breakpoint %s" % (str(bp))) + bp.install(frame) + else: + print("Skipping disabled breakpoint %s" % (str(bp))) + + return True + + +def __lldb_init_module(debugger, dict): + target = debugger.GetTargetAtIndex(0) + debugger.SetAsync(False) + if not target: + print("No target is loaded, please load a target before loading this script.") + return + if debugger.GetNumTargets() > 1: + print( + "Multiple targets (%s) loaded, attaching MLIR debugging to %s" + % (debugger.GetNumTargets(), target) + ) + + # Register all classes that have a register_lldb_command method + module_name = __name__ + parser = MlirDebug.create_options() + MlirDebug.__doc__ = parser.format_help() + + # Add the MLIR entry point to LLDB as a command. + command = "command script add -o -c %s.%s %s" % ( + module_name, + MlirDebug.__name__, + MlirDebug.lldb_command, + ) + debugger.HandleCommand(command) + + main_bp = target.BreakpointCreateByName("main") + main_bp.SetScriptCallbackFunction("action_debugging.on_start") + main_bp.SetAutoContinue(auto_continue=True) + + on_breackpoint = target.BreakpointCreateByName("mlirDebuggerBreakpointHook") + + print( + 'The "{0}" command has been installed for target `{1}`, type "help {0}" or "{0} ' + '--help" for detailed help.'.format(MlirDebug.lldb_command, target) + ) + for _name, cls in inspect.getmembers(sys.modules[module_name]): + if inspect.isclass(cls) and getattr(cls, "register_mlir_subparser", None): + cls.register_mlir_subparser() |