tint: add pretty printers for gdb and lldb

Currently supports pretty printing of:
- tint::Utils::Vector, VectorRef, and Slice
- tint::Utils::Hashset, Hashmap

Change-Id: Ifbf2547b0f87d7fde8d9ff0dd458aa35c5fd57f4
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/106720
Reviewed-by: dan sinclair <dsinclair@google.com>
Reviewed-by: Dan Sinclair <dsinclair@chromium.org>
Commit-Queue: Antonio Maiorano <amaiorano@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/Doxyfile b/Doxyfile
index fa05684..0866a53 100644
--- a/Doxyfile
+++ b/Doxyfile
@@ -1008,7 +1008,8 @@
 # Note that relative paths are relative to the directory from which doxygen is
 # run.
 
-EXCLUDE                =
+EXCLUDE                = src/tint/tint_gdb.py \
+                         src/tint/tint_lldb.py
 
 # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
 # directories that are symbolic links (a Unix file system feature) are excluded
diff --git a/src/tint/tint_gdb.py b/src/tint/tint_gdb.py
new file mode 100644
index 0000000..2dce446
--- /dev/null
+++ b/src/tint/tint_gdb.py
@@ -0,0 +1,249 @@
+# Copyright 2022 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Pretty printers for the Tint project.
+# Add a line to your ~/.gdbinit to source this file, e.g.:
+#
+# source /path/to/dawn/src/tint/tint_gdb.py
+
+import gdb
+import gdb.printing
+from itertools import chain
+
+# When debugging this module, set _DEBUGGING = True so that re-sourcing this file in gdb replaces
+# the existing printers.
+_DEBUGGING = True
+
+# Enable to display other data members along with child elements of compound data types (arrays, etc.).
+# This is useful in debuggers like VS Code that doesn't display the `to_string()` result in the watch window.
+# OTOH, it's less useful when using gdb/lldb's print command.
+_DISPLAY_MEMBERS_AS_CHILDREN = False
+
+
+# Tips for debugging using VS Code:
+# - Set a breakpoint where you can view the types you want to debug/write pretty printers for.
+# - Debug Console: source /path/to/dawn/src/tint/tint_gdb.py
+# - To execute Python code, in the Debug Console:
+#   -exec python foo = gdb.parse_and_eval('map.set_')
+#   -exec python v = (foo['slots_']['impl_']['slice']['data'] + 8).dereference()['value']
+#
+# - Useful docs:
+#   Python API: https://sourceware.org/gdb/onlinedocs/gdb/Python-API.html#Python-API
+#   Especially:
+#     Types: https://sourceware.org/gdb/onlinedocs/gdb/Types-In-Python.html#Types-In-Python
+#     Values: https://sourceware.org/gdb/onlinedocs/gdb/Values-From-Inferior.html#Values-From-Inferior
+
+
+pp_set = gdb.printing.RegexpCollectionPrettyPrinter("tint")
+
+
+class Printer(object):
+    '''Base class for Printers'''
+
+    def __init__(self, val):
+        self.val = val
+
+    def template_type(self, index):
+        '''Returns template type at index'''
+        return self.val.type.template_argument(index)
+
+
+class UtilsSlicePrinter(Printer):
+    '''Printer for tint::utils::Slice<T>'''
+
+    def __init__(self, val):
+        super(UtilsSlicePrinter, self).__init__(val)
+        self.len = self.val['len']
+        self.cap = self.val['cap']
+        self.data = self.val['data']
+        self.elem_type = self.data.type.target().unqualified()
+
+    def length(self):
+        return self.len
+
+    def value_at(self, index):
+        '''Returns array value at index'''
+        return (self.data + index).dereference().cast(self.elem_type)
+
+    def to_string(self):
+        return 'length={} capacity={}'.format(self.len, self.cap)
+
+    def members(self):
+        if _DISPLAY_MEMBERS_AS_CHILDREN:
+            return [
+                ('length', self.len),
+                ('capacity', self.cap),
+            ]
+        else:
+            return []
+
+    def children(self):
+        for m in self.members():
+            yield m
+        for i in range(self.len):
+            yield str(i), self.value_at(i)
+
+    def display_hint(self):
+        return 'array'
+
+
+pp_set.add_printer('UtilsSlicePrinter',
+                   '^tint::utils::Slice<.*>$', UtilsSlicePrinter)
+
+
+class UtilsVectorPrinter(Printer):
+    '''Printer for tint::utils::Vector<T, N>'''
+
+    def __init__(self, val):
+        super(UtilsVectorPrinter, self).__init__(val)
+        self.slice = self.val['impl_']['slice']
+        self.using_heap = self.slice['cap'] > self.template_type(1)
+
+    def slice_printer(self):
+        return UtilsSlicePrinter(self.slice)
+
+    def to_string(self):
+        return 'heap={} {}'.format(self.using_heap, self.slice)
+
+    def members(self):
+        if _DISPLAY_MEMBERS_AS_CHILDREN:
+            return [
+                ('heap', self.using_heap),
+            ]
+        else:
+            return []
+
+    def children(self):
+        return chain(self.members(), self.slice_printer().children())
+
+    def display_hint(self):
+        return 'array'
+
+
+pp_set.add_printer(
+    'UtilsVector', '^tint::utils::Vector<.*>$', UtilsVectorPrinter)
+
+
+class UtilsVectorRefPrinter(Printer):
+    '''Printer for tint::utils::VectorRef<T>'''
+
+    def __init__(self, val):
+        super(UtilsVectorRefPrinter, self).__init__(val)
+        self.slice = self.val['slice_']
+        self.can_move = self.val['can_move_']
+
+    def to_string(self):
+        return 'can_move={} {}'.format(self.can_move, self.slice)
+
+    def members(self):
+        if _DISPLAY_MEMBERS_AS_CHILDREN:
+            return [
+                ('can_move', self.can_move),
+            ]
+        else:
+            return []
+
+    def children(self):
+        return chain(self.members(), UtilsSlicePrinter(self.slice).children())
+
+    def display_hint(self):
+        return 'array'
+
+
+pp_set.add_printer(
+    'UtilsVector', '^tint::utils::VectorRef<.*>$', UtilsVectorRefPrinter)
+
+
+class UtilsHashsetPrinter(Printer):
+    '''Printer for Hashset<T, N, HASH, EQUAL>'''
+
+    def __init__(self, val):
+        super(UtilsHashsetPrinter, self).__init__(val)
+        self.slice = UtilsVectorPrinter(self.val['slots_']).slice_printer()
+        self.try_read_std_optional_func = self.try_read_std_optional
+
+    def to_string(self):
+        length = 0
+        for slot in range(0, self.slice.length()):
+            v = self.slice.value_at(slot)
+            if v['hash'] != 0:
+                length += 1
+        return 'length={}'.format(length)
+
+    def children(self):
+        for slot in range(0, self.slice.length()):
+            v = self.slice.value_at(slot)
+            if v['hash'] != 0:
+                value = v['value']
+
+                # value is a std::optional, let's try to extract its value for display
+                kvp = self.try_read_std_optional_func(slot, value)
+                if kvp is None:
+                    # If we failed, just output the slot and value as is, which will use
+                    # the default visualizer for each.
+                    kvp = slot, value
+
+                yield str(kvp[0]), kvp[1]
+
+    def display_hint(self):
+        return 'array'
+
+    def try_read_std_optional(self, slot, value):
+        try:
+            # libstdc++
+            v = value['_M_payload']['_M_payload']['_M_value']
+            return slot, v
+            # return str(kvp['key']), kvp['value']
+        except:
+            return None
+
+
+pp_set.add_printer(
+    'UtilsHashset', '^tint::utils::Hashset<.*>$', UtilsHashsetPrinter)
+
+
+class UtilsHashmapPrinter(Printer):
+    '''Printer for Hashmap<K, V, N, HASH, EQUAL>'''
+
+    def __init__(self, val):
+        super(UtilsHashmapPrinter, self).__init__(val)
+        self.hash_set = UtilsHashsetPrinter(self.val['set_'])
+        # Replace the lookup function so we can extract the key and value out of the std::optionals in the Hashset
+        self.hash_set.try_read_std_optional_func = self.try_read_std_optional
+
+    def to_string(self):
+        return self.hash_set.to_string()
+
+    def children(self):
+        return self.hash_set.children()
+
+    def display_hint(self):
+        return 'array'
+
+    def try_read_std_optional(self, slot, value):
+        try:
+            # libstdc++
+            kvp = value['_M_payload']['_M_payload']['_M_value']
+            return str(kvp['key']), kvp['value']
+        except:
+            pass
+        # Failed, fall back on hash_set
+        return self.hash_set.try_read_std_optional(slot, value)
+
+
+pp_set.add_printer(
+    'UtilsHashmap', '^tint::utils::Hashmap<.*>$', UtilsHashmapPrinter)
+
+
+gdb.printing.register_pretty_printer(gdb, pp_set, replace=_DEBUGGING)
diff --git a/src/tint/tint_lldb.py b/src/tint/tint_lldb.py
new file mode 100644
index 0000000..85a93ff
--- /dev/null
+++ b/src/tint/tint_lldb.py
@@ -0,0 +1,397 @@
+# Copyright 2022 The Tint Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Pretty printers for the Tint project.
+#
+# If using lldb from command line, add a line to your ~/.lldbinit to import the printers:
+#
+#    command script import /path/to/dawn/src/tint/tint_lldb.py
+#
+#
+# If using VS Code on MacOS with the Microsoft C/C++ extension, add the following to
+# your launch.json (make sure you specify an absolute path to tint_lldb.py):
+#
+#    "name": "Launch",
+#    "type": "cppdbg",
+#    "request": "launch",
+#    ...
+#    "setupCommands": [
+#        {
+#            "description": "Load tint pretty printers",
+#            "ignoreFailures": false,
+#            "text": "command script import /path/to/dawn/src/tint/tint_lldb.py,
+#        }
+#    ]
+#
+# If using VS Code with the CodeLLDB extension (https://github.com/vadimcn/vscode-lldb),
+# add the following to your launch.json:
+#
+#    "name": "Launch",
+#    "type": "lldb",
+#    "request": "launch",
+#    ...
+#    "initCommands": [
+#        "command script import /path/to/dawn/src/tint/tint_lldb.py"
+#    ]
+
+# Based on pretty printers for:
+# Rust: https://github.com/vadimcn/vscode-lldb/blob/master/formatters/rust.py
+# Dlang: https://github.com/Pure-D/dlang-debug/blob/master/lldb_dlang.py
+#
+#
+# Tips for debugging using VS Code:
+#
+# - Set a breakpoint where you can view the types you want to debug/write pretty printers for.
+# - Debug Console: -exec command script import /path/to/dawn/src/tint/tint_lldb.py
+# - You can re-run the above command to reload the printers after modifying the python script.
+
+# - Useful docs:
+#   Formattesr: https://lldb.llvm.org/use/variable.html
+#   Python API: https://lldb.llvm.org/python_api.html
+#   Especially:
+#     SBType: https://lldb.llvm.org/python_api/lldb.SBType.html
+#     SBValue: https://lldb.llvm.org/python_api/lldb.SBValue.html
+
+from __future__ import print_function, division
+import sys
+import logging
+import re
+import lldb
+import types
+
+if sys.version_info[0] == 2:
+    # python2-based LLDB accepts utf8-encoded ascii strings only.
+    def to_lldb_str(s): return s.encode(
+        'utf8', 'backslashreplace') if isinstance(s, unicode) else s
+    range = xrange
+else:
+    to_lldb_str = str
+
+string_encoding = "escape"  # remove | unicode | escape
+
+log = logging.getLogger(__name__)
+
+module = sys.modules[__name__]
+tint_category = None
+
+
+def __lldb_init_module(debugger, dict):
+    global tint_category
+
+    tint_category = debugger.CreateCategory('tint')
+    tint_category.SetEnabled(True)
+
+    attach_synthetic_to_type(
+        UtilsSlicePrinter, r'^tint::utils::Slice<.+>$', True)
+
+    attach_synthetic_to_type(
+        UtilsVectorPrinter, r'^tint::utils::Vector<.+>$', True)
+
+    attach_synthetic_to_type(
+        UtilsVectorRefPrinter, r'^tint::utils::VectorRef<.+>$', True)
+
+    attach_synthetic_to_type(
+        UtilsHashsetPrinter, r'^tint::utils::Hashset<.+>$', True)
+
+    attach_synthetic_to_type(
+        UtilsHashmapPrinter, r'^tint::utils::Hashmap<.+>$', True)
+
+
+def attach_synthetic_to_type(synth_class, type_name, is_regex=False):
+    global module, tint_category
+    synth = lldb.SBTypeSynthetic.CreateWithClassName(
+        __name__ + '.' + synth_class.__name__)
+    synth.SetOptions(lldb.eTypeOptionCascade)
+    ret = tint_category.AddTypeSynthetic(
+        lldb.SBTypeNameSpecifier(type_name, is_regex), synth)
+    log.debug('attaching synthetic %s to "%s", is_regex=%s -> %s',
+              synth_class.__name__, type_name, is_regex, ret)
+
+    def summary_fn(valobj, dict): return get_synth_summary(
+        synth_class, valobj, dict)
+    # LLDB accesses summary fn's by name, so we need to create a unique one.
+    summary_fn.__name__ = '_get_synth_summary_' + synth_class.__name__
+    setattr(module, summary_fn.__name__, summary_fn)
+    attach_summary_to_type(summary_fn, type_name, is_regex)
+
+
+def attach_summary_to_type(summary_fn, type_name, is_regex=False):
+    global module, tint_category
+    summary = lldb.SBTypeSummary.CreateWithFunctionName(
+        __name__ + '.' + summary_fn.__name__)
+    summary.SetOptions(lldb.eTypeOptionCascade)
+    ret = tint_category.AddTypeSummary(
+        lldb.SBTypeNameSpecifier(type_name, is_regex), summary)
+    log.debug('attaching summary %s to "%s", is_regex=%s -> %s',
+              summary_fn.__name__, type_name, is_regex, ret)
+
+
+def get_synth_summary(synth_class, valobj, dict):
+    ''''
+    get_summary' is annoyingly not a part of the standard LLDB synth provider API.
+    This trick allows us to share data extraction logic between synth providers and their sibling summary providers.
+    '''
+    synth = synth_class(valobj.GetNonSyntheticValue(), dict)
+    synth.update()
+    summary = synth.get_summary()
+    return to_lldb_str(summary)
+
+
+def member(valobj, *chain):
+    '''Performs chained GetChildMemberWithName lookups'''
+    for name in chain:
+        valobj = valobj.GetChildMemberWithName(name)
+    return valobj
+
+
+class Printer(object):
+    '''Base class for Printers'''
+
+    def __init__(self, valobj, dict={}):
+        self.valobj = valobj
+        self.initialize()
+
+    def initialize(self):
+        return None
+
+    def update(self):
+        return False
+
+    def num_children(self):
+        return 0
+
+    def has_children(self):
+        return False
+
+    def get_child_at_index(self, index):
+        return None
+
+    def get_child_index(self, name):
+        return None
+
+    def get_summary(self):
+        return None
+
+    def member(self, *chain):
+        '''Performs chained GetChildMemberWithName lookups'''
+        return member(self.valobj, *chain)
+
+    def template_params(self):
+        '''Returns list of template params values (as strings)'''
+        type_name = self.valobj.GetTypeName()
+        params = []
+        level = 0
+        start = 0
+        for i, c in enumerate(type_name):
+            if c == '<':
+                level += 1
+                if level == 1:
+                    start = i + 1
+            elif c == '>':
+                level -= 1
+                if level == 0:
+                    params.append(type_name[start:i].strip())
+            elif c == ',' and level == 1:
+                params.append(type_name[start:i].strip())
+                start = i + 1
+        return params
+
+    def template_param_at(self, index):
+        '''Returns template param value at index (as string)'''
+        return self.template_params()[index]
+
+
+class UtilsSlicePrinter(Printer):
+    '''Printer for tint::utils::Slice<T>'''
+
+    def initialize(self):
+        self.len = self.valobj.GetChildMemberWithName('len')
+        self.cap = self.valobj.GetChildMemberWithName('cap')
+        self.data = self.valobj.GetChildMemberWithName('data')
+        self.elem_type = self.data.GetType().GetPointeeType()
+        self.elem_size = self.elem_type.GetByteSize()
+
+    def get_summary(self):
+        return 'length={} capacity={}'.format(self.len.GetValueAsUnsigned(), self.cap.GetValueAsUnsigned())
+
+    def num_children(self):
+        # NOTE: VS Code on MacOS hangs if we try to expand something too large, so put an artificial limit
+        # until we can figure out how to know if this is a valid instance.
+        return min(self.len.GetValueAsUnsigned(), 256)
+
+    def has_children(self):
+        return True
+
+    def get_child_at_index(self, index):
+        try:
+            if not 0 <= index < self.num_children():
+                return None
+            # TODO: return self.value_at(index)
+            offset = index * self.elem_size
+            return self.data.CreateChildAtOffset('[%s]' % index, offset, self.elem_type)
+        except Exception as e:
+            log.error('%s', e)
+            raise
+
+    def value_at(self, index):
+        '''Returns array value at index'''
+        offset = index * self.elem_size
+        return self.data.CreateChildAtOffset('[%s]' % index, offset, self.elem_type)
+
+
+class UtilsVectorPrinter(Printer):
+    '''Printer for tint::utils::Vector<T, N>'''
+
+    def initialize(self):
+        self.slice = self.member('impl_', 'slice')
+        self.slice_printer = UtilsSlicePrinter(self.slice)
+        self.fixed_size = int(self.template_param_at(1))
+        self.cap = self.slice_printer.member('cap')
+
+    def get_summary(self):
+        using_heap = self.cap.GetValueAsUnsigned() > self.fixed_size
+        return 'heap={} {}'.format(using_heap, self.slice_printer.get_summary())
+
+    def num_children(self):
+        return self.slice_printer.num_children()
+
+    def has_children(self):
+        return self.slice_printer.has_children()
+
+    def get_child_at_index(self, index):
+        return self.slice_printer.get_child_at_index(index)
+
+    def make_slice_printer(self):
+        return UtilsSlicePrinter(self.slice)
+
+
+class UtilsVectorRefPrinter(Printer):
+    '''Printer for tint::utils::VectorRef<T>'''
+
+    def initialize(self):
+        self.slice = self.member('slice_')
+        self.slice_printer = UtilsSlicePrinter(self.slice)
+        self.can_move = self.member('can_move_')
+
+    def get_summary(self):
+        return 'can_move={} {}'.format(self.can_move.GetValue(), self.slice_printer.get_summary())
+
+    def num_children(self):
+        return self.slice_printer.num_children()
+
+    def has_children(self):
+        return self.slice_printer.has_children()
+
+    def get_child_at_index(self, index):
+        return self.slice_printer.get_child_at_index(index)
+
+
+class UtilsHashsetPrinter(Printer):
+    '''Printer for Hashset<T, N, HASH, EQUAL>'''
+
+    def initialize(self):
+        self.slice = UtilsVectorPrinter(
+            self.member('slots_')).make_slice_printer()
+
+        self.try_read_std_optional_func = self.try_read_std_optional
+
+    def update(self):
+        self.valid_slots = []
+        for slot in range(0, self.slice.num_children()):
+            v = self.slice.value_at(slot)
+            if member(v, 'hash').GetValueAsUnsigned() != 0:
+                self.valid_slots.append(slot)
+        return False
+
+    def get_summary(self):
+        return 'length={}'.format(self.num_children())
+
+    def num_children(self):
+        return len(self.valid_slots)
+
+    def has_children(self):
+        return True
+
+    def get_child_at_index(self, index):
+        slot = self.valid_slots[index]
+        v = self.slice.value_at(slot)
+        value = member(v, 'value')
+
+        # value is a std::optional, let's try to extract its value for display
+        kvp = self.try_read_std_optional_func(slot, value)
+        if kvp is None:
+            # If we failed, just output the slot and value as is, which will use
+            # the default printer for std::optional.
+            kvp = slot, value
+
+        return kvp[1].CreateChildAtOffset('[{}]'.format(kvp[0]), 0, kvp[1].GetType())
+
+    def try_read_std_optional(self, slot, value):
+        try:
+            # libc++
+            v = value.EvaluateExpression('__val_')
+            if v.name is not None:
+                return slot, v
+
+            # libstdc++
+            v = value.EvaluateExpression('_M_payload._M_payload._M_value')
+            if v.name is not None:
+                return slot, v
+            return None
+        except:
+            return None
+
+
+class UtilsHashmapPrinter(Printer):
+    '''Printer for Hashmap<K, V, N, HASH, EQUAL>'''
+
+    def initialize(self):
+        self.hash_set = UtilsHashsetPrinter(self.member('set_'))
+        # Replace the lookup function so we can extract the key and value out of the std::optionals in the Hashset
+        self.hash_set.try_read_std_optional_func = self.try_read_std_optional
+
+    def update(self):
+        self.hash_set.update()
+
+    def get_summary(self):
+        return self.hash_set.get_summary()
+
+    def num_children(self):
+        return self.hash_set.num_children()
+
+    def has_children(self):
+        return self.hash_set.has_children()
+
+    def get_child_at_index(self, index):
+        return self.hash_set.get_child_at_index(index)
+
+    def try_read_std_optional(self, slot, value):
+        try:
+            # libc++
+            val = value.EvaluateExpression('__val_')
+            k = val.EvaluateExpression('key')
+            v = val.EvaluateExpression('value')
+            if k.name is not None and v.name is not None:
+                return k.GetValue(), v
+
+            # libstdc++
+            val = value.EvaluateExpression('_M_payload._M_payload._M_value')
+            k = val.EvaluateExpression('key')
+            v = val.EvaluateExpression('value')
+            if k.name is not None and v.name is not None:
+                return k.GetValue(), v
+        except:
+            pass
+        # Failed, fall back on hash_set
+        return self.hash_set.try_read_std_optional(slot, value)