[libcxx-commits] [libcxx] [llvm] [libcxx][lldb] Add initial LLDB data-formatters (PR #187677)
Michael Buch via libcxx-commits
libcxx-commits at lists.llvm.org
Fri Mar 20 04:18:05 PDT 2026
https://github.com/Michael137 created https://github.com/llvm/llvm-project/pull/187677
Depends on:
* https://github.com/llvm/llvm-project/pull/165769
Adds synthetic child providers for `std::map` and `std::vector`. These
were translated from C++ into Python (with the help of Claude).
>From 8524ee9c9e4ad360afbb282fce34f6ba3d99c0c6 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Fri, 20 Mar 2026 09:06:43 +0000
Subject: [PATCH 1/2] [libc++] Optionally support filecheck-based tests
---
.github/workflows/libcxx-build-and-test.yaml | 22 ++++++++++++++-----
libcxx/docs/TestingLibcxx.rst | 7 ++++++
libcxx/test/requirements.txt | 5 +++++
.../test/selftest/filecheck.negative.sh.cpp | 16 ++++++++++++++
libcxx/test/selftest/filecheck.sh.cpp | 15 +++++++++++++
libcxx/utils/libcxx/test/features/misc.py | 16 +++++++++++++-
6 files changed, 75 insertions(+), 6 deletions(-)
create mode 100644 libcxx/test/requirements.txt
create mode 100644 libcxx/test/selftest/filecheck.negative.sh.cpp
create mode 100644 libcxx/test/selftest/filecheck.sh.cpp
diff --git a/.github/workflows/libcxx-build-and-test.yaml b/.github/workflows/libcxx-build-and-test.yaml
index 233eb027a8b88..0321dd3e1b379 100644
--- a/.github/workflows/libcxx-build-and-test.yaml
+++ b/.github/workflows/libcxx-build-and-test.yaml
@@ -56,7 +56,11 @@ jobs:
steps:
- uses: actions/checkout at 8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: ${{ matrix.config }}.${{ matrix.cxx }}
- run: libcxx/utils/ci/run-buildbot ${{ matrix.config }}
+ run: |
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install -r libcxx/test/requirements.txt
+ libcxx/utils/ci/run-buildbot ${{ matrix.config }}
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
@@ -101,7 +105,11 @@ jobs:
steps:
- uses: actions/checkout at 8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: ${{ matrix.config }}
- run: libcxx/utils/ci/run-buildbot ${{ matrix.config }}
+ run: |
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install -r libcxx/test/requirements.txt
+ libcxx/utils/ci/run-buildbot ${{ matrix.config }}
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
@@ -156,7 +164,11 @@ jobs:
steps:
- uses: actions/checkout at 8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: ${{ matrix.config }}
- run: libcxx/utils/ci/run-buildbot ${{ matrix.config }}
+ run: |
+ python3 -m venv .venv
+ source .venv/bin/activate
+ pip install -r libcxx/test/requirements.txt
+ libcxx/utils/ci/run-buildbot ${{ matrix.config }}
env:
CC: clang-22
CXX: clang++-22
@@ -212,7 +224,7 @@ jobs:
run: |
python3 -m venv .venv
source .venv/bin/activate
- python -m pip install psutil
+ pip install -r libcxx/test/requirements.txt
xcrun bash libcxx/utils/ci/run-buildbot ${{ matrix.config }}
env:
CC: clang
@@ -250,7 +262,7 @@ jobs:
- uses: actions/checkout at 8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install dependencies
run: |
- pip install psutil
+ pip install -r libcxx/test/requirements.txt
- name: Install a current LLVM
if: ${{ matrix.mingw != true }}
run: |
diff --git a/libcxx/docs/TestingLibcxx.rst b/libcxx/docs/TestingLibcxx.rst
index e15c5b1a5d32f..aaeceda29fe15 100644
--- a/libcxx/docs/TestingLibcxx.rst
+++ b/libcxx/docs/TestingLibcxx.rst
@@ -23,6 +23,13 @@ Please see the `Lit Command Guide`_ for more information about LIT.
.. _LIT Command Guide: https://llvm.org/docs/CommandGuide/lit.html
+Dependencies
+------------
+
+The libc++ test suite has a few optional dependencies. These can be installed
+with ``pip install -r libcxx/test/requirements.txt``. Installing these dependencies
+will ensure that the maximum number of tests can be run.
+
Usage
-----
diff --git a/libcxx/test/requirements.txt b/libcxx/test/requirements.txt
new file mode 100644
index 0000000000000..842b8ca4ef901
--- /dev/null
+++ b/libcxx/test/requirements.txt
@@ -0,0 +1,5 @@
+#
+# This file defines Python requirements to run the libc++ test suite.
+#
+filecheck
+psutil
diff --git a/libcxx/test/selftest/filecheck.negative.sh.cpp b/libcxx/test/selftest/filecheck.negative.sh.cpp
new file mode 100644
index 0000000000000..5227b3e43752f
--- /dev/null
+++ b/libcxx/test/selftest/filecheck.negative.sh.cpp
@@ -0,0 +1,16 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: has-filecheck
+
+// Make sure that %{filecheck} fails when it should fail. This ensure that %{filecheck}
+// actually checks the content of the file.
+// XFAIL: *
+
+// RUN: echo "hello world" | %{filecheck} %s
+// CHECK: foobar
diff --git a/libcxx/test/selftest/filecheck.sh.cpp b/libcxx/test/selftest/filecheck.sh.cpp
new file mode 100644
index 0000000000000..33b7d683e792d
--- /dev/null
+++ b/libcxx/test/selftest/filecheck.sh.cpp
@@ -0,0 +1,15 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+// REQUIRES: has-filecheck
+
+// Make sure that we can use %{filecheck} to write tests when the `has-filecheck`
+// Lit feature is defined.
+
+// RUN: echo "hello world" | %{filecheck} %s
+// CHECK: hello world
diff --git a/libcxx/utils/libcxx/test/features/misc.py b/libcxx/utils/libcxx/test/features/misc.py
index 738e3d8bb207c..76571534bcd26 100644
--- a/libcxx/utils/libcxx/test/features/misc.py
+++ b/libcxx/utils/libcxx/test/features/misc.py
@@ -7,7 +7,7 @@
# ===----------------------------------------------------------------------===##
from libcxx.test.dsl import compilerMacros, sourceBuilds, hasCompileFlag, programSucceeds, runScriptExitCode
-from libcxx.test.dsl import Feature, AddCompileFlag, AddLinkFlag
+from libcxx.test.dsl import Feature, AddCompileFlag, AddLinkFlag, AddSubstitution
import platform
import sys
@@ -296,4 +296,18 @@ def _mingwSupportsModules(cfg):
""",
),
),
+
+ # Whether a `FileCheck` executable is available. Note that we intend not to depend
+ # on how that executable has been installed: we can either use the LLVM FileCheck
+ # executable or the `filecheck` Python port of the same utility.
+ Feature(
+ name="has-filecheck",
+ when=lambda cfg: runScriptExitCode(cfg, ["filecheck --version"]) == 0,
+ actions=[AddSubstitution("%{filecheck}", "filecheck")]
+ ),
+ Feature(
+ name="has-filecheck",
+ when=lambda cfg: runScriptExitCode(cfg, ["FileCheck --version"]) == 0,
+ actions=[AddSubstitution("%{filecheck}", "FileCheck")]
+ ),
]
>From 9ebe377a9a292fd8f279a20fe3ba984d5155c39b Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Thu, 19 Mar 2026 10:04:05 +0000
Subject: [PATCH 2/2] [libcxx][lldb] Add initial LLDB data-formatters
Depends on:
* https://github.com/llvm/llvm-project/pull/165769
Adds synthetic child providers for `std::map` and `std::vector`. These
were translated from C++ into Python (with the help of Claude).
---
libcxx/test/libcxx/lldb/map_formatter.test.in | 3 +
.../libcxx/lldb/map_formatter_test.sh.cpp | 28 ++
.../test/libcxx/lldb/vector_formatter.test.in | 3 +
.../libcxx/lldb/vector_formatter_test.sh.cpp | 25 ++
libcxx/utils/libcxx/test/features/__init__.py | 3 +-
libcxx/utils/libcxx/test/features/lldb.py | 40 ++
libcxx/utils/lldb/libcxx/libcxx.py | 17 +
.../utils/lldb/libcxx/libcxx_map_formatter.py | 420 ++++++++++++++++++
.../lldb/libcxx/libcxx_vector_formatter.py | 263 +++++++++++
9 files changed, 801 insertions(+), 1 deletion(-)
create mode 100644 libcxx/test/libcxx/lldb/map_formatter.test.in
create mode 100644 libcxx/test/libcxx/lldb/map_formatter_test.sh.cpp
create mode 100644 libcxx/test/libcxx/lldb/vector_formatter.test.in
create mode 100644 libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp
create mode 100644 libcxx/utils/libcxx/test/features/lldb.py
create mode 100644 libcxx/utils/lldb/libcxx/libcxx.py
create mode 100644 libcxx/utils/lldb/libcxx/libcxx_map_formatter.py
create mode 100644 libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py
diff --git a/libcxx/test/libcxx/lldb/map_formatter.test.in b/libcxx/test/libcxx/lldb/map_formatter.test.in
new file mode 100644
index 0000000000000..17c0cdea91e2e
--- /dev/null
+++ b/libcxx/test/libcxx/lldb/map_formatter.test.in
@@ -0,0 +1,3 @@
+break set -p return -X main
+run
+frame var mii
diff --git a/libcxx/test/libcxx/lldb/map_formatter_test.sh.cpp b/libcxx/test/libcxx/lldb/map_formatter_test.sh.cpp
new file mode 100644
index 0000000000000..eabd4f33ccacd
--- /dev/null
+++ b/libcxx/test/libcxx/lldb/map_formatter_test.sh.cpp
@@ -0,0 +1,28 @@
+// REQUIRES: host-has-lldb-with-python
+// REQUIRES: has-filecheck
+// REQUIRES: optimization=none
+//
+// RUN: %{cxx} %{flags} %s -o %t.out %{compile_flags} -g %{link_flags}
+//
+// RUN: %{lldb} --no-lldbinit --batch -o "settings set target.load-script-from-symbol-file false" \
+// RUN: -o "settings set stop-line-count-after 0" \
+// RUN: -o "settings set stop-line-count-before 0" \
+// RUN: -o "command script import %S/../../../utils/lldb/libcxx/libcxx.py" \
+// RUN: -s %S/map_formatter.test.in %t.out | %{filecheck} %s
+
+#include <map>
+
+int main(int, char**) {
+ std::map<int, int> mii = {
+ {1, 2},
+ {3, 4},
+ };
+
+ return 0;
+}
+
+// CHECK: (lldb) frame var mii
+// CHECK-NEXT: (std::map<int, int>) mii = size=2 {
+// CHECK-NEXT: [0] = (first = 1, second = 2)
+// CHECK-NEXT: [1] = (first = 3, second = 4)
+// CHECK-NEXT: }
diff --git a/libcxx/test/libcxx/lldb/vector_formatter.test.in b/libcxx/test/libcxx/lldb/vector_formatter.test.in
new file mode 100644
index 0000000000000..548c730573a37
--- /dev/null
+++ b/libcxx/test/libcxx/lldb/vector_formatter.test.in
@@ -0,0 +1,3 @@
+break set -p return -X main
+run
+frame var vi
diff --git a/libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp b/libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp
new file mode 100644
index 0000000000000..893c2a8a08d55
--- /dev/null
+++ b/libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp
@@ -0,0 +1,25 @@
+// REQUIRES: host-has-lldb-with-python
+// REQUIRES: has-filecheck
+// REQUIRES: optimization=none
+//
+// RUN: %{cxx} %{flags} %s -o %t.out %{compile_flags} -g %{link_flags}
+//
+// RUN: %{lldb} --no-lldbinit --batch -o "settings set target.load-script-from-symbol-file false" \
+// RUN: -o "settings set stop-line-count-after 0" \
+// RUN: -o "settings set stop-line-count-before 0" \
+// RUN: -o "command script import %S/../../../utils/lldb/libcxx/libcxx.py" \
+// RUN: -s %S/vector_formatter.test.in %t.out | FileCheck %s
+
+#include <vector>
+
+int main(int, char**) {
+ std::vector<int> vi = {1, 2};
+
+ return 0;
+}
+
+// CHECK: (lldb) frame var vi
+// CHECK-NEXT: (std::vector<int>) vi = size=2 {
+// CHECK-NEXT: [0] = 1
+// CHECK-NEXT: [1] = 2
+// CHECK-NEXT: }
diff --git a/libcxx/utils/libcxx/test/features/__init__.py b/libcxx/utils/libcxx/test/features/__init__.py
index 5c0d1f3aaafc6..f0957a2cc79ef 100644
--- a/libcxx/utils/libcxx/test/features/__init__.py
+++ b/libcxx/utils/libcxx/test/features/__init__.py
@@ -6,7 +6,7 @@
#
# ===----------------------------------------------------------------------===##
-from . import availability, compiler, gdb, libcxx_macros, localization, misc, platform
+from . import availability, compiler, gdb, lldb, libcxx_macros, localization, misc, platform
# Lit features are evaluated in order. Some features depend on other features, so
# we are careful to define them in the correct order. For example, several features
@@ -17,5 +17,6 @@
DEFAULT_FEATURES += platform.features
DEFAULT_FEATURES += localization.features
DEFAULT_FEATURES += gdb.features
+DEFAULT_FEATURES += lldb.features
DEFAULT_FEATURES += misc.features
DEFAULT_FEATURES += availability.features
diff --git a/libcxx/utils/libcxx/test/features/lldb.py b/libcxx/utils/libcxx/test/features/lldb.py
new file mode 100644
index 0000000000000..c022c9c358ece
--- /dev/null
+++ b/libcxx/utils/libcxx/test/features/lldb.py
@@ -0,0 +1,40 @@
+# ===----------------------------------------------------------------------===##
+#
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+#
+# ===----------------------------------------------------------------------===##
+
+from libcxx.test.dsl import Feature, AddSubstitution
+import shutil
+import subprocess
+
+# Detect whether LLDB is on the system and has Python scripting support.
+# If so add a substitution to access it.
+
+def check_lldb(cfg):
+ lldb_path = shutil.which("lldb")
+ if lldb_path is None:
+ return False
+
+ try:
+ stdout = subprocess.check_output(
+ [lldb_path, "--batch", "-o", "script -l python -- print(\"Has\", \"Python\", \"!\")"],
+ stderr=subprocess.DEVNULL,
+ universal_newlines=True,
+ )
+ except subprocess.CalledProcessError:
+ return False
+
+ # Check we actually ran the Python
+ return "Has Python !" in stdout
+
+
+features = [
+ Feature(
+ name="host-has-lldb-with-python",
+ when=check_lldb,
+ actions=[AddSubstitution("%{lldb}", lambda cfg: shutil.which("lldb"))],
+ )
+]
diff --git a/libcxx/utils/lldb/libcxx/libcxx.py b/libcxx/utils/lldb/libcxx/libcxx.py
new file mode 100644
index 0000000000000..3e904cbd06758
--- /dev/null
+++ b/libcxx/utils/lldb/libcxx/libcxx.py
@@ -0,0 +1,17 @@
+import lldb
+from libcxx_map_formatter import *
+from libcxx_vector_formatter import *
+
+def register_synthetic(debugger: lldb.SBDebugger, regex: str, class_name: str):
+ debugger.HandleCommand(f'type synthetic add -x "{regex}" -l {__name__}.{class_name} -w "cplusplus-py"')
+
+def __lldb_init_module(debugger, dict):
+ register_synthetic(debugger, "^std::__[[:alnum:]]+::map<.+> >$", "LibcxxStdMapSyntheticProvider")
+ register_synthetic(debugger, "^std::__[[:alnum:]]+::set<.+> >$", "LibcxxStdMapSyntheticProvider")
+ register_synthetic(debugger, "^std::__[[:alnum:]]+::multiset<.+> >$", "LibcxxStdMapSyntheticProvider")
+ register_synthetic(debugger, "^std::__[[:alnum:]]+::multimap<.+> >$", "LibcxxStdMapSyntheticProvider")
+ register_synthetic(debugger, "^std::__[[:alnum:]]+::__map_(const_)?iterator<.+>$", "LibCxxMapIteratorSyntheticProvider")
+ register_synthetic(debugger, "^std::__[[:alnum:]]+::vector<.+>$", "LibCxxStdVectorSyntheticFrontendCreator")
+
+ # Enables registered formatters in LLDB.
+ debugger.HandleCommand('type category enable cplusplus-py')
diff --git a/libcxx/utils/lldb/libcxx/libcxx_map_formatter.py b/libcxx/utils/lldb/libcxx/libcxx_map_formatter.py
new file mode 100644
index 0000000000000..93bdddd9a7d2f
--- /dev/null
+++ b/libcxx/utils/lldb/libcxx/libcxx_map_formatter.py
@@ -0,0 +1,420 @@
+"""
+Python LLDB synthetic child provider for libc++ std::map
+
+1-to-1 translation from the LLDB builtin std::(multi)map formatter.
+
+Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+See https://llvm.org/LICENSE.txt for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""
+
+import lldb
+
+
+class MapEntry:
+ """Wrapper around an LLDB ValueObject representing a tree node entry."""
+
+ def __init__(self, entry_sp=None):
+ self.m_entry_sp = entry_sp
+
+ def left(self):
+ """Get the left child pointer (offset 0)."""
+ if not self.m_entry_sp:
+ return None
+ return self.m_entry_sp.CreateChildAtOffset("", 0, self.m_entry_sp.GetType())
+
+ def right(self):
+ if not self.m_entry_sp:
+ return None
+ addr_size = self.m_entry_sp.GetProcess().GetAddressByteSize()
+ return self.m_entry_sp.CreateChildAtOffset(
+ "", addr_size, self.m_entry_sp.GetType()
+ )
+
+ def parent(self):
+ if not self.m_entry_sp:
+ return None
+ addr_size = self.m_entry_sp.GetProcess().GetAddressByteSize()
+ return self.m_entry_sp.CreateChildAtOffset(
+ "", 2 * addr_size, self.m_entry_sp.GetType()
+ )
+
+ def value(self):
+ """Get the unsigned integer value of the entry (pointer address)."""
+ if not self.m_entry_sp:
+ return 0
+ return self.m_entry_sp.GetValueAsUnsigned(0)
+
+ def error(self):
+ if not self.m_entry_sp:
+ return True
+ return self.m_entry_sp.GetError().Fail()
+
+ def null(self):
+ return self.value() == 0
+
+ def get_entry(self):
+ return self.m_entry_sp
+
+ def set_entry(self, entry):
+ self.m_entry_sp = entry
+
+ def __eq__(self, other):
+ if not isinstance(other, MapEntry):
+ return False
+ if self.m_entry_sp is None and other.m_entry_sp is None:
+ return True
+ if self.m_entry_sp is None or other.m_entry_sp is None:
+ return False
+ return self.m_entry_sp.GetLoadAddress() == other.m_entry_sp.GetLoadAddress()
+
+
+class MapIterator:
+ """Iterator for traversing the tree backing std::map."""
+
+ def __init__(self, entry=None, depth=0):
+ self.m_entry = MapEntry(entry) if entry else MapEntry()
+ self.m_max_depth = depth
+ self.m_error = False
+
+ def value(self):
+ return self.m_entry.get_entry()
+
+ def advance(self, count):
+ """Advance the iterator by count steps and return the entry."""
+ if self.m_error:
+ return None
+
+ steps = 0
+ while count > 0:
+ self._next()
+ count -= 1
+ steps += 1
+ if self.m_error or self.m_entry.null() or (steps > self.m_max_depth):
+ return None
+
+ return self.m_entry.get_entry()
+
+ def _next(self):
+ """
+ Mimics libc++'s __tree_next algorithm, which libc++ uses
+ in its __tree_iterator::operator++.
+ """
+ if self.m_entry.null():
+ return
+
+ right = MapEntry(self.m_entry.right())
+ if not right.null():
+ self.m_entry = self._tree_min(right)
+ return
+
+ steps = 0
+ while not self._is_left_child(self.m_entry):
+ if self.m_entry.error():
+ self.m_error = True
+ return
+ self.m_entry.set_entry(self.m_entry.parent())
+ steps += 1
+ if steps > self.m_max_depth:
+ self.m_entry = MapEntry()
+ return
+
+ self.m_entry = MapEntry(self.m_entry.parent())
+
+ def _tree_min(self, x):
+ """Mimics libc++'s __tree_min algorithm."""
+ if x.null():
+ return MapEntry()
+
+ left = MapEntry(x.left())
+ steps = 0
+ while not left.null():
+ if left.error():
+ self.m_error = True
+ return MapEntry()
+ x = MapEntry(left.get_entry())
+ left = MapEntry(x.left())
+ steps += 1
+ if steps > self.m_max_depth:
+ return MapEntry()
+
+ return x
+
+ def _is_left_child(self, x):
+ """Check if x is a left child of its parent."""
+ if x.null():
+ return False
+ rhs = MapEntry(x.parent())
+ rhs.set_entry(rhs.left())
+ return x.value() == rhs.value()
+
+
+def _get_first_value_of_libcxx_compressed_pair(pair):
+ """
+ Get the first value from a libc++ compressed pair.
+ Handles both old and new layouts.
+ """
+ # Try __value_ first (newer layout)
+ value_sp = pair.GetChildMemberWithName("__value_")
+ if value_sp and value_sp.IsValid():
+ return value_sp
+
+ # Try __first_ (older layout)
+ first_sp = pair.GetChildMemberWithName("__first_")
+ if first_sp and first_sp.IsValid():
+ return first_sp
+
+ return None
+
+
+def _get_value_or_old_compressed_pair(tree, value_name, pair_name):
+ """
+ Try to get a value member directly, or fall back to compressed pair layout.
+ Returns (value_sp, is_compressed_pair).
+ """
+ # Try new layout first (direct member)
+ value_sp = tree.GetChildMemberWithName(value_name)
+ if value_sp and value_sp.IsValid():
+ return (value_sp, False)
+
+ # Fall back to old compressed pair layout
+ pair_sp = tree.GetChildMemberWithName(pair_name)
+ if pair_sp and pair_sp.IsValid():
+ return (pair_sp, True)
+
+ return (None, False)
+
+
+class LibcxxStdMapSyntheticProvider:
+ """Synthetic children provider for libc++ std::map."""
+
+ def __init__(self, valobj, internal_dict):
+ self.valobj = valobj
+ self.m_tree = None
+ self.m_root_node = None
+ self.m_node_ptr_type = None
+ self.m_count = None
+ self.m_iterators = {}
+
+ def num_children(self):
+ if self.m_count is not None:
+ return self.m_count
+
+ if self.m_tree is None:
+ return 0
+
+ # Try new layout (__size_) or old compressed pair layout (__pair3_)
+ size_sp, is_compressed_pair = _get_value_or_old_compressed_pair(
+ self.m_tree, "__size_", "__pair3_"
+ )
+
+ if not size_sp:
+ return 0
+
+ if is_compressed_pair:
+ return self._calculate_num_children_for_old_compressed_pair_layout(size_sp)
+
+ self.m_count = size_sp.GetValueAsUnsigned(0)
+ return self.m_count
+
+ def _calculate_num_children_for_old_compressed_pair_layout(self, pair):
+ """Handle old libc++ compressed pair layout."""
+ node_sp = _get_first_value_of_libcxx_compressed_pair(pair)
+
+ if not node_sp:
+ return 0
+
+ self.m_count = node_sp.GetValueAsUnsigned(0)
+ return self.m_count
+
+ def get_child_index(self, name):
+ """Get the index of a child with the given name (e.g., "[0]" -> 0)."""
+ try:
+ if name.startswith("[") and name.endswith("]"):
+ return int(name[1:-1])
+ except ValueError:
+ pass
+ return None
+
+ def get_child_at_index(self, index):
+ num_children = self.num_children()
+ if index >= num_children:
+ return None
+
+ if self.m_tree is None or self.m_root_node is None:
+ return None
+
+ key_val_sp = self._get_key_value_pair(index, num_children)
+ if not key_val_sp:
+ # This will stop all future searches until an update() happens
+ self.m_tree = None
+ return None
+
+ name = "[%d]" % index
+ potential_child_sp = key_val_sp.Clone(name)
+
+ if potential_child_sp and potential_child_sp.IsValid():
+ num_child_children = potential_child_sp.GetNumChildren()
+
+ # Handle __cc_ or __cc wrapper (1 child case)
+ if num_child_children == 1:
+ child0_sp = potential_child_sp.GetChildAtIndex(0)
+ if child0_sp:
+ child_name = child0_sp.GetName()
+ if child_name in ("__cc_", "__cc"):
+ potential_child_sp = child0_sp.Clone(name)
+
+ # Handle __cc_ and __nc wrapper (2 children case)
+ elif num_child_children == 2:
+ child0_sp = potential_child_sp.GetChildAtIndex(0)
+ child1_sp = potential_child_sp.GetChildAtIndex(1)
+ if child0_sp and child1_sp:
+ child0_name = child0_sp.GetName()
+ child1_name = child1_sp.GetName()
+ if child0_name in ("__cc_", "__cc") and child1_name == "__nc":
+ potential_child_sp = child0_sp.Clone(name)
+
+ return potential_child_sp
+
+ def update(self):
+ """Update the cached state. Called when the underlying value changes."""
+ self.m_count = None
+ self.m_tree = None
+ self.m_root_node = None
+ self.m_iterators.clear()
+
+ self.m_tree = self.valobj.GetChildMemberWithName("__tree_")
+ if not self.m_tree or not self.m_tree.IsValid():
+ return False
+
+ self.m_root_node = self.m_tree.GetChildMemberWithName("__begin_node_")
+
+ # Get the __node_pointer type from the tree's type
+ tree_type = self.m_tree.GetType()
+ self.m_node_ptr_type = tree_type.FindDirectNestedType("__node_pointer")
+
+ return False
+
+ def has_children(self):
+ """Check if this object has children."""
+ return True
+
+ def _get_key_value_pair(self, idx, max_depth):
+ """
+ Returns the ValueObject for the __tree_node type that
+ holds the key/value pair of the node at index idx.
+ """
+ iterator = MapIterator(self.m_root_node, max_depth)
+
+ advance_by = idx
+ if idx > 0:
+ # If we have already created the iterator for the previous
+ # index, we can start from there and advance by 1.
+ if idx - 1 in self.m_iterators:
+ iterator = self.m_iterators[idx - 1]
+ advance_by = 1
+
+ iterated_sp = iterator.advance(advance_by)
+ if not iterated_sp:
+ # This tree is garbage - stop
+ return None
+
+ if not self.m_node_ptr_type or not self.m_node_ptr_type.IsValid():
+ return None
+
+ # iterated_sp is a __iter_pointer at this point.
+ # We can cast it to a __node_pointer (which is what libc++ does).
+ value_type_sp = iterated_sp.Cast(self.m_node_ptr_type)
+ if not value_type_sp or not value_type_sp.IsValid():
+ return None
+
+ # Finally, get the key/value pair.
+ value_type_sp = value_type_sp.GetChildMemberWithName("__value_")
+ if not value_type_sp or not value_type_sp.IsValid():
+ return None
+
+ self.m_iterators[idx] = iterator
+
+ return value_type_sp
+
+
+class LibCxxMapIteratorSyntheticProvider:
+ """Synthetic children provider for libc++ std::map::iterator."""
+
+ def __init__(self, valobj, internal_dict):
+ self.valobj = valobj
+ self.m_pair_sp = None
+
+ def num_children(self):
+ """Map iterators always have 2 children (first and second)."""
+ return 2
+
+ def get_child_index(self, name):
+ if not self.m_pair_sp:
+ return None
+ return self.m_pair_sp.GetIndexOfChildWithName(name)
+
+ def get_child_at_index(self, index):
+ if not self.m_pair_sp:
+ return None
+ return self.m_pair_sp.GetChildAtIndex(index)
+
+ def update(self):
+ self.m_pair_sp = None
+
+ if not self.valobj.IsValid():
+ return False
+
+ target = self.valobj.GetTarget()
+ if not target or not target.IsValid():
+ return False
+
+ # valobj is a std::map::iterator
+ # ...which is a __map_iterator<__tree_iterator<..., __node_pointer, ...>>
+ #
+ # Then, __map_iterator::__i_ is a __tree_iterator
+ tree_iter_sp = self.valobj.GetChildMemberWithName("__i_")
+ if not tree_iter_sp or not tree_iter_sp.IsValid():
+ return False
+
+ # Type is __tree_iterator::__node_pointer
+ # (We could alternatively also get this from the template argument)
+ tree_iter_type = tree_iter_sp.GetType()
+ node_pointer_type = tree_iter_type.FindDirectNestedType("__node_pointer")
+ if not node_pointer_type or not node_pointer_type.IsValid():
+ return False
+
+ # __ptr_ is a __tree_iterator::__iter_pointer
+ iter_pointer_sp = tree_iter_sp.GetChildMemberWithName("__ptr_")
+ if not iter_pointer_sp or not iter_pointer_sp.IsValid():
+ return False
+
+ # Cast the __iter_pointer to a __node_pointer (which stores our key/value pair)
+ node_pointer_sp = iter_pointer_sp.Cast(node_pointer_type)
+ if not node_pointer_sp or not node_pointer_sp.IsValid():
+ return False
+
+ key_value_sp = node_pointer_sp.GetChildMemberWithName("__value_")
+ if not key_value_sp or not key_value_sp.IsValid():
+ return False
+
+ # Create the synthetic child, which is a pair where the key and value can be
+ # retrieved by querying the synthetic provider for
+ # get_child_index("first") and get_child_index("second")
+ # respectively.
+ #
+ # std::map stores the actual key/value pair in value_type::__cc_ (or
+ # previously __cc).
+ key_value_sp = key_value_sp.Clone("pair")
+ if key_value_sp.GetNumChildren() == 1:
+ child0_sp = key_value_sp.GetChildAtIndex(0)
+ if child0_sp:
+ child_name = child0_sp.GetName()
+ if child_name in ("__cc_", "__cc"):
+ key_value_sp = child0_sp.Clone("pair")
+
+ self.m_pair_sp = key_value_sp
+ return False
+
+ def has_children(self):
+ return True
diff --git a/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py b/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py
new file mode 100644
index 0000000000000..368f121c2808f
--- /dev/null
+++ b/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py
@@ -0,0 +1,263 @@
+"""
+Python LLDB data formatter for libc++ std::vector
+
+1-to-1 translation from the LLDB builtin std::vector formatter.
+
+Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+See https://llvm.org/LICENSE.txt for license information.
+SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+"""
+
+import lldb
+
+
+def get_data_pointer(root):
+ """Get the data pointer from a vector, handling compressed pair layout."""
+ # Try new layout
+ cap_sp = root.GetChildMemberWithName("__cap_")
+ if cap_sp and cap_sp.IsValid():
+ return cap_sp
+
+ # Try old compressed pair layout
+ end_cap_sp = root.GetChildMemberWithName("__end_cap_")
+ if not end_cap_sp or not end_cap_sp.IsValid():
+ return None
+
+ # Get first value of compressed pair
+ value_sp = end_cap_sp.GetChildMemberWithName("__value_")
+ if value_sp and value_sp.IsValid():
+ return value_sp
+
+ first_sp = end_cap_sp.GetChildMemberWithName("__first_")
+ if first_sp and first_sp.IsValid():
+ return first_sp
+
+ return None
+
+
+class LibCxxStdVectorSyntheticFrontEnd:
+ """Synthetic children frontend for libc++ std::vector."""
+
+ def __init__(self, valobj, internal_dict):
+ self.valobj = valobj
+ self.m_start = None
+ self.m_finish = None
+ self.m_element_type = None
+ self.m_element_size = 0
+ self.update()
+
+ def num_children(self):
+ if not self.m_start or not self.m_finish:
+ return 0
+
+ start_val = self.m_start.GetValueAsUnsigned(0)
+ finish_val = self.m_finish.GetValueAsUnsigned(0)
+
+ # A default-initialized empty vector
+ if start_val == 0 and finish_val == 0:
+ return 0
+
+ if start_val == 0:
+ return 0
+
+ if finish_val == 0:
+ return 0
+
+ if start_val > finish_val:
+ return 0
+
+ num_children = finish_val - start_val
+ if num_children % self.m_element_size != 0:
+ return 0
+
+ return num_children // self.m_element_size
+
+ def get_child_index(self, name):
+ if not self.m_start or not self.m_finish:
+ return None
+ try:
+ if name.startswith("[") and name.endswith("]"):
+ return int(name[1:-1])
+ except ValueError:
+ pass
+ return None
+
+ def get_child_at_index(self, index):
+ if not self.m_start or not self.m_finish:
+ return None
+
+ offset = index * self.m_element_size
+ offset = offset + self.m_start.GetValueAsUnsigned(0)
+
+ name = "[%d]" % index
+ target = self.valobj.GetTarget()
+ if not target or not target.IsValid():
+ return None
+
+ addr = lldb.SBAddress(offset, target)
+ return target.CreateValueFromAddress(name, addr, self.m_element_type)
+
+ def update(self):
+ self.m_start = None
+ self.m_finish = None
+
+ data_sp = get_data_pointer(self.valobj)
+ if not data_sp or not data_sp.IsValid():
+ return False
+
+ self.m_element_type = data_sp.GetType().GetPointeeType()
+ if not self.m_element_type.IsValid():
+ return False
+
+ size = self.m_element_type.GetByteSize()
+ if not size or size == 0:
+ return False
+
+ self.m_element_size = size
+
+ begin_sp = self.valobj.GetChildMemberWithName("__begin_")
+ end_sp = self.valobj.GetChildMemberWithName("__end_")
+
+ if not begin_sp or not end_sp:
+ return False
+
+ self.m_start = begin_sp
+ self.m_finish = end_sp
+
+ return True
+
+ def has_children(self):
+ return True
+
+
+class LibCxxVectorBoolSyntheticFrontEnd:
+ """Synthetic children frontend for libc++ std::vector<bool>."""
+
+ def __init__(self, valobj, internal_dict):
+ self.valobj = valobj
+ self.m_bool_type = None
+ self.m_count = 0
+ self.m_base_data_address = 0
+ self.m_children = {}
+ self.update()
+ # Get bool type
+ if valobj.IsValid():
+ target = valobj.GetTarget()
+ if target and target.IsValid():
+ self.m_bool_type = target.GetBasicType(lldb.eBasicTypeBool)
+
+ def num_children(self):
+ return self.m_count
+
+ def get_child_index(self, name):
+ if not self.m_count or not self.m_base_data_address:
+ return None
+ try:
+ if name.startswith("[") and name.endswith("]"):
+ idx = int(name[1:-1])
+ if idx >= self.m_count:
+ return None
+ return idx
+ except ValueError:
+ pass
+ return None
+
+ def get_child_at_index(self, index):
+ if index in self.m_children:
+ return self.m_children[index]
+
+ if index >= self.m_count:
+ return None
+
+ if self.m_base_data_address == 0 or self.m_count == 0:
+ return None
+
+ if not self.m_bool_type or not self.m_bool_type.IsValid():
+ return None
+
+ # Calculate byte and bit index
+ byte_idx = index >> 3 # divide by 8
+ bit_index = index & 7 # modulo 8
+
+ byte_location = self.m_base_data_address + byte_idx
+
+ process = self.valobj.GetProcess()
+ if not process or not process.IsValid():
+ return None
+
+ error = lldb.SBError()
+ byte_data = process.ReadMemory(byte_location, 1, error)
+ if error.Fail() or not byte_data or len(byte_data) == 0:
+ return None
+
+ byte = ord(byte_data[0:1])
+ mask = 1 << bit_index
+ bit_set = (byte & mask) != 0
+
+ bool_size = self.m_bool_type.GetByteSize()
+ if not bool_size:
+ return None
+
+ # Create data for the bool value
+ data = lldb.SBData()
+ data.SetData(
+ error,
+ bytes([1 if bit_set else 0]),
+ process.GetByteOrder(),
+ process.GetAddressByteSize(),
+ )
+
+ name = "[%d]" % index
+ target = self.valobj.GetTarget()
+ if not target or not target.IsValid():
+ return None
+
+ retval_sp = target.CreateValueFromData(name, data, self.m_bool_type)
+ if retval_sp and retval_sp.IsValid():
+ self.m_children[index] = retval_sp
+
+ return retval_sp
+
+ def update(self):
+ self.m_children = {}
+
+ if not self.valobj.IsValid():
+ return False
+
+ size_sp = self.valobj.GetChildMemberWithName("__size_")
+ if not size_sp or not size_sp.IsValid():
+ return False
+
+ self.m_count = size_sp.GetValueAsUnsigned(0)
+ if not self.m_count:
+ return True
+
+ begin_sp = self.valobj.GetChildMemberWithName("__begin_")
+ if not begin_sp or not begin_sp.IsValid():
+ self.m_count = 0
+ return False
+
+ self.m_base_data_address = begin_sp.GetValueAsUnsigned(0)
+ if not self.m_base_data_address:
+ self.m_count = 0
+ return False
+
+ return True
+
+ def has_children(self):
+ return True
+
+
+def LibCxxStdVectorSyntheticFrontendCreator(valobj, internal_dict):
+ if not valobj or not valobj.IsValid():
+ return None
+
+ compiler_type = valobj.GetType()
+ if not compiler_type.IsValid() or compiler_type.GetNumberOfTemplateArguments() == 0:
+ return None
+
+ arg_type = compiler_type.GetTemplateArgumentType(0)
+ if arg_type.GetName() == "bool":
+ return LibCxxVectorBoolSyntheticFrontEnd(valobj, internal_dict)
+
+ return LibCxxStdVectorSyntheticFrontEnd(valobj, internal_dict)
More information about the libcxx-commits
mailing list