summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rwxr-xr-xbin/nim-gdb18
-rw-r--r--compiler/astalgo.nim6
-rw-r--r--tests/untestable/gdb/gdb_pretty_printer_test.py33
-rw-r--r--tests/untestable/gdb/gdb_pretty_printer_test_output.txt3
-rw-r--r--tests/untestable/gdb/gdb_pretty_printer_test_program.nim53
-rwxr-xr-xtests/untestable/gdb/gdb_pretty_printer_test_run.sh15
-rw-r--r--tools/nim-gdb.py513
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')])