[libcxx-commits] [libcxx] [lldb] [llvm] [libcxx][lldb] Add initial LLDB data-formatters (PR #187677)

Michael Buch via libcxx-commits libcxx-commits at lists.llvm.org
Wed Mar 25 05:00:42 PDT 2026


https://github.com/Michael137 updated https://github.com/llvm/llvm-project/pull/187677

>From a68591960146f9a65a86302a612ee50e844edb84 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 24 Mar 2026 16:14:53 +0000
Subject: [PATCH 1/6] [libcxx][test] Add support for LLVM-style split tests

This patch adds support for a new kind of test (whose names have
`.split.` in them). An example of such test is as follows:
```
   // RUN: use %{temp}/main.cpp
   // RUN: use %{temp}/inputs.txt

   //--- main.cpp
   int main() { return 0; }

   //--- input.txt
   some inputs
```

This patch creates the subfiles in the directory pointed to by the `%{temp}` substitution. Then it runs the original test file just like a regular shell LIT test.

The LLVM `split-file` utility would do this for us already, but it's not available as a Python package yet. So in the meantime we piggyback off of the hand-rolled `_splitFile` that's used for the `.gen.` tests.

This is useful for the upcoming LLDB data-formatter tests (see https://github.com/llvm/llvm-project/pull/187677).
---
 libcxx/test/selftest/split/test.split.sh | 24 +++++++++++++
 libcxx/utils/libcxx/test/format.py       | 43 ++++++++++++++++++++++--
 2 files changed, 65 insertions(+), 2 deletions(-)
 create mode 100644 libcxx/test/selftest/split/test.split.sh

diff --git a/libcxx/test/selftest/split/test.split.sh b/libcxx/test/selftest/split/test.split.sh
new file mode 100644
index 0000000000000..a72e3aadfc501
--- /dev/null
+++ b/libcxx/test/selftest/split/test.split.sh
@@ -0,0 +1,24 @@
+# Pre-delimiter comment.
+
+# RUN: grep 'int main' %{temp}/main.cpp
+# RUN: grep 'return 0' %{temp}/main.cpp
+# RUN: not grep -c 'Pre-delimiter' %{temp}/main.cpp
+# RUN: not grep -c 'foo' %{temp}/main.cpp
+# RUN: not grep -c '//---' %{temp}/main.cpp
+
+# RUN: grep foo %{temp}/input.txt
+# RUN: grep bar %{temp}/input.txt
+# RUN: not grep -c 'Pre-delimiter' %{temp}/input.txt
+# RUN: not grep -c 'int main' %{temp}/input.txt
+# RUN: not grep -c '//---' %{temp}/input.txt
+
+#--- main.cpp
+
+int main() {
+  return 0;
+}
+
+#--- input.txt
+
+foo
+bar
diff --git a/libcxx/utils/libcxx/test/format.py b/libcxx/utils/libcxx/test/format.py
index 49cbe8a8db618..19175c9fe35ea 100644
--- a/libcxx/utils/libcxx/test/format.py
+++ b/libcxx/utils/libcxx/test/format.py
@@ -281,6 +281,7 @@ def getTestsForPath(self, testSuite, pathInSuite, litConfig, localConfig):
             "[.]sh[.][^.]+$",
             "[.]gen[.][^.]+$",
             "[.]verify[.]cpp$",
+            "[.]split[.]sh$",
         ]
 
         sourcePath = testSuite.getSourcePath(pathInSuite)
@@ -365,6 +366,8 @@ def execute(self, test, litConfig):
             return self._executeShTest(test, litConfig, steps)
         elif re.search('[.]gen[.][^.]+$', filename): # This only happens when a generator test is not supported
             return self._executeShTest(test, litConfig, [])
+        elif re.search("[.]split[.]sh$", filename):
+            return self._executeSplitTest(test, litConfig)
         else:
             return lit.Test.Result(
                 lit.Test.UNRESOLVED, "Unknown test suffix for '{}'".format(filename)
@@ -418,6 +421,34 @@ def _generateGenTest(self, testSuite, pathInSuite, litConfig, localConfig):
                 f.write(content)
             yield lit.Test.Test(testSuite, (generatedFile,), localConfig)
 
+    # Split tests have following structure:
+    #
+    #    # RUN: use %{temp}/main.cpp
+    #    # RUN: use %{temp}/inputs.txt
+    #
+    #    #--- main.cpp
+    #    int main() { return 0; }
+    #
+    #    #--- input.txt
+    #    some inputs
+    #
+    # This function takes such test and creates the subfiles in the directory
+    # pointed to by the %{temp} substitution. Then it runs the original test file
+    # just like a regular shell LIT test.
+    def _executeSplitTest(self, test, litConfig):
+        with open(test.getSourcePath(), "r") as f:
+            content_to_split = f.read()
+            for subfile, content in self._splitFile(content_to_split):
+                tempDir, _ = _getTempPaths(test)
+                subfile_path = os.path.join(tempDir, subfile)
+                os.makedirs(os.path.dirname(subfile_path), exist_ok=True)
+                with open(subfile_path, "w") as sf:
+                    sf.write(content)
+
+        # Just as for regular .sh tests, the steps are already in the script.
+        steps = []
+        return self._executeShTest(test, litConfig, steps)
+
     def _splitFile(self, input):
         DELIM = r'^(//|#)---(.+)'
         lines = input.splitlines()
@@ -430,7 +461,15 @@ def _splitFile(self, input):
                     yield (currentFile, '\n'.join(thisFileContent))
                 currentFile = match.group(2).strip()
                 thisFileContent = []
-            assert currentFile is not None, f"Some input to split-file doesn't belong to any file, input was:\n{input}"
-            thisFileContent.append(line)
+
+            # Anything before the first match line is disregarded.
+            # E.g., .split. tests put all the RUN lines before any
+            # split delimiters.
+            if currentFile is None:
+                continue
+
+            # Don't put the delimiter itself into the split content.
+            if not match:
+                thisFileContent.append(line)
         if currentFile is not None:
             yield (currentFile, '\n'.join(thisFileContent))

>From 3a12dfde92ccb899d1f171d86301909c0d501612 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 2/6] [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 aea7af62aa8ff..c6d1d2cec7a37 100644
--- a/.github/workflows/libcxx-build-and-test.yaml
+++ b/.github/workflows/libcxx-build-and-test.yaml
@@ -58,7 +58,11 @@ jobs:
         with:
           persist-credentials: false
       - 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 }}
@@ -105,7 +109,11 @@ jobs:
         with:
           persist-credentials: false
       - 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 }}
@@ -162,7 +170,11 @@ jobs:
         with:
           persist-credentials: false
       - 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
@@ -220,7 +232,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
@@ -260,7 +272,7 @@ jobs:
           persist-credentials: false
       - 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 0b86743bb8fd95dc079f4a5744502af85c102825 Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Wed, 25 Mar 2026 11:43:43 +0000
Subject: [PATCH 3/6] [lldb][CommandObjectType] Add --wants-dereference flag

---
 lldb/source/Commands/CommandObjectType.cpp | 21 +++++++++++++++++----
 lldb/source/Commands/Options.td            |  4 ++++
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/lldb/source/Commands/CommandObjectType.cpp b/lldb/source/Commands/CommandObjectType.cpp
index 60a695c04ad95..c17645ef07bf7 100644
--- a/lldb/source/Commands/CommandObjectType.cpp
+++ b/lldb/source/Commands/CommandObjectType.cpp
@@ -67,14 +67,16 @@ class SynthAddOptions {
   bool m_skip_pointers;
   bool m_skip_references;
   bool m_cascade;
+  bool m_wants_deref;
   FormatterMatchType m_match_type;
   StringList m_target_types;
   std::string m_category;
 
-  SynthAddOptions(bool sptr, bool sref, bool casc,
+  SynthAddOptions(bool sptr, bool sref, bool casc, bool wants_deref,
                   FormatterMatchType match_type, std::string catg)
       : m_skip_pointers(sptr), m_skip_references(sref), m_cascade(casc),
-        m_match_type(match_type), m_category(catg) {}
+        m_wants_deref(wants_deref), m_match_type(match_type), m_category(catg) {
+  }
 
   typedef std::shared_ptr<SynthAddOptions> SharedPointer;
 };
@@ -322,6 +324,13 @@ class CommandObjectTypeSynthAdd : public CommandObjectParsed,
           error = Status::FromErrorStringWithFormat(
               "invalid value for cascade: %s", option_arg.str().c_str());
         break;
+      case 'D':
+        m_wants_deref = OptionArgParser::ToBoolean(option_arg, true, &success);
+        if (!success)
+          error = Status::FromErrorStringWithFormat(
+              "invalid value for wants-dereference: %s",
+              option_arg.str().c_str());
+        break;
       case 'P':
         handwrite_python = true;
         break;
@@ -361,6 +370,7 @@ class CommandObjectTypeSynthAdd : public CommandObjectParsed,
 
     void OptionParsingStarting(ExecutionContext *execution_context) override {
       m_cascade = true;
+      m_wants_deref = true;
       m_class_name = "";
       m_skip_pointers = false;
       m_skip_references = false;
@@ -379,6 +389,7 @@ class CommandObjectTypeSynthAdd : public CommandObjectParsed,
     bool m_cascade;
     bool m_skip_references;
     bool m_skip_pointers;
+    bool m_wants_deref;
     std::string m_class_name;
     bool m_input_python;
     std::string m_category;
@@ -454,7 +465,8 @@ class CommandObjectTypeSynthAdd : public CommandObjectParsed,
                     SyntheticChildren::Flags()
                         .SetCascades(options->m_cascade)
                         .SetSkipPointers(options->m_skip_pointers)
-                        .SetSkipReferences(options->m_skip_references),
+                        .SetSkipReferences(options->m_skip_references)
+                        .SetFrontEndWantsDereference(options->m_wants_deref),
                     class_name_str.c_str());
 
                 lldb::TypeCategoryImplSP category;
@@ -2131,7 +2143,8 @@ bool CommandObjectTypeSynthAdd::Execute_HandwritePython(
     Args &command, CommandReturnObject &result) {
   auto options = std::make_unique<SynthAddOptions>(
       m_options.m_skip_pointers, m_options.m_skip_references,
-      m_options.m_cascade, m_options.m_match_type, m_options.m_category);
+      m_options.m_cascade, m_options.m_cascade, m_options.m_match_type,
+      m_options.m_category);
 
   for (auto &entry : command.entries()) {
     if (entry.ref().empty()) {
diff --git a/lldb/source/Commands/Options.td b/lldb/source/Commands/Options.td
index a4d72010d2c4c..6301fffe733dc 100644
--- a/lldb/source/Commands/Options.td
+++ b/lldb/source/Commands/Options.td
@@ -2267,6 +2267,10 @@ let Command = "type synth add" in {
   def type_synth_add_cascade : Option<"cascade", "C">,
                                Arg<"Boolean">,
                                Desc<"If true, cascade through typedef chains.">;
+  def type_synth_add_wants_deref
+      : Option<"wants-dereference", "D">,
+        Arg<"Boolean">,
+        Desc<"If true, dereference pointers and references.">;
   def type_synth_add_skip_pointers
       : Option<"skip-pointers", "p">,
         Desc<"Don't use this format for pointers-to-type objects.">;

>From 13724e495c1b1ed50a70acad9f65a0bc5f18d682 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 4/6] [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)

>From 1edffc921e5152f1a96a050cead3796247bebb8b Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 24 Mar 2026 08:25:51 +0000
Subject: [PATCH 5/6] fixup! remove redundant methods and IsValid checks

---
 .../lldb/libcxx/libcxx_vector_formatter.py    | 79 +++++--------------
 1 file changed, 21 insertions(+), 58 deletions(-)

diff --git a/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py b/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py
index 368f121c2808f..f7217a36c67c1 100644
--- a/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py
+++ b/libcxx/utils/lldb/libcxx/libcxx_vector_formatter.py
@@ -15,21 +15,21 @@ 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():
+    if cap_sp:
         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():
+    if not end_cap_sp:
         return None
 
     # Get first value of compressed pair
     value_sp = end_cap_sp.GetChildMemberWithName("__value_")
-    if value_sp and value_sp.IsValid():
+    if value_sp:
         return value_sp
 
     first_sp = end_cap_sp.GetChildMemberWithName("__first_")
-    if first_sp and first_sp.IsValid():
+    if first_sp:
         return first_sp
 
     return None
@@ -44,7 +44,6 @@ def __init__(self, valobj, internal_dict):
         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:
@@ -53,14 +52,7 @@ def num_children(self):
         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:
+        if start_val == 0 or finish_val == 0:
             return 0
 
         if start_val > finish_val:
@@ -72,16 +64,6 @@ def num_children(self):
 
         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
@@ -91,7 +73,7 @@ def get_child_at_index(self, index):
 
         name = "[%d]" % index
         target = self.valobj.GetTarget()
-        if not target or not target.IsValid():
+        if not target:
             return None
 
         addr = lldb.SBAddress(offset, target)
@@ -102,11 +84,11 @@ def update(self):
         self.m_finish = None
 
         data_sp = get_data_pointer(self.valobj)
-        if not data_sp or not data_sp.IsValid():
+        if not data_sp:
             return False
 
         self.m_element_type = data_sp.GetType().GetPointeeType()
-        if not self.m_element_type.IsValid():
+        if not self.m_element_type:
             return False
 
         size = self.m_element_type.GetByteSize()
@@ -118,7 +100,7 @@ def update(self):
         begin_sp = self.valobj.GetChildMemberWithName("__begin_")
         end_sp = self.valobj.GetChildMemberWithName("__end_")
 
-        if not begin_sp or not end_sp:
+        if not begin_sp:
             return False
 
         self.m_start = begin_sp
@@ -126,9 +108,6 @@ def update(self):
 
         return True
 
-    def has_children(self):
-        return True
-
 
 class LibCxxVectorBoolSyntheticFrontEnd:
     """Synthetic children frontend for libc++ std::vector<bool>."""
@@ -139,29 +118,16 @@ def __init__(self, valobj, internal_dict):
         self.m_count = 0
         self.m_base_data_address = 0
         self.m_children = {}
-        self.update()
+
         # Get bool type
-        if valobj.IsValid():
+        if valobj:
             target = valobj.GetTarget()
-            if target and target.IsValid():
+            if target:
                 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]
@@ -172,7 +138,7 @@ def get_child_at_index(self, index):
         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():
+        if not self.m_bool_type:
             return None
 
         # Calculate byte and bit index
@@ -182,7 +148,7 @@ def get_child_at_index(self, index):
         byte_location = self.m_base_data_address + byte_idx
 
         process = self.valobj.GetProcess()
-        if not process or not process.IsValid():
+        if not process:
             return None
 
         error = lldb.SBError()
@@ -209,11 +175,11 @@ def get_child_at_index(self, index):
 
         name = "[%d]" % index
         target = self.valobj.GetTarget()
-        if not target or not target.IsValid():
+        if not target:
             return None
 
         retval_sp = target.CreateValueFromData(name, data, self.m_bool_type)
-        if retval_sp and retval_sp.IsValid():
+        if retval_sp:
             self.m_children[index] = retval_sp
 
         return retval_sp
@@ -221,11 +187,11 @@ def get_child_at_index(self, index):
     def update(self):
         self.m_children = {}
 
-        if not self.valobj.IsValid():
+        if not self.valobj:
             return False
 
         size_sp = self.valobj.GetChildMemberWithName("__size_")
-        if not size_sp or not size_sp.IsValid():
+        if not size_sp:
             return False
 
         self.m_count = size_sp.GetValueAsUnsigned(0)
@@ -233,7 +199,7 @@ def update(self):
             return True
 
         begin_sp = self.valobj.GetChildMemberWithName("__begin_")
-        if not begin_sp or not begin_sp.IsValid():
+        if not begin_sp:
             self.m_count = 0
             return False
 
@@ -244,16 +210,13 @@ def update(self):
 
         return True
 
-    def has_children(self):
-        return True
-
 
 def LibCxxStdVectorSyntheticFrontendCreator(valobj, internal_dict):
-    if not valobj or not valobj.IsValid():
+    if not valobj:
         return None
 
     compiler_type = valobj.GetType()
-    if not compiler_type.IsValid() or compiler_type.GetNumberOfTemplateArguments() == 0:
+    if not compiler_type or compiler_type.GetNumberOfTemplateArguments() == 0:
         return None
 
     arg_type = compiler_type.GetTemplateArgumentType(0)

>From e8bd1e8a0d36198dc00e9d55574a72c12188eeea Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 24 Mar 2026 09:00:54 +0000
Subject: [PATCH 6/6] fixup! more tests

---
 ....test.in => vector_bool_formatter.test.in} |   2 +-
 ....cpp => vector_bool_formatter_test.sh.cpp} |  13 +-
 .../lldb/vector_formatter_test.split.sh       | 158 ++++++++++++++++++
 libcxx/utils/lldb/libcxx/libcxx.py            |   6 +-
 4 files changed, 169 insertions(+), 10 deletions(-)
 rename libcxx/test/libcxx/lldb/{vector_formatter.test.in => vector_bool_formatter.test.in} (71%)
 rename libcxx/test/libcxx/lldb/{vector_formatter_test.sh.cpp => vector_bool_formatter_test.sh.cpp} (66%)
 create mode 100644 libcxx/test/libcxx/lldb/vector_formatter_test.split.sh

diff --git a/libcxx/test/libcxx/lldb/vector_formatter.test.in b/libcxx/test/libcxx/lldb/vector_bool_formatter.test.in
similarity index 71%
rename from libcxx/test/libcxx/lldb/vector_formatter.test.in
rename to libcxx/test/libcxx/lldb/vector_bool_formatter.test.in
index 548c730573a37..e9fadc9f4f094 100644
--- a/libcxx/test/libcxx/lldb/vector_formatter.test.in
+++ b/libcxx/test/libcxx/lldb/vector_bool_formatter.test.in
@@ -1,3 +1,3 @@
 break set -p return -X main
 run
-frame var vi
+frame var vb
diff --git a/libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp b/libcxx/test/libcxx/lldb/vector_bool_formatter_test.sh.cpp
similarity index 66%
rename from libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp
rename to libcxx/test/libcxx/lldb/vector_bool_formatter_test.sh.cpp
index 893c2a8a08d55..9864b506efc62 100644
--- a/libcxx/test/libcxx/lldb/vector_formatter_test.sh.cpp
+++ b/libcxx/test/libcxx/lldb/vector_bool_formatter_test.sh.cpp
@@ -8,18 +8,19 @@
 // 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
+// RUN:         -s %S/vector_bool_formatter.test.in %t.out | FileCheck %s
 
 #include <vector>
 
 int main(int, char**) {
-  std::vector<int> vi = {1, 2};
+  std::vector<bool> vb = {true, false, false};
 
   return 0;
 }
 
-// CHECK: (lldb) frame var vi
-// CHECK-NEXT: (std::vector<int>) vi = size=2 {
-// CHECK-NEXT:   [0] = 1
-// CHECK-NEXT:   [1] = 2
+// CHECK: (lldb) frame var vb
+// CHECK-NEXT: (std::vector<bool>) vb = size=3 {
+// CHECK-NEXT:   [0] = true
+// CHECK-NEXT:   [1] = false
+// CHECK-NEXT:   [2] = false
 // CHECK-NEXT: }
diff --git a/libcxx/test/libcxx/lldb/vector_formatter_test.split.sh b/libcxx/test/libcxx/lldb/vector_formatter_test.split.sh
new file mode 100644
index 0000000000000..9cd8607960d22
--- /dev/null
+++ b/libcxx/test/libcxx/lldb/vector_formatter_test.split.sh
@@ -0,0 +1,158 @@
+# REQUIRES: host-has-lldb-with-python
+# REQUIRES: has-filecheck
+# REQUIRES: optimization=none
+#
+# RUN: %{cxx} %{flags} %{temp}/main.cpp -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 interpreter.stop-command-source-on-error false" \
+# RUN:         -o "command script import %S/../../../utils/lldb/libcxx/libcxx.py" \
+# RUN:         -s %{temp}/lldb.commands %t.out 2>&1 | %{filecheck} %s
+
+# CHECK: (lldb) frame var empty
+# CHECK-NEXT: (std::vector<float>) empty = size=0 {}
+
+# CHECK: (lldb) frame var vi
+# CHECK-NEXT: (std::vector<int>) vi = size=2 {
+# CHECK-NEXT:   [0] = 1
+# CHECK-NEXT:   [1] = 2
+# CHECK-NEXT: }
+
+# CHECK: (lldb) frame var empty[0]
+# CHECK: array index 0 is not valid
+
+# CHECK: (lldb) continue
+# CHECK: (lldb) frame var vi
+# CHECK-NEXT: (std::vector<int>) vi = size=5 {
+# CHECK-NEXT:   [0] = 1
+# CHECK-NEXT:   [1] = 2
+# CHECK-NEXT:   [2] = 5
+# CHECK-NEXT:   [3] = 4
+# CHECK-NEXT:   [4] = 0
+# CHECK-NEXT: }
+
+# CHECK: (lldb) frame var vi[2]
+# CHECK-NEXT: (int) vi[2] = 5
+
+# CHECK: (lldb) frame var vi[0]
+# CHECK-NEXT: (int) vi[0] = 1
+
+# CHECK: (lldb) continue
+# CHECK: (lldb) frame var vi[0]
+# CHECK-NEXT: array index 0 is not valid
+
+# CHECK: (lldb) frame var vi
+# CHECK-NEXT: (std::vector<int>) vi = size=0 {}
+
+# CHECK: (lldb) continue
+# CHECK: (lldb) frame var vec_of_vec
+# CHECK-NEXT: (std::vector<std::vector<int> >) vec_of_vec = size=1 {
+# CHECK-NEXT:   [0] = size=2 {
+# CHECK-NEXT:     [0] = 10
+# CHECK-NEXT:     [1] = 11
+# CHECK-NEXT:   }
+# CHECK-NEXT: }
+
+# CHECK: (lldb) frame var vec_of_vec[0]
+# CHECK-NEXT: (std::vector<int>) vec_of_vec[0] = size=2 {
+# CHECK-NEXT:   [0] = 10
+# CHECK-NEXT:   [1] = 11
+# CHECK-NEXT: }
+
+# CHECK: (lldb) continue
+# CHECK: (lldb) frame var vs
+# CHECK-NEXT: (std::vector<std::string>) vs = size=2 {
+# CHECK-NEXT:   [0] = "Foo"
+# CHECK-NEXT:   [1] = "Bar"
+# CHECK-NEXT: }
+
+# CHECK: (lldb) frame var vs[1]
+# CHECK-NEXT: (std::string) vs[1] = "Bar"
+
+# CHECK: (lldb) continue
+# CHECK: (lldb) frame var vec_ref
+# CHECK-NEXT: (const std::vector<int> &) vec_ref = 0x{{.*}} size=2: {
+# CHECK-NEXT:   [0] = 10
+# CHECK-NEXT:   [1] = 11
+# CHECK-NEXT: }
+
+# CHECK: (lldb) frame var vec_ref[1]
+# CHECK-NEXT: (int) vec_ref[1] = 11
+
+# CHECK: (lldb) frame var vec_ptr
+# CHECK-NEXT: (const std::vector<int> *) vec_ptr = 0x{{.*}} size=2
+
+# CHECK: (lldb) frame var vec_ptr[1]
+# CHECK-NEXT: (int) vec_ptr[1] = 11
+
+#--- main.cpp
+
+#include <vector>
+#include <string>
+
+void break_here() {}
+
+int main() {
+  std::vector<float> empty;
+  std::vector<int> vi = {1, 2};
+
+  break_here();
+
+  vi.push_back(5);
+  vi.push_back(4);
+  vi.push_back(0);
+
+  break_here();
+
+  vi.clear();
+
+  break_here();
+
+  vi.push_back(10);
+  vi.push_back(11);
+  std::vector<std::vector<int>> vec_of_vec{vi};
+
+  break_here();
+
+  std::vector<std::string>  vs{
+    "Foo",
+    "Bar"
+  };
+
+  break_here();
+
+  [[maybe_unused]] const auto &vec_ref = vi;
+  [[maybe_unused]] const auto *vec_ptr = &vi;
+
+  break_here();
+}
+
+#--- lldb.commands
+break set -p break_here -X main
+run
+frame var empty
+frame var vi
+frame var empty[0]
+
+continue
+frame var vi
+frame var vi[2]
+frame var vi[0]
+
+continue
+frame var vi[0]
+frame var vi
+
+continue
+frame var vec_of_vec
+frame var vec_of_vec[0]
+
+continue
+frame var vs
+frame var vs[1]
+
+continue
+frame var vec_ref
+frame var vec_ref[1]
+frame var vec_ptr
+frame var vec_ptr[1]
diff --git a/libcxx/utils/lldb/libcxx/libcxx.py b/libcxx/utils/lldb/libcxx/libcxx.py
index 3e904cbd06758..46877527ddebb 100644
--- a/libcxx/utils/lldb/libcxx/libcxx.py
+++ b/libcxx/utils/lldb/libcxx/libcxx.py
@@ -2,8 +2,8 @@
 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 register_synthetic(debugger: lldb.SBDebugger, regex: str, class_name: str, extra_flags: str = ""):
+    debugger.HandleCommand(f'type synthetic add {extra_flags} -x "{regex}" -l {__name__}.{class_name} -w "cplusplus-py"')
 
 def __lldb_init_module(debugger, dict):
     register_synthetic(debugger, "^std::__[[:alnum:]]+::map<.+> >$", "LibcxxStdMapSyntheticProvider")
@@ -11,7 +11,7 @@ def __lldb_init_module(debugger, dict):
     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")
+    register_synthetic(debugger, "^std::__[[:alnum:]]+::vector<.+>$", "LibCxxStdVectorSyntheticFrontendCreator", "--wants-dereference")
 
     # Enables registered formatters in LLDB.
     debugger.HandleCommand('type category enable cplusplus-py')



More information about the libcxx-commits mailing list