[lldb][ADT] Fix LLDB/GDB formatters for PointerUnion after recactoring (#188483)

In #188242, we replaced `PointerUnion`'s `PointerIntPair` storage with
`PunnedPointer<void*>`. The old formatters relied on the PIP synthetic
provider (LLDB) / `get_pointer_int_pair helper` (GDB) which no longer
work.

Instead, read raw bytes from `PunnedPointer` and compute the active tag
from template argument type alignments -- the same fixed-width encoding
the C++ implementation uses. When template arg enumeration is truncated
(e.g., function-local types in GDB), the formatters fall back to showing
a tag-stripped `void*` instead of silently misdecoding.

Alternatives that didn't work out:
- Adding a C++ helper (`getActiveMemberIdx`) callable from Python: gets
optimized out even with `__attribute__((used, noinline))`, and
expression evaluation fails for synthetic children.
- Using `isa`/`dyn_cast` checks from Python: requires expression
evaluation, which does not work for local types or synthetic children
without a frame context.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jakub Kuderski 2026-03-25 10:16:01 -04:00 committed by GitHub
parent ca9ac0e24a
commit 9c6054d29d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 156 additions and 31 deletions

View File

@ -33,8 +33,8 @@ llvm::PointerIntPair<int *, 1> PointerIntPair(IntPtr, 1);
struct alignas(8) Z {};
llvm::PointerUnion<Z *, int *> PointerUnion(IntPtr);
// No members which instantiate PointerUnionUIntTraits<Z *> (e.g. get<T *>())
// are called, and this instance will therefore be raw-printed.
// The PunnedPointer-based formatter can decode all PointerUnion instances
// from type alignments, regardless of member template instantiation.
llvm::PointerUnion<Z *, float *> RawPrintingPointerUnion(nullptr);
using IlistTag = llvm::ilist_tag<struct A>;

View File

@ -44,7 +44,7 @@ p PointerIntPair
# CHECK: Containing int * = {pointer = 0xabc}
p PointerUnion
# CHECK: PointerUnionMembers<llvm::PointerUnion<Z*, float*>,
# CHECK: Containing Z * = {pointer = 0x0}
p RawPrintingPointerUnion
# Switch to print pretty adds newlines to the following statements.

View File

@ -13,19 +13,20 @@ struct DerivedWithVirtual : public HasVirtual {
void DerivedWithVirtual::func() {}
struct alignas(8) Z {};
struct Derived : public Z {};
int main() {
int a = 5;
float f = 4.0;
struct alignas(8) Z {};
Z z;
struct Derived : public Z {};
Derived derived;
DerivedWithVirtual dv;
llvm::PointerUnion<Z *, float *> z_float(&f);
llvm::PointerUnion<Z *, float *> raw_z_float(nullptr);
llvm::PointerUnion<Z *, float *> null_float(static_cast<float *>(nullptr));
llvm::PointerUnion<long long *, int *, float *> long_int_float(&a);
llvm::PointerUnion<Z *> z_only(&z);
@ -42,4 +43,11 @@ int main() {
llvm::PointerUnion<HasVirtual *, float *> virtual_float(&dv);
puts("Break here");
// Function-local types stress template_argument lookup in debuggers.
struct alignas(8) Local {};
Local local;
llvm::PointerUnion<Local *, float *> local_float(&local);
puts("Break here");
}

View File

@ -18,6 +18,7 @@ p &derived
p &dv
v -T z_float
v -T raw_z_float
v -T null_float
v -T long_int_float
v -T z_only
v -T union_int_pair
@ -34,6 +35,11 @@ continue
v -T virtual_float
p virtual_float.Pointer
continue
p &local
v -T local_float
#--- checks
# CHECK: (lldb) p &f
# CHECK-NEXT: (float *) [[PTR_F:0x[0-9a-zA-Z]+]]
@ -60,6 +66,13 @@ p virtual_float.Pointer
# CHECK-NEXT: (Z *) Pointer = nullptr
# CHECK-NEXT: }
# null float* has non-zero raw_value (tag=1), pointer component=0.
# LLDB renders this as an address literal rather than nullptr.
# CHECK: (lldb) v -T null_float
# CHECK-NEXT: (llvm::PointerUnion<Z *, float *>) null_float = {
# CHECK-NEXT: (float *) Pointer = 0x{{0+}}
# CHECK-NEXT: }
# CHECK: (lldb) v -T long_int_float
# CHECK-NEXT: (llvm::PointerUnion<long long *, int *, float *>) long_int_float = {
# CHECK-NEXT: (int *) Pointer = [[PTR_A]]
@ -101,3 +114,11 @@ p virtual_float.Pointer
# CHECK: (lldb) p virtual_float.Pointer
# CHECK-NEXT: (DerivedWithVirtual *) [[PTR_DV]]
# CHECK: (lldb) p &local
# CHECK-NEXT: (Local *) [[PTR_LOCAL:0x[0-9a-zA-Z]+]]
# CHECK: (lldb) v -T local_float
# CHECK-NEXT: (llvm::PointerUnion<Local *, float *>) local_float = {
# CHECK-NEXT: (Local *) Pointer = [[PTR_LOCAL]]
# CHECK-NEXT: }

View File

@ -443,14 +443,66 @@ class PointerUnionPrinter:
return "Containing %s" % self.pointer.type
def _make_pointer_union_raw_fallback(raw_value, min_low_bits):
"""Strip tag bits and return as void* when active type is unknown."""
pointer = raw_value & ~((1 << min_low_bits) - 1)
void_ptr = gdb.lookup_type("void").pointer()
return PointerUnionPrinter(gdb.Value(pointer).cast(void_ptr))
def make_pointer_union_printer(val):
"""Factory for an llvm::PointerUnion printer."""
try:
pointer, value = get_pointer_int_pair(val["Val"])
except gdb.error:
return None # If PointerIntPair cannot be analyzed, print as raw value.
pointer_type = val.type.template_argument(int(value))
return PointerUnionPrinter(pointer.cast(pointer_type))
raw_value = int(
val["Val"]["Data"]
.address.cast(gdb.lookup_type("uintptr_t").pointer())
.dereference()
)
# Collect template argument types.
# Distinguish truncation (RuntimeError — type resolution failure) from
# normal end-of-args (gdb.error — index out of range).
arg_types = []
truncated = False
while True:
try:
arg_types.append(val.type.template_argument(len(arg_types)))
except RuntimeError:
truncated = True
break
except gdb.error:
break
if not arg_types:
return None
# Compute tag from type alignments (fixed-width encoding).
num_args = len(arg_types)
min_low_bits = min(
(a.bit_length() - 1 if a > 0 else 0)
for a in (t.target().alignof for t in arg_types)
)
# If template args are truncated, we can't reliably decode the tag.
# Fall back to showing the raw pointer with tag bits stripped.
if truncated:
return _make_pointer_union_raw_fallback(raw_value, min_low_bits)
tag_bits = (num_args - 1).bit_length()
if tag_bits > min_low_bits:
return _make_pointer_union_raw_fallback(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 _make_pointer_union_raw_fallback(raw_value, min_low_bits)
pointer_type = arg_types[active_tag]
align = pointer_type.target().alignof
low_bits = align.bit_length() - 1 if align > 0 else 0
pointer = raw_value & ~((1 << low_bits) - 1)
except (gdb.error, RuntimeError, IndexError):
return None
return PointerUnionPrinter(gdb.Value(pointer).cast(pointer_type))
class IlistNodePrinter:

View File

@ -370,35 +370,79 @@ class PointerUnionSynthProvider:
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):
pointer_int_pair: lldb.SBValue = self.valobj.GetChildMemberWithName(
"Val"
).GetSyntheticValue()
self.pointer_valobj = None
if not pointer_int_pair:
valobj_type = self.valobj.GetType()
num_args = valobj_type.GetNumberOfTemplateArguments()
if num_args == 0:
return
pointer: lldb.SBValue = pointer_int_pair.GetChildAtIndex(0)
if not pointer:
# Read the raw uintptr_t from PunnedPointer<void*>.
val: lldb.SBValue = self.valobj.GetChildMemberWithName("Val")
if not val:
return
active_tag: lldb.SBValue = pointer_int_pair.GetChildAtIndex(1)
if not active_tag:
return
# Index into the parameter pack of llvm::PointerUnion to find the active type.
active_type: lldb.SBType = self.valobj.GetType().GetTemplateArgumentType(
active_tag.GetValueAsUnsigned()
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
return self._set_raw_pointer(raw_value, min_low_bits)
data = lldb.SBData()
data.SetDataFromUInt64Array([pointer.GetValueAsUnsigned()])
# 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.valobj.CreateValueFromData(
"Pointer", data, active_type
)
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: