diff --git a/Doxyfile b/Doxyfile index fa056842bb..0866a538c3 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1008,7 +1008,8 @@ RECURSIVE = YES # 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 0000000000..2dce446f3c --- /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''' + + 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''' + + 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''' + + 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''' + + 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''' + + 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 0000000000..85a93ff6c7 --- /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''' + + 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''' + + 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''' + + 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''' + + 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''' + + 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)