| # 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::Slice<.+>$', True) |
| |
| attach_synthetic_to_type(UtilsVectorPrinter, r'^tint::Vector<.+>$', True) |
| |
| attach_synthetic_to_type(UtilsVectorRefPrinter, r'^tint::VectorRef<.+>$', |
| True) |
| |
| attach_synthetic_to_type(UtilsHashsetPrinter, r'^tint::Hashset<.+>$', True) |
| |
| attach_synthetic_to_type(UtilsHashmapPrinter, r'^tint::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::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::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::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 UtilsHashmapBasePrinter(Printer): |
| '''Base Printer for HashmapBase-derived types''' |
| |
| 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) |
| entry = member(v, 'entry') |
| |
| # entry is a std::optional, let's try to extract its value for display |
| kvp = self.try_read_std_optional_func(slot, entry) |
| if kvp is None: |
| # If we failed, just output the slot and entry as is, which will use |
| # the default printer for std::optional. |
| kvp = slot, entry |
| |
| return kvp[1].CreateChildAtOffset('[{}]'.format(kvp[0]), 0, kvp[1].GetType()) |
| |
| def try_read_std_optional(self, slot, entry): |
| return None |
| |
| |
| class UtilsHashsetPrinter(UtilsHashmapBasePrinter): |
| '''Printer for Hashset<T, N, HASH, EQUAL>''' |
| |
| def try_read_std_optional(self, slot, entry): |
| try: |
| # libc++ |
| v = entry.EvaluateExpression('__val_') |
| if v.name is not None: |
| return slot, v |
| |
| # libstdc++ |
| v = entry.EvaluateExpression('_M_payload._M_payload._M_value') |
| if v.name is not None: |
| return slot, v |
| return None |
| except: |
| return None |
| |
| |
| class UtilsHashmapPrinter(UtilsHashsetPrinter): |
| '''Printer for Hashmap<K, V, N, HASH, EQUAL>''' |
| |
| def try_read_std_optional(self, slot, entry): |
| try: |
| # libc++ |
| val = entry.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 = entry.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 |
| return None |
| except: |
| return None |