Files
Fangrui SongandDave Lee aec0da1e94 [utils] Fix DenseMap debugger printers for the packed used-bit array (#201755)
DenseMap no longer use in-band sentinel keys. (#200595 and #201281).
Update the GDB pretty printer and LLDB data formatters to test the used
bit rather than comparing keys.

GDB: advancePastEmptyBuckets relied on DenseMapInfo::getEmptyKey(),
which could not be evaluated in GDB and so was disabled, leaving the printer
to emit empty and erased buckets. It now walks bucket indices and skips any
whose used bit is clear.

LLDB: DenseMapSynthetic used a key-uniqueness heuristic to guess which
buckets were live, which mishandled a lone erased bucket (hence the
former tombstones=1 summary note). It now reads the used array directly,
so erased entries are skipped exactly. NumTombstones no longer exists,
so drop it from the summary.

Written by Claude Opus 4.8

---------

Co-authored-by: Dave Lee <davelee.com@gmail.com>
2026-06-07 19:14:14 -07:00

597 lines
20 KiB
Python

"""
LLDB Formatters for LLVM data types.
Load into LLDB with 'command script import /path/to/lldbDataFormatters.py'
"""
from __future__ import annotations
from typing import Literal, Optional
import lldb
def __lldb_init_module(debugger: lldb.SBDebugger, internal_dict) -> None:
debugger.HandleCommand("type category define -e llvm -l c++")
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.SmallVectorSynthProvider "
'-x "^llvm::SmallVectorImpl<.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
'-e -s "size=${svar%#}" '
'-x "^llvm::SmallVectorImpl<.+>$"'
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.SmallVectorSynthProvider "
'-x "^llvm::SmallVector<.+,.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
'-e -s "size=${svar%#}" '
'-x "^llvm::SmallVector<.+,.+>$"'
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.ArrayRefSynthProvider "
'-x "^llvm::ArrayRef<.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
'-e -s "size=${svar%#}" '
'-x "^llvm::ArrayRef<.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
f"-F {__name__}.SmallStringSummaryProvider "
'-x "^llvm::SmallString<.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
f"-F {__name__}.StringRefSummaryProvider "
"llvm::StringRef"
)
debugger.HandleCommand(
"type summary add -w llvm "
f"-F {__name__}.ConstStringSummaryProvider "
"lldb_private::ConstString"
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.PointerIntPairSynthProvider "
'-x "^llvm::PointerIntPair<.+>$"'
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.PointerUnionSynthProvider "
'-x "^llvm::PointerUnion<.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
f"-e -F {__name__}.DenseMapSummary "
'-x "^llvm::DenseMap<.+>$"'
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.DenseMapSynthetic "
'-x "^llvm::DenseMap<.+>$"'
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.DenseSetSynthetic "
'-x "^llvm::DenseSet<.+>$"'
)
debugger.HandleCommand(
"type synthetic add -w llvm "
f"-l {__name__}.ExpectedSynthetic "
'-x "^llvm::Expected<.+>$"'
)
debugger.HandleCommand(
"type summary add -w llvm "
f"-F {__name__}.SmallBitVectorSummary "
"llvm::SmallBitVector"
)
# Pretty printer for llvm::SmallVector/llvm::SmallVectorImpl
class SmallVectorSynthProvider:
valobj: lldb.SBValue
begin: lldb.SBValue
size: lldb.SBValue
data_type: lldb.SBType
type_size: int
def __init__(self, valobj, internal_dict) -> None:
self.valobj = valobj
self.update() # initialize this provider
def num_children(self) -> int:
return self.size.GetValueAsUnsigned(0)
def get_child_index(self, name):
try:
return int(name.lstrip("[").rstrip("]"))
except:
return -1
def get_child_at_index(self, index) -> Optional[lldb.SBValue]:
# Do bounds checking.
if index < 0:
return None
if index >= self.num_children():
return None
offset = index * self.type_size
return self.begin.CreateChildAtOffset(
"[" + str(index) + "]", offset, self.data_type
)
def update(self):
self.begin = self.valobj.GetChildMemberWithName("BeginX")
self.size = self.valobj.GetChildMemberWithName("Size")
the_type = self.valobj.GetType()
# If this is a reference type we have to dereference it to get to the
# template parameter.
if the_type.IsReferenceType():
the_type = the_type.GetDereferencedType()
if the_type.IsPointerType():
the_type = the_type.GetPointeeType()
self.data_type = the_type.GetTemplateArgumentType(0)
self.type_size = self.data_type.GetByteSize()
assert self.type_size != 0
class ArrayRefSynthProvider:
"""Provider for llvm::ArrayRef"""
valobj: lldb.SBValue
data: lldb.SBValue
length: int
data_type: lldb.SBType
type_size: int
def __init__(self, valobj: lldb.SBValue, internal_dict) -> None:
self.valobj = valobj
self.update() # initialize this provider
def num_children(self) -> int:
return self.length
def get_child_index(self, name):
try:
return int(name.lstrip("[").rstrip("]"))
except:
return -1
def get_child_at_index(self, index) -> Optional[lldb.SBValue]:
if index < 0 or index >= self.num_children():
return None
offset = index * self.type_size
return self.data.CreateChildAtOffset(
"[" + str(index) + "]", offset, self.data_type
)
def update(self):
self.data = self.valobj.GetChildMemberWithName("Data")
length_obj = self.valobj.GetChildMemberWithName("Length")
self.length = length_obj.GetValueAsUnsigned(0)
self.data_type = self.data.GetType().GetPointeeType()
self.type_size = self.data_type.GetByteSize()
assert self.type_size != 0
def SmallStringSummaryProvider(valobj: lldb.SBValue, internal_dict) -> str:
# The underlying SmallVector base class is the first child.
vector = valobj.GetChildAtIndex(0)
num_elements = vector.GetNumChildren()
res = '"'
for i in range(num_elements):
c = vector.GetChildAtIndex(i)
if c:
res += chr(c.GetValueAsUnsigned())
res += '"'
return res
def StringRefSummaryProvider(valobj: lldb.SBValue, internal_dict) -> str:
data_pointer = valobj.GetChildMemberWithName("Data")
length = valobj.GetChildMemberWithName("Length").unsigned
if data_pointer.unsigned == 0 or length == 0:
return '""'
data = data_pointer.deref
# StringRef may be uninitialized with length exceeding available memory,
# potentially causing bad_alloc exceptions. Limit the length to max string summary setting.
limit_obj = valobj.target.debugger.GetSetting("target.max-string-summary-length")
if limit_obj:
length = min(length, limit_obj.GetUnsignedIntegerValue())
# Get a char[N] type, from the underlying char type.
array_type = data.type.GetArrayType(length)
# Cast the char* string data to a char[N] array.
char_array = data.Cast(array_type)
# Use the builtin summary for its support of max-string-summary-length and
# display of non-printable bytes.
return char_array.summary
def ConstStringSummaryProvider(valobj: lldb.SBValue, internal_dict) -> str:
if valobj.GetNumChildren() == 1:
return valobj.GetChildAtIndex(0).GetSummary()
return ""
class PointerIntPairSynthProvider:
valobj: lldb.SBValue
byteorder: Literal["big", "little"]
ptr_size: int
value: lldb.SBValue
pointer_valobj: Optional[lldb.SBValue]
int_valobj: Optional[lldb.SBValue]
def __init__(self, valobj: lldb.SBValue, internal_dict) -> None:
self.valobj = valobj
self.update()
def num_children(self) -> int:
return 2
def get_child_index(self, name: str) -> int:
if name == "Pointer":
return 0
if name == "Int":
return 1
return -1
def _get_raw_value(self) -> Optional[bytes]:
data: lldb.SBData = self.value.GetData()
error = lldb.SBError()
raw_bytes = data.ReadRawData(error, 0, self.ptr_size)
if error.Fail():
return None
return raw_bytes
def _get_pointer(
self, pointer_bit_mask: int, pointer_ty: lldb.SBType
) -> Optional[lldb.SBValue]:
raw_bytes = self._get_raw_value()
if raw_bytes is None:
return
unmasked_pointer = int.from_bytes(raw_bytes, self.byteorder)
pointer_value = unmasked_pointer & pointer_bit_mask
data = lldb.SBData()
data.SetDataFromUInt64Array([pointer_value])
return self.valobj.CreateValueFromData("Pointer", data, pointer_ty)
def _get_int(
self, int_shift: int, int_mask: int, int_ty: lldb.SBType
) -> Optional[lldb.SBValue]:
raw_bytes = self._get_raw_value()
if raw_bytes is None:
return
unmasked_pointer = int.from_bytes(raw_bytes, self.byteorder)
int_value = (unmasked_pointer >> int_shift) & int_mask
data = lldb.SBData()
data.SetDataFromUInt64Array([int_value])
return self.valobj.CreateValueFromData("Int", data, int_ty)
def get_child_at_index(self, index) -> Optional[lldb.SBValue]:
if index == 0:
return self.pointer_valobj
if index == 1:
return self.int_valobj
return None
def update(self):
self.byteorder = (
"big"
if self.valobj.target.GetByteOrder() == lldb.eByteOrderBig
else "little"
)
self.ptr_size = self.valobj.target.GetAddressByteSize()
self.value: lldb.SBValue = self.valobj.GetChildMemberWithName("Value")
if not self.value:
return
valobj_type = self.valobj.GetType()
pointer_ty: lldb.SBType = valobj_type.GetTemplateArgumentType(0)
if not pointer_ty:
return
int_ty: lldb.SBType = valobj_type.GetTemplateArgumentType(2)
if not int_ty:
return
pointer_info = valobj_type.GetTemplateArgumentType(4)
if not pointer_info:
return
mask_and_shift_constants = pointer_info.FindDirectNestedType(
"MaskAndShiftConstants"
).GetEnumMembers()
# FIXME: SBAPI should provide a way to retrieve an enum member
# by name.
pointer_bit_mask: lldb.SBTypeEnumMember = (
mask_and_shift_constants.GetTypeEnumMemberAtIndex(0)
)
if pointer_bit_mask.name != "PointerBitMask":
return
int_shift: lldb.SBTypeEnumMember = (
mask_and_shift_constants.GetTypeEnumMemberAtIndex(1)
)
if int_shift.name != "IntShift":
return
int_mask: lldb.SBTypeEnumMember = (
mask_and_shift_constants.GetTypeEnumMemberAtIndex(2)
)
if int_mask.name != "IntMask":
return
self.pointer_valobj = self._get_pointer(
pointer_bit_mask.GetValueAsUnsigned(), pointer_ty
)
self.int_valobj = self._get_int(
int_shift.GetValueAsUnsigned(), int_mask.GetValueAsUnsigned(), int_ty
)
class PointerUnionSynthProvider:
valobj: lldb.SBValue
pointer_valobj: lldb.SBValue
def __init__(self, valobj: lldb.SBValue, internal_dict) -> None:
self.valobj = valobj
self.update()
def num_children(self) -> int:
return 1
def get_child_index(self, name: str) -> int:
if name == "Pointer":
return 0
return -1
def get_child_at_index(self, index: int) -> Optional[lldb.SBValue]:
if index != 0:
return None
return self.pointer_valobj
@staticmethod
def _get_low_bits_for_type(ty: lldb.SBType) -> int:
"""Return NumLowBitsAvailable for a pointer type (from pointee byte alignment)."""
pointee = ty.GetPointeeType()
if pointee.IsValid():
align = pointee.GetByteAlign()
return align.bit_length() - 1 if align > 0 else 0
return 0
def _make_pointer_value(self, name, pointer, ty):
"""Create an SBValue for a pointer, using proper byte order and address size."""
data = lldb.SBData.CreateDataFromUInt64Array(
self.valobj.target.GetByteOrder(),
self.valobj.process.GetAddressByteSize(),
[pointer],
)
return self.valobj.CreateValueFromData(name, data, ty)
def update(self):
self.pointer_valobj = None
valobj_type = self.valobj.GetType()
num_args = valobj_type.GetNumberOfTemplateArguments()
if num_args == 0:
return
# Read the raw uintptr_t from PunnedPointer<void*>.
val: lldb.SBValue = self.valobj.GetChildMemberWithName("Val")
if not val:
return
byteorder = (
"big"
if self.valobj.target.GetByteOrder() == lldb.eByteOrderBig
else "little"
)
raw_bytes = val.GetData().uint8s
if not raw_bytes:
return
raw_value = int.from_bytes(raw_bytes, byteorder)
# Compute tag from type alignments (fixed-width encoding).
tag_bits = (num_args - 1).bit_length()
min_low_bits = min(
self._get_low_bits_for_type(valobj_type.GetTemplateArgumentType(i))
for i in range(num_args)
)
if tag_bits > min_low_bits:
return self._set_raw_pointer(raw_value, min_low_bits)
tag_shift = min_low_bits - tag_bits
tag_mask = (1 << tag_bits) - 1
active_tag = (raw_value >> tag_shift) & tag_mask
if active_tag >= num_args:
return self._set_raw_pointer(raw_value, min_low_bits)
active_type: lldb.SBType = valobj_type.GetTemplateArgumentType(active_tag)
if not active_type:
return self._set_raw_pointer(raw_value, min_low_bits)
# Clear the active type's low bits to recover the pointer.
low_bits = self._get_low_bits_for_type(active_type)
pointer = raw_value & ~((1 << low_bits) - 1)
self.pointer_valobj = self._make_pointer_value("Pointer", pointer, active_type)
def _set_raw_pointer(self, raw_value, min_low_bits):
"""Fallback: strip tag bits and show as void* when active type is unknown."""
pointer = raw_value & ~((1 << min_low_bits) - 1)
void_ptr_ty = self.valobj.target.FindFirstType("void").GetPointerType()
if void_ptr_ty.IsValid():
self.pointer_valobj = self._make_pointer_value(
"Pointer", pointer, void_ptr_ty
)
def DenseMapSummary(valobj: lldb.SBValue, _) -> str:
raw_value = valobj.GetNonSyntheticValue()
num_entries = raw_value.GetChildMemberWithName("NumEntries").unsigned
return f"size={num_entries}"
class DenseMapSynthetic:
valobj: lldb.SBValue
# The indexes into `Buckets` that contain valid map entries.
child_buckets: list[int]
def __init__(self, valobj: lldb.SBValue, _) -> None:
self.valobj = valobj
def num_children(self) -> int:
return len(self.child_buckets)
def get_child_at_index(self, child_index: int) -> lldb.SBValue:
bucket_index = self.child_buckets[child_index]
entry = self.valobj.GetValueForExpressionPath(f".Buckets[{bucket_index}]")
# By default, DenseMap instances use DenseMapPair to hold key-value
# entries. When the entry is a DenseMapPair, unwrap it to expose the
# children as simple std::pair values.
#
# This entry type is customizable (a template parameter). For other
# types, expose the entry type as is.
if entry.type.name.startswith("llvm::detail::DenseMapPair<"):
entry = entry.GetChildAtIndex(0)
return entry.Clone(f"[{child_index}]")
def update(self):
self.child_buckets = []
num_entries = self.valobj.GetChildMemberWithName("NumEntries").unsigned
if num_entries == 0:
return
num_buckets = self.valobj.GetChildMemberWithName("NumBuckets").unsigned
used = self.valobj.GetChildMemberWithName("Used")
# Occupancy is tracked in a packed 1-bit-per-bucket "used" array of
# uint32_t words. A bucket holds a valid entry iff its bit is set;
# empty and erased buckets are clear. Read the whole array in one go
# rather than fetching each word with a separate expression path.
num_words = (num_buckets + 31) // 32
words = used.GetPointeeData(0, num_words).uint32s
for index in range(num_buckets):
if (words[index >> 5] >> (index & 31)) & 1:
self.child_buckets.append(index)
class DenseSetSynthetic:
valobj: lldb.SBValue
map: lldb.SBValue
def __init__(self, valobj: lldb.SBValue, _) -> None:
self.valobj = valobj
def num_children(self) -> int:
return self.map.num_children
def get_child_at_index(self, idx: int) -> lldb.SBValue:
map_entry = self.map.child[idx]
set_entry = map_entry.GetChildAtIndex(0)
return set_entry.Clone(f"[{idx}]")
def update(self):
raw_map = self.valobj.GetChildMemberWithName("TheMap")
self.map = raw_map.GetSyntheticValue()
class ExpectedSynthetic:
# The llvm::Expected<T> value.
expected: lldb.SBValue
# The stored success value or error value.
stored_value: lldb.SBValue
def __init__(self, valobj: lldb.SBValue, _) -> None:
self.expected = valobj
def update(self) -> None:
has_error = self.expected.GetChildMemberWithName("HasError").unsigned
if not has_error:
name = "value"
member = "TStorage"
else:
name = "error"
member = "ErrorStorage"
# Anonymous union.
union = self.expected.child[0]
storage = union.GetChildMemberWithName(member)
# For reference types, storage is std::reference_wrapper<T>, so we
# unwrap to get T. For non-reference types, storage is T directly.
# Use GetCanonicalType() to resolve the typedef to the underlying type.
canonical_type_name = storage.type.GetCanonicalType().name
if "reference_wrapper<" in canonical_type_name:
# reference_wrapper<T> stores a T* pointer to the referenced value.
# Get the first child (the pointer member) and dereference it.
ptr_member = storage.GetChildAtIndex(0)
if ptr_member and ptr_member.IsValid() and ptr_member.type.IsPointerType():
self.stored_value = ptr_member.Dereference().Clone(name)
else:
# Fallback: just use storage as-is.
self.stored_value = storage.Clone(name)
else:
self.stored_value = storage.Clone(name)
def num_children(self) -> int:
return 1
def get_child_index(self, name: str) -> int:
if name == self.stored_value.name:
return 0
# Allow dereferencing for values, not errors.
if name == "$$dereference$$" and self.stored_value.name == "value":
return 0
return -1
def get_child_at_index(self, idx: int) -> lldb.SBValue:
if idx == 0:
return self.stored_value
return lldb.SBValue()
def SmallBitVectorSummary(valobj, _) -> str:
underlyingValue = valobj.GetChildMemberWithName("X").unsigned
numBaseBits = valobj.target.addr_size * 8
smallNumRawBits = numBaseBits - 1
smallNumSizeBits = None
if numBaseBits == 32:
smallNumSizeBits = 5
elif numBaseBits == 64:
smallNumSizeBits = 6
else:
smallNumSizeBits = smallNumRawBits
smallNumDataBits = smallNumRawBits - smallNumSizeBits
# If our underlying value is not small, print we can not dump large values.
isSmallMask = 1
if underlyingValue & isSmallMask == 0:
return "<can not read large SmallBitVector>"
smallRawBits = underlyingValue >> 1
smallSize = smallRawBits >> smallNumDataBits
bits = smallRawBits & ((1 << (smallSize + 1)) - 1)
# format `bits` in binary (b), with 0 padding, of width `smallSize`, and left aligned (>)
return f"[{bits:0>{smallSize}b}]"