diff options
-rwxr-xr-x | bin/nim-gdb | 18 | ||||
-rw-r--r-- | compiler/astalgo.nim | 6 | ||||
-rw-r--r-- | tests/untestable/gdb/gdb_pretty_printer_test.py | 33 | ||||
-rw-r--r-- | tests/untestable/gdb/gdb_pretty_printer_test_output.txt | 3 | ||||
-rw-r--r-- | tests/untestable/gdb/gdb_pretty_printer_test_program.nim | 53 | ||||
-rwxr-xr-x | tests/untestable/gdb/gdb_pretty_printer_test_run.sh | 15 | ||||
-rw-r--r-- | tools/nim-gdb.py | 513 |
7 files changed, 638 insertions, 3 deletions
diff --git a/bin/nim-gdb b/bin/nim-gdb new file mode 100755 index 000000000..e7b41094d --- /dev/null +++ b/bin/nim-gdb @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +# Exit if anything fails +set -e + +# Find out where the pretty printer Python module is +NIM_SYSROOT=$(dirname $(dirname $(readlink -e $(which nim)))) +GDB_PYTHON_MODULE_PATH="$NIM_SYSROOT/tools/nim-gdb.py" + +# Run GDB with the additional arguments that load the pretty printers +# Set the environment variable `NIM_GDB` to overwrite the call to a +# different/specific command (defaults to `gdb`). +NIM_GDB="${NIM_GDB:-gdb}" +# exec replaces the new process of bash with gdb. It is always good to +# have fewer processes. +exec ${NIM_GDB} \ + -eval-command "source $GDB_PYTHON_MODULE_PATH" \ + "$@" diff --git a/compiler/astalgo.nim b/compiler/astalgo.nim index f474ca65e..333376f6a 100644 --- a/compiler/astalgo.nim +++ b/compiler/astalgo.nim @@ -27,9 +27,9 @@ proc lineInfoToStr*(conf: ConfigRef; info: TLineInfo): Rope when declared(echo): # these are for debugging only: They are not really deprecated, but I want # the warning so that release versions do not contain debugging statements: - proc debug*(n: PSym; conf: ConfigRef = nil) {.deprecated.} - proc debug*(n: PType; conf: ConfigRef = nil) {.deprecated.} - proc debug*(n: PNode; conf: ConfigRef = nil) {.deprecated.} + proc debug*(n: PSym; conf: ConfigRef = nil) {.exportc: "debugSym", deprecated.} + proc debug*(n: PType; conf: ConfigRef = nil) {.exportc: "debugType", deprecated.} + proc debug*(n: PNode; conf: ConfigRef = nil) {.exportc: "debugNode", deprecated.} template debug*(x: PSym|PType|PNode) {.deprecated.} = when compiles(c.config): diff --git a/tests/untestable/gdb/gdb_pretty_printer_test.py b/tests/untestable/gdb/gdb_pretty_printer_test.py new file mode 100644 index 000000000..54af65d9a --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test.py @@ -0,0 +1,33 @@ +import gdb +# this test should test the gdb pretty printers of the nim +# library. But be aware this test is not complete. It only tests the +# command line version of gdb. It does not test anything for the +# machine interface of gdb. This means if if this test passes gdb +# frontends might still be broken. + +gdb.execute("source ../../../tools/nim-gdb.py") +# debug all instances of the generic function `myDebug`, should be 8 +gdb.execute("rbreak myDebug") +gdb.execute("run") + +outputs = [ + 'meTwo', + '"meTwo"', + '{meOne, meThree}', + 'MyOtherEnum(1)', + '5', + 'array = {1, 2, 3, 4, 5}', + 'seq(3, 3) = {"one", "two", "three"}', + 'Table(3, 64) = {["two"] = 2, ["three"] = 3, ["one"] = 1}', +] + +for i, expected in enumerate(outputs): + if i == 5: + # myArray is passed as pointer to int to myDebug. I look up myArray up in the stack + gdb.execute("up") + output = str(gdb.parse_and_eval("myArray")) + else: + output = str(gdb.parse_and_eval("arg")) + + assert output == expected, output + " != " + expected + gdb.execute("continue") diff --git a/tests/untestable/gdb/gdb_pretty_printer_test_output.txt b/tests/untestable/gdb/gdb_pretty_printer_test_output.txt new file mode 100644 index 000000000..cbc9bde8d --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test_output.txt @@ -0,0 +1,3 @@ +Loading Nim Runtime support. +NimEnumPrinter: lookup global symbol 'NTI_z9cu80OJCfNgw9bUdzn5ZEzw_ failed for tyEnum_MyOtherEnum_z9cu80OJCfNgw9bUdzn5ZEzw. +8 diff --git a/tests/untestable/gdb/gdb_pretty_printer_test_program.nim b/tests/untestable/gdb/gdb_pretty_printer_test_program.nim new file mode 100644 index 000000000..458435c1a --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test_program.nim @@ -0,0 +1,53 @@ + + +import tables + +type + MyEnum = enum + meOne, + meTwo, + meThree, + meFour, + + MyOtherEnum = enum + moOne, + moTwo, + moThree, + moFoure, + + +var counter = 0 + +proc myDebug[T](arg: T): void = + counter += 1 + +proc testProc(): void = + var myEnum = meTwo + myDebug(myEnum) + # create a string object but also make the NTI for MyEnum is generated + var myString = $myEnum + myDebug(myString) + var mySet = {meOne,meThree} + myDebug(mySet) + + # for MyOtherEnum there is no NTI. This tests the fallback for the pretty printer. + var moEnum = moTwo + myDebug(moEnum) + var moSet = {moOne,moThree} + myDebug(moSet) + + let myArray = [1,2,3,4,5] + myDebug(myArray) + let mySeq = @["one","two","three"] + myDebug(mySeq) + + var myTable = initTable[string, int]() + myTable["one"] = 1 + myTable["two"] = 2 + myTable["three"] = 3 + myDebug(myTable) + + echo(counter) + + +testProc() diff --git a/tests/untestable/gdb/gdb_pretty_printer_test_run.sh b/tests/untestable/gdb/gdb_pretty_printer_test_run.sh new file mode 100755 index 000000000..525f54705 --- /dev/null +++ b/tests/untestable/gdb/gdb_pretty_printer_test_run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Exit if anything fails +set -e +#!/usr/bin/env bash +# Compile the test project with fresh debug information. +nim c --debugger:native gdb_pretty_printer_test_program.nim &> /dev/null +# 2>&1 redirects stderr to stdout (all output in stdout) +# <(...) is a bash feature that makes the output of a command into a +# file handle. +# diff compares the two files, the expected output, and the file +# handle that is created by the execution of gdb. +diff ./gdb_pretty_printer_test_output.txt <(gdb -x gdb_pretty_printer_test.py --batch-silent --args gdb_pretty_printer_test_program 2>&1) +# The exit code of diff is forwarded as the exit code of this +# script. So when the comparison fails, the exit code of this script +# won't be 0. So this script should be embeddable in a test suite. diff --git a/tools/nim-gdb.py b/tools/nim-gdb.py new file mode 100644 index 000000000..b98dc96fe --- /dev/null +++ b/tools/nim-gdb.py @@ -0,0 +1,513 @@ + +import gdb +import re +import sys + +# some feedback that the nim runtime support is loading, isn't a bad +# thing at all. +gdb.write("Loading Nim Runtime support.\n", gdb.STDERR) + +# When error occure they occur regularly. This 'caches' known errors +# and prevents them from being reprinted over and over again. +errorSet = set() +def printErrorOnce(id, message): + global errorSet + if id not in errorSet: + errorSet.add(id) + gdb.write(message, gdb.STDERR) + +nimobjfile = gdb.current_objfile() or gdb.objfiles()[0] +nimobjfile.type_printers = [] + +################################################################################ +##### Type pretty printers +################################################################################ + +type_hash_regex = re.compile("^\w*_([A-Za-z0-9]*)$") + +def getNimRti(type_name): + """ Return a ``gdb.Value`` object for the Nim Runtime Information of ``type_name``. """ + + # Get static const TNimType variable. This should be available for + # every non trivial Nim type. + m = type_hash_regex.match(type_name) + if m: + try: + return gdb.parse_and_eval("NTI_" + m.group(1) + "_") + except: + return None + +class NimTypeRecognizer: + # this type map maps from types that are generated in the C files to + # how they are called in nim. To not mix up the name ``int`` from + # system.nim with the name ``int`` that could still appear in + # generated code, ``NI`` is mapped to ``system.int`` and not just + # ``int``. + + type_map_static = { + 'NI': 'system.int', 'NI8': 'int8', 'NI16': 'int16', 'NI32': 'int32', 'NI64': 'int64', + 'NU': 'uint', 'NU8': 'uint8','NU16': 'uint16', 'NU32': 'uint32', 'NU64': 'uint64', + 'NF': 'float', 'NF32': 'float32', 'NF64': 'float64', + 'NIM_BOOL': 'bool', 'NIM_CHAR': 'char', 'NCSTRING': 'cstring', + 'NimStringDesc': 'string' + } + + # Normally gdb distinguishes between the command `ptype` and + # `whatis`. `ptype` prints a very detailed view of the type, and + # `whatis` a very brief representation of the type. I haven't + # figured out a way to know from the type printer that is + # implemented here how to know if a type printer should print the + # short representation or the long representation. As a hacky + # workaround I just say I am not resposible for printing pointer + # types (seq and string are exception as they are semantically + # values). this way the default type printer will handle pointer + # types and dive into the members of that type. So I can still + # control with `ptype myval` and `ptype *myval` if I want to have + # detail or not. I this this method stinks but I could not figure + # out a better solution. + + object_type_pattern = re.compile("^(\w*):ObjectType$") + + def recognize(self, type_obj): + + tname = None + if type_obj.tag is not None: + tname = type_obj.tag + elif type_obj.name is not None: + tname = type_obj.name + + # handle pointer types + if not tname: + if type_obj.code == gdb.TYPE_CODE_PTR: + target_type = type_obj.target() + target_type_name = target_type.name + if target_type_name: + # visualize 'string' as non pointer type (unpack pointer type). + if target_type_name == "NimStringDesc": + tname = target_type_name # could also just return 'string' + # visualize 'seq[T]' as non pointer type. + if target_type_name.find('tySequence_') == 0: + tname = target_type_name + + if not tname: + # We are not resposible for this type printing. + # Basically this means we don't print pointer types. + return None + + result = self.type_map_static.get(tname, None) + if result: + return result + + rti = getNimRti(tname) + if rti: + return rti['name'].string("utf-8", "ignore") + else: + return None + +class NimTypePrinter: + """Nim type printer. One printer for all Nim types.""" + + + # enabling and disabling of type printers can be done with the + # following gdb commands: + # + # enable type-printer NimTypePrinter + # disable type-printer NimTypePrinter + + name = "NimTypePrinter" + def __init__ (self): + self.enabled = True + + def instantiate(self): + return NimTypeRecognizer() + + +nimobjfile.type_printers = [NimTypePrinter()] + +################################################################################ +##### GDB Function, equivalent of Nim's $ operator +################################################################################ + +class DollarPrintFunction (gdb.Function): + "Nim's equivalent of $ operator as a gdb function, available in expressions `print $dollar(myvalue)" + + _gdb_dollar_functions = gdb.execute("info functions dollar__", True, True) + dollar_functions = re.findall('NimStringDesc \*(dollar__[A-z0-9_]+?)\(([^,)]*)\);', _gdb_dollar_functions) + + def __init__ (self): + super (DollarPrintFunction, self).__init__("dollar") + + @staticmethod + def invoke_static(arg): + + for func, arg_typ in DollarPrintFunction.dollar_functions: + + if arg.type.name == arg_typ: + func_value = gdb.lookup_global_symbol(func, gdb.SYMBOL_FUNCTIONS_DOMAIN).value() + return func_value(arg) + + if arg.type.name + " *" == arg_typ: + func_value = gdb.lookup_global_symbol(func, gdb.SYMBOL_FUNCTIONS_DOMAIN).value() + return func_value(arg.address) + + typeName = arg.type.name + printErrorOnce(typeName, "No suitable Nim $ operator found for type: " + typeName + ".\n") + + def invoke(self, arg): + return self.invoke_static(arg) + +DollarPrintFunction() + +################################################################################ +##### GDB Command, equivalent of Nim's $ operator +################################################################################ + +class DollarPrintCmd (gdb.Command): + """Dollar print command for Nim, `$ expr` will invoke Nim's $ operator""" + + def __init__ (self): + super (DollarPrintCmd, self).__init__ ("$", gdb.COMMAND_DATA, gdb.COMPLETE_EXPRESSION) + + def invoke (self, arg, from_tty): + param = gdb.parse_and_eval(arg) + gdb.write(str(DollarPrintFunction.invoke_static(param)) + "\n", gdb.STDOUT) + +DollarPrintCmd() + +################################################################################ +##### Value pretty printers +################################################################################ + +class NimBoolPrinter: + + pattern = re.compile(r'^NIM_BOOL$') + + def __init__(self, val): + self.val = val + + def to_string(self): + if self.val == 0: + return "false" + else: + return "true" + +################################################################################ + +class NimStringPrinter: + pattern = re.compile(r'^NimStringDesc \*$') + + def __init__(self, val): + self.val = val + + def display_hint(self): + return 'string' + + def to_string(self): + if self.val: + l = int(self.val['Sup']['len']) + return self.val['data'][0].address.string("utf-8", "ignore", l) + else: + return "" + +################################################################################ + +# proc reprEnum(e: int, typ: PNimType): string {.compilerRtl.} = +# ## Return string representation for enumeration values +# var n = typ.node +# if ntfEnumHole notin typ.flags: +# let o = e - n.sons[0].offset +# if o >= 0 and o <% typ.node.len: +# return $n.sons[o].name +# else: +# # ugh we need a slow linear search: +# var s = n.sons +# for i in 0 .. n.len-1: +# if s[i].offset == e: +# return $s[i].name +# result = $e & " (invalid data!)" + +def reprEnum(e, typ): + """ this is a port of the nim runtime function `reprEnum` to python """ + e = int(e) + n = typ["node"] + flags = int(typ["flags"]) + # 1 << 2 is {ntfEnumHole} + if ((1 << 2) & flags) == 0: + o = e - int(n["sons"][0]["offset"]) + if o >= 0 and 0 < int(n["len"]): + return n["sons"][o]["name"].string("utf-8", "ignore") + else: + # ugh we need a slow linear search: + s = n["sons"] + for i in range(0, int(n["len"])): + if int(s[i]["offset"]) == e: + return s[i]["name"].string("utf-8", "ignore") + + return str(e) + " (invalid data!)" + +class NimEnumPrinter: + pattern = re.compile(r'^tyEnum_(\w*)_([A-Za-z0-9]*)$') + + def __init__(self, val): + self.val = val + match = self.pattern.match(self.val.type.name) + self.typeNimName = match.group(1) + typeInfoName = "NTI_" + match.group(2) + "_" + self.nti = gdb.lookup_global_symbol(typeInfoName) + + if self.nti is None: + printErrorOnce(typeInfoName, "NimEnumPrinter: lookup global symbol '" + typeInfoName + " failed for " + self.val.type.name + ".\n") + + def to_string(self): + if self.nti: + arg0 = self.val + arg1 = self.nti.value(gdb.newest_frame()) + return reprEnum(arg0, arg1) + else: + return self.typeNimName + "(" + str(int(self.val)) + ")" + +################################################################################ + +class NimSetPrinter: + ## the set printer is limited to sets that fit in an integer. Other + ## sets are compiled to `NU8 *` (ptr uint8) and are invisible to + ## gdb (currently). + pattern = re.compile(r'^tySet_tyEnum_(\w*)_([A-Za-z0-9]*)$') + + def __init__(self, val): + self.val = val + match = self.pattern.match(self.val.type.name) + self.typeNimName = match.group(1) + + typeInfoName = "NTI_" + match.group(2) + "_" + self.nti = gdb.lookup_global_symbol(typeInfoName) + + if self.nti is None: + printErrorOnce(typeInfoName, "NimSetPrinter: lookup global symbol '"+ typeInfoName +" failed for " + self.val.type.name + ".\n") + + def to_string(self): + if self.nti: + nti = self.nti.value(gdb.newest_frame()) + enumStrings = [] + val = int(self.val) + i = 0 + while val > 0: + if (val & 1) == 1: + enumStrings.append(reprEnum(i, nti)) + val = val >> 1 + i += 1 + + return '{' + ', '.join(enumStrings) + '}' + else: + return str(int(self.val)) + +################################################################################ + +class NimHashSetPrinter: + pattern = re.compile(r'^tyObject_(HashSet)_([A-Za-z0-9]*)$') + + def __init__(self, val): + self.val = val + + def display_hint(self): + return 'array' + + def to_string(self): + counter = 0 + capacity = 0 + if self.val: + counter = int(self.val['counter']) + if self.val['data']: + capacity = int(self.val['data']['Sup']['len']) + + return 'HashSet({0}, {1})'.format(counter, capacity) + + def children(self): + if self.val: + data = NimSeqPrinter(self.val['data']) + for idxStr, entry in data.children(): + if int(entry['Field0']) > 0: + yield ("data." + idxStr + ".Field1", str(entry['Field1'])) + +################################################################################ + +class NimSeqPrinter: + # the pointer is explicity part of the type. So it is part of + # ``pattern``. + pattern = re.compile(r'^tySequence_\w* \*$') + + def __init__(self, val): + self.val = val + + def display_hint(self): + return 'array' + + def to_string(self): + len = 0 + cap = 0 + if self.val: + len = int(self.val['Sup']['len']) + cap = int(self.val['Sup']['reserved']) + + return 'seq({0}, {1})'.format(len, cap) + + def children(self): + if self.val: + length = int(self.val['Sup']['len']) + #align = len(str(length - 1)) + for i in range(length): + yield ("data[{0}]".format(i), self.val["data"][i]) + +################################################################################ + +class NimArrayPrinter: + pattern = re.compile(r'^tyArray_\w*$') + + def __init__(self, val): + self.val = val + + def display_hint(self): + return 'array' + + def to_string(self): + return 'array' + + def children(self): + length = self.val.type.sizeof // self.val[0].type.sizeof + align = len(str(length-1)) + for i in range(length): + yield ("[{0:>{1}}]".format(i, align), self.val[i]) + +################################################################################ + +class NimStringTablePrinter: + pattern = re.compile(r'^tyObject_(StringTableObj)_([A-Za-z0-9]*)(:? \*)?$') + + def __init__(self, val): + self.val = val + + def display_hint(self): + return 'map' + + def to_string(self): + counter = 0 + capacity = 0 + if self.val: + counter = int(self.val['counter']) + if self.val['data']: + capacity = int(self.val['data']['Sup']['len']) + + return 'StringTableObj({0}, {1})'.format(counter, capacity) + + def children(self): + if self.val: + data = NimSeqPrinter(self.val['data']) + for idxStr, entry in data.children(): + if int(entry['Field2']) > 0: + yield (idxStr + ".Field0", entry['Field0']) + yield (idxStr + ".Field1", entry['Field1']) + +################################################################ + +class NimTablePrinter: + pattern = re.compile(r'^tyObject_(Table)_([A-Za-z0-9]*)(:? \*)?$') + + def __init__(self, val): + self.val = val + # match = self.pattern.match(self.val.type.name) + + def display_hint(self): + return 'map' + + def to_string(self): + counter = 0 + capacity = 0 + if self.val: + counter = int(self.val['counter']) + if self.val['data']: + capacity = int(self.val['data']['Sup']['len']) + + return 'Table({0}, {1})'.format(counter, capacity) + + def children(self): + if self.val: + data = NimSeqPrinter(self.val['data']) + for idxStr, entry in data.children(): + if int(entry['Field0']) > 0: + yield (idxStr + '.Field1', entry['Field1']) + yield (idxStr + '.Field2', entry['Field2']) + + +################################################################ + +# this is untested, therefore disabled + +# class NimObjectPrinter: +# pattern = re.compile(r'^tyObject_.*$') + +# def __init__(self, val): +# self.val = val + +# def display_hint(self): +# return 'object' + +# def to_string(self): +# return str(self.val.type) + +# def children(self): +# if not self.val: +# yield "object", "<nil>" +# raise StopIteration + +# for (i, field) in enumerate(self.val.type.fields()): +# if field.type.code == gdb.TYPE_CODE_UNION: +# yield _union_field +# else: +# yield (field.name, self.val[field]) + +# def _union_field(self, i, field): +# rti = getNimRti(self.val.type.name) +# if rti is None: +# return (field.name, "UNION field can't be displayed without RTI") + +# node_sons = rti['node'].dereference()['sons'] +# prev_field = self.val.type.fields()[i - 1] + +# descriminant_node = None +# for i in range(int(node['len'])): +# son = node_sons[i].dereference() +# if son['name'].string("utf-8", "ignore") == str(prev_field.name): +# descriminant_node = son +# break +# if descriminant_node is None: +# raise ValueError("Can't find union descriminant field in object RTI") + +# if descriminant_node is None: raise ValueError("Can't find union field in object RTI") +# union_node = descriminant_node['sons'][int(self.val[prev_field])].dereference() +# union_val = self.val[field] + +# for f1 in union_val.type.fields(): +# for f2 in union_val[f1].type.fields(): +# if str(f2.name) == union_node['name'].string("utf-8", "ignore"): +# return (str(f2.name), union_val[f1][f2]) + +# raise ValueError("RTI is absent or incomplete, can't find union definition in RTI") + + +################################################################################ + +def makematcher(klass): + def matcher(val): + typeName = str(val.type) + try: + if hasattr(klass, 'pattern') and hasattr(klass, '__name__'): + # print(typeName + " <> " + klass.__name__) + if klass.pattern.match(typeName): + return klass(val) + except Exception as e: + print(klass) + printErrorOnce(typeName, "No matcher for type '" + typeName + "': " + str(e) + "\n") + return matcher + +nimobjfile.pretty_printers = [] +nimobjfile.pretty_printers.extend([makematcher(var) for var in list(vars().values()) if hasattr(var, 'pattern')]) |