[Lldb-commits] [lldb] [LLDB] Hide unresolvable children from ObjC tagged pointers (PR #211936)

Adrian Prantl via lldb-commits lldb-commits at lists.llvm.org
Fri Jul 24 14:57:11 PDT 2026


https://github.com/adrian-prantl created https://github.com/llvm/llvm-project/pull/211936

Foundation's small value inline representations have valid summaries and sometimed synthetic children, but LLDB cannot access any ivars from their base classes (such as [NSObject isa]).

Use a synthetic child provider to hide them.

rdar://182434208

Assisted-by: claude

>From e68cb4915abaf7054ca9d77d2fc1cbe55098b7d4 Mon Sep 17 00:00:00 2001
From: Adrian Prantl <aprantl at apple.com>
Date: Fri, 24 Jul 2026 14:53:33 -0700
Subject: [PATCH] [LLDB] Hide unresolvable children from ObjC tagged pointers

Foundation's small value inline representations have valid summaries
and sometimed synthetic children, but LLDB cannot access any ivars
from their base classes (such as [NSObject isa]).

Use a synthetic child provider to hide them.

rdar://182434208

Assisted-by: claude
---
 .../Plugins/Language/ObjC/ObjCLanguage.cpp    | 38 +++++++++++
 .../Plugins/Language/ObjC/ObjCLanguage.h      |  3 +
 .../ObjC/ObjCLanguageRuntime.cpp              | 17 +++++
 .../ObjC/ObjCLanguageRuntime.h                |  2 +
 .../objc/tagged-pointer-children/Makefile     |  4 ++
 .../TestTaggedPointerChildren.py              | 68 +++++++++++++++++++
 .../lang/objc/tagged-pointer-children/main.m  | 19 ++++++
 7 files changed, 151 insertions(+)
 create mode 100644 lldb/test/API/lang/objc/tagged-pointer-children/Makefile
 create mode 100644 lldb/test/API/lang/objc/tagged-pointer-children/TestTaggedPointerChildren.py
 create mode 100644 lldb/test/API/lang/objc/tagged-pointer-children/main.m

diff --git a/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp b/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp
index 4aec66951ed12..4fd3e72527e3d 100644
--- a/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp
+++ b/lldb/source/Plugins/Language/ObjC/ObjCLanguage.cpp
@@ -881,6 +881,44 @@ lldb::TypeCategoryImplSP ObjCLanguage::GetFormatters() {
   return g_category;
 }
 
+HardcodedFormatters::HardcodedSyntheticFinder
+ObjCLanguage::GetHardcodedSynthetics() {
+  static llvm::once_flag g_initialize;
+  static HardcodedFormatters::HardcodedSyntheticFinder g_formatters;
+
+  llvm::call_once(g_initialize, []() -> void {
+    // An Objective-C tagged pointer (e.g. a single-index NSIndexSet
+    // or a small NSNumber) packs its whole state into the pointer
+    // bits and has no object in memory. Since its declared base
+    // classes and ivars cannot be read, this synthtic child provider
+    // hides them. Other synthetic child providers take precedence of
+    // this.
+    g_formatters.push_back([](lldb_private::ValueObject &valobj,
+                              lldb::DynamicValueType, FormatManager &)
+                               -> SyntheticChildren::SharedPointer {
+      static CXXSyntheticChildren::SharedPointer formatter_sp(
+          new CXXSyntheticChildren(
+              SyntheticChildren::Flags()
+                  .SetCascades(true)
+                  .SetSkipPointers(false)
+                  .SetSkipReferences(false)
+                  .SetNonCacheable(true),
+              "tagged pointer synthetic children",
+              lldb_private::formatters::ObjCClassSyntheticFrontEndCreator));
+
+      ProcessSP process_sp = valobj.GetProcessSP();
+      if (!process_sp)
+        return nullptr;
+      if (ObjCLanguageRuntime *runtime = ObjCLanguageRuntime::Get(*process_sp))
+        if (runtime->IsTaggedPointerValue(valobj))
+          return formatter_sp;
+      return nullptr;
+    });
+  });
+
+  return g_formatters;
+}
+
 std::vector<FormattersMatchCandidate>
 ObjCLanguage::GetPossibleFormattersMatches(ValueObject &valobj,
                                            lldb::DynamicValueType use_dynamic) {
diff --git a/lldb/source/Plugins/Language/ObjC/ObjCLanguage.h b/lldb/source/Plugins/Language/ObjC/ObjCLanguage.h
index 98f3b2ec3f6fa..6c9323f6ed74f 100644
--- a/lldb/source/Plugins/Language/ObjC/ObjCLanguage.h
+++ b/lldb/source/Plugins/Language/ObjC/ObjCLanguage.h
@@ -146,6 +146,9 @@ class ObjCLanguage : public Language {
 
   lldb::TypeCategoryImplSP GetFormatters() override;
 
+  HardcodedFormatters::HardcodedSyntheticFinder
+  GetHardcodedSynthetics() override;
+
   std::vector<FormattersMatchCandidate>
   GetPossibleFormattersMatches(ValueObject &valobj,
                                lldb::DynamicValueType use_dynamic) override;
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.cpp b/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.cpp
index 170546f4243f5..fbde6ecc91ff6 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.cpp
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.cpp
@@ -290,6 +290,23 @@ ObjCLanguageRuntime::GetClassDescriptor(ValueObject &valobj) {
   return objc_class_sp;
 }
 
+bool ObjCLanguageRuntime::IsTaggedPointerValue(ValueObject &in_value) {
+  TaggedPointerVendor *tagged_pointer_vendor = GetTaggedPointerVendor();
+  if (!tagged_pointer_vendor)
+    return false;
+
+  // Only Objective-C object values can be tagged pointers.
+  if (!(in_value.GetTypeInfo() & lldb::eTypeIsObjC))
+    return false;
+
+  addr_t ptr = in_value.IsPointerType() ? in_value.GetPointerValue().address
+                                        : in_value.GetAddressOf().address;
+  if (ptr == LLDB_INVALID_ADDRESS)
+    return false;
+
+  return tagged_pointer_vendor->IsPossibleTaggedPointer(ptr);
+}
+
 ObjCLanguageRuntime::ClassDescriptorSP
 ObjCLanguageRuntime::GetNonKVOClassDescriptor(ValueObject &valobj) {
   ObjCLanguageRuntime::ClassDescriptorSP objc_class_sp(
diff --git a/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h b/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h
index f6b8854ed187a..089b11d59ea03 100644
--- a/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h
+++ b/lldb/source/Plugins/LanguageRuntime/ObjC/ObjCLanguageRuntime.h
@@ -233,6 +233,8 @@ class ObjCLanguageRuntime : public LanguageRuntime {
 
   virtual TaggedPointerVendor *GetTaggedPointerVendor() { return nullptr; }
 
+  bool IsTaggedPointerValue(ValueObject &in_value);
+
   typedef std::shared_ptr<EncodingToType> EncodingToTypeSP;
 
   virtual EncodingToTypeSP GetEncodingToType();
diff --git a/lldb/test/API/lang/objc/tagged-pointer-children/Makefile b/lldb/test/API/lang/objc/tagged-pointer-children/Makefile
new file mode 100644
index 0000000000000..afecbf969483e
--- /dev/null
+++ b/lldb/test/API/lang/objc/tagged-pointer-children/Makefile
@@ -0,0 +1,4 @@
+OBJC_SOURCES := main.m
+LD_EXTRAS := -lobjc -framework Foundation
+
+include Makefile.rules
diff --git a/lldb/test/API/lang/objc/tagged-pointer-children/TestTaggedPointerChildren.py b/lldb/test/API/lang/objc/tagged-pointer-children/TestTaggedPointerChildren.py
new file mode 100644
index 0000000000000..f2ed921a736e8
--- /dev/null
+++ b/lldb/test/API/lang/objc/tagged-pointer-children/TestTaggedPointerChildren.py
@@ -0,0 +1,68 @@
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class TaggedPointerChildrenTestCase(TestBase):
+    def is_tagged(self, valobj):
+        """Return True if valobj holds an Objective-C tagged pointer."""
+        res = lldb.SBCommandReturnObject()
+        ci = self.dbg.GetCommandInterpreter()
+        ci.HandleCommand(
+            "language objc tagged-pointer info %s" % valobj.GetValue(), res
+        )
+        return res.Succeeded() and "is tagged" in res.GetOutput()
+
+    def assert_no_unreadable_children(self, valobj):
+        """Recursively assert no child of valobj fails with a memory-read error."""
+        for i in range(valobj.GetNumChildren()):
+            child = valobj.GetChildAtIndex(i)
+            err = child.GetError()
+            self.assertFalse(
+                err.Fail() and "read memory" in (err.GetCString() or ""),
+                "child '%s' of '%s' has a memory-read error: %s"
+                % (child.GetName(), valobj.GetName(), err.GetCString()),
+            )
+            self.assert_no_unreadable_children(child)
+
+    @skipUnlessDarwin
+    @skipIf(archs=["i386", "i686"])
+    def test(self):
+        """
+        Test that Objective-C tagged pointers (inline values) do not present phantom,
+        memory-backed children.
+        """
+        self.build()
+        _, _, thread, _ = lldbutil.run_to_source_breakpoint(
+            self, "// break here", lldb.SBFileSpec("main.m")
+        )
+        frame = thread.GetSelectedFrame()
+
+        tagged = frame.FindVariable("tagged")
+        self.assertTrue(tagged.IsValid(), "found 'tagged'")
+
+        # If this platform does not represent a single-index NSIndexSet as a
+        # tagged pointer there is nothing to test.
+        if not self.is_tagged(tagged):
+            self.skipTest("NSIndexSet is not a tagged pointer on this platform")
+
+        # The tagged pointer is still summarized.
+        self.assertIsNotNone(tagged.GetSummary())
+        self.assertIn("index", tagged.GetSummary())
+
+        # But it must not present any unresolvable children.
+        self.assertEqual(
+            tagged.GetNumChildren(),
+            0,
+            "tagged pointer must not have children",
+        )
+        self.assertFalse(tagged.MightHaveChildren())
+        self.assert_no_unreadable_children(tagged)
+
+        # Sanity check.
+        heap = frame.FindVariable("heap")
+        self.assertTrue(heap.IsValid(), "found 'heap'")
+        self.assertFalse(self.is_tagged(heap), "'heap' is a real object")
+        self.assertGreater(heap.GetNumChildren(), 0, "real object still has children")
+        self.assert_no_unreadable_children(heap)
diff --git a/lldb/test/API/lang/objc/tagged-pointer-children/main.m b/lldb/test/API/lang/objc/tagged-pointer-children/main.m
new file mode 100644
index 0000000000000..7f28e77510956
--- /dev/null
+++ b/lldb/test/API/lang/objc/tagged-pointer-children/main.m
@@ -0,0 +1,19 @@
+#import <Foundation/Foundation.h>
+
+int main(int argc, const char *argv[]) {
+  @autoreleasepool {
+    // A single-index NSIndexSet is stored as an Objective-C tagged pointer.
+    // It has a summary but no ivars or base classes LLDB could materialize
+    // from memory.
+    NSIndexSet *tagged = [NSIndexSet indexSetWithIndex:1];
+
+    // A discontiguous set cannot be tagged, so it is a real heap object with a
+    // readable isa and ivar layout.
+    NSMutableIndexSet *heap = [NSMutableIndexSet indexSet];
+    [heap addIndex:1];
+    [heap addIndex:1000000];
+
+    NSLog(@"%@ %@", tagged, heap); // break here
+  }
+  return 0;
+}



More information about the lldb-commits mailing list