Author: Charles Zablit
Date: 2026-01-19T11:43:45+01:00
New Revision: e43331f94a594421180ee783b5b92e5dea70f4c9

URL: 
https://github.com/llvm/llvm-project/commit/e43331f94a594421180ee783b5b92e5dea70f4c9
DIFF: 
https://github.com/llvm/llvm-project/commit/e43331f94a594421180ee783b5b92e5dea70f4c9.diff

LOG: [lldb] add a marker before hidden frames (#167550)

**This patch adds a marker to make hidden frames more explicit.**

---

Hidden frames can be confusing for some users, who see that the indexes
of the frames in a backtrace are not contiguous. This patch aims to
lessen the confusion by adding a delimiter for the first and last non
hidden frame, i.e the boundaries.

IDE's like Xcode and VSCode represent those in the UI by having the
hidden frames either greyed out or collapsed.

It's not possible to do this in the CLI, therefore, this patch makes use
of 2 unicode characters to mark the beginning and end of the hidden
frames range.

This patch depends on:
- https://github.com/llvm/llvm-project/pull/168603

# Examples

In the example below, frame `#2` to `#7` are is hidden, and therefore,
frame `#1` is the first non hidden frame of the range while frame `#8`
is the last non hidden frame:
<img width="488" height="112" alt="Screenshot 2025-11-18 at 18 41 11"
src="https://github.com/user-attachments/assets/a21431da-9729-4cf0-a6bc-024aa306fc45";
/>

If the selected frame is one of the 2 boundary frames, we replace the
delimiter character with the select character (`*`).

<img width="487" height="111" alt="Screenshot 2025-11-18 at 18 41 03"
src="https://github.com/user-attachments/assets/5616fa81-6db6-457d-9d1e-bbe46e710c26";
/>
<img width="488" height="111" alt="Screenshot 2025-11-18 at 18 40 55"
src="https://github.com/user-attachments/assets/93dfa6cf-0956-4718-b31c-f965ec72b56d";
/>

Added: 
    lldb/test/API/terminal/hidden_frame_markers/Makefile
    lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
    lldb/test/API/terminal/hidden_frame_markers/main.cpp

Modified: 
    lldb/include/lldb/Core/Debugger.h
    lldb/include/lldb/Target/StackFrame.h
    lldb/include/lldb/Target/StackFrameList.h
    lldb/packages/Python/lldbsuite/test/decorators.py
    lldb/source/Core/CoreProperties.td
    lldb/source/Core/Debugger.cpp
    lldb/source/Target/StackFrame.cpp
    lldb/source/Target/StackFrameList.cpp
    lldb/source/Target/Thread.cpp

Removed: 
    


################################################################################
diff  --git a/lldb/include/lldb/Core/Debugger.h 
b/lldb/include/lldb/Core/Debugger.h
index a39413c06340c..dadccaeb5d17b 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -340,6 +340,8 @@ class Debugger : public 
std::enable_shared_from_this<Debugger>,
 
   bool SetUseSourceCache(bool use_source_cache);
 
+  bool GetMarkHiddenFrames() const;
+
   bool GetHighlightSource() const;
 
   lldb::StopShowColumn GetStopShowColumn() const;

diff  --git a/lldb/include/lldb/Target/StackFrame.h 
b/lldb/include/lldb/Target/StackFrame.h
index 46922448d6e59..0d07515bf0f13 100644
--- a/lldb/include/lldb/Target/StackFrame.h
+++ b/lldb/include/lldb/Target/StackFrame.h
@@ -363,7 +363,7 @@ class StackFrame : public ExecutionContextScope,
   /// \param [in] frame_marker
   ///   Optional string that will be prepended to the frame output description.
   virtual void DumpUsingSettingsFormat(Stream *strm, bool show_unique = false,
-                                       const char *frame_marker = nullptr);
+                                       const llvm::StringRef frame_marker = 
"");
 
   /// Print a description for this frame using a default format.
   ///
@@ -400,7 +400,7 @@ class StackFrame : public ExecutionContextScope,
   ///   Returns true if successful.
   virtual bool GetStatus(Stream &strm, bool show_frame_info, bool show_source,
                          bool show_unique = false,
-                         const char *frame_marker = nullptr);
+                         const llvm::StringRef frame_marker = "");
 
   /// Query whether this frame is a concrete frame on the call stack, or if it
   /// is an inlined frame derived from the debug information and presented by

diff  --git a/lldb/include/lldb/Target/StackFrameList.h 
b/lldb/include/lldb/Target/StackFrameList.h
index c096fe3ff61a0..c54df941e3aa9 100644
--- a/lldb/include/lldb/Target/StackFrameList.h
+++ b/lldb/include/lldb/Target/StackFrameList.h
@@ -49,6 +49,22 @@ class StackFrameList : public 
std::enable_shared_from_this<StackFrameList> {
   /// Resets the selected frame index of this object.
   void ClearSelectedFrameIndex();
 
+  /// Returns \p true if the next frame is hidden.
+  bool IsNextFrameHidden(lldb_private::StackFrame &frame);
+
+  /// Returns \p true if the previous frame is hidden.
+  bool IsPreviousFrameHidden(lldb_private::StackFrame &frame);
+
+  /// Returns the stack frame marker depending on if \p frame_sp:
+  /// @li is selected: *
+  /// @li is the first non hidden frame: ﹍
+  /// @li is the last non hidden frame: ﹉
+  ///
+  /// If the terminal does not support Unicode rendering, the hidden frame
+  /// markers are replaced with whitespaces.
+  std::string FrameMarker(lldb::StackFrameSP frame_sp,
+                          lldb::StackFrameSP selected_frame_sp);
+
   /// Get the currently selected frame index.
   /// We should only call SelectMostRelevantFrame if (a) the user hasn't 
already
   /// selected a frame, and (b) if this really is a user facing
@@ -96,7 +112,8 @@ class StackFrameList : public 
std::enable_shared_from_this<StackFrameList> {
   size_t GetStatus(Stream &strm, uint32_t first_frame, uint32_t num_frames,
                    bool show_frame_info, uint32_t num_frames_with_source,
                    bool show_unique = false, bool show_hidden = false,
-                   const char *frame_marker = nullptr);
+                   bool show_hidden_marker = true,
+                   bool show_selected_frame = false);
 
   /// Returns whether we have currently fetched all the frames of a stack.
   bool WereAllFramesFetched() const;

diff  --git a/lldb/packages/Python/lldbsuite/test/decorators.py 
b/lldb/packages/Python/lldbsuite/test/decorators.py
index a7df9fe63badc..d7c8ed1da00e0 100644
--- a/lldb/packages/Python/lldbsuite/test/decorators.py
+++ b/lldb/packages/Python/lldbsuite/test/decorators.py
@@ -439,6 +439,35 @@ def impl(func):
     return impl
 
 
+def unicode_test(func):
+    """Decorate the item as a test which requires Unicode to be enabled.
+
+    lldb checks the value of the `LANG` environment variable for the substring 
"utf-8"
+    to determine if the terminal supports Unicode (except on Windows, were we 
assume
+    it's always supported).
+    This decorator sets LANG to `utf-8` before running the test and resets it 
to its
+    previous value afterwards.
+    """
+
+    def unicode_wrapped(*args, **kwargs):
+        import os
+
+        previous_lang = os.environ.get("LANG", None)
+        os.environ["LANG"] = "en_US.UTF-8"
+        try:
+            func(*args, **kwargs)
+        except Exception as err:
+            raise err
+        finally:
+            # Reset the value, whether the test failed or not.
+            if previous_lang is not None:
+                os.environ["LANG"] = previous_lang
+            else:
+                del os.environ["LANG"]
+
+    return unicode_wrapped
+
+
 def no_debug_info_test(func):
     """Decorate the item as a test what don't use any debug info. If this 
annotation is specified
     then the test runner won't generate a separate test for each debug info 
format."""

diff  --git a/lldb/source/Core/CoreProperties.td 
b/lldb/source/Core/CoreProperties.td
index 99bb5a3fc6f73..f39973fdc7a10 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -114,6 +114,10 @@ let Definition = "debugger" in {
     Global,
     DefaultTrue,
     Desc<"If true, LLDB will highlight the displayed source code.">;
+  def MarkHiddenFrames: Property<"mark-hidden-frames", "Boolean">,
+    Global,
+    DefaultTrue,
+    Desc<"If true, LLDB will add a marker to delimit hidden frames in 
backtraces.">;
   def StopShowColumn: Property<"stop-show-column", "Enum">,
     DefaultEnumValue<"eStopShowColumnAnsiOrCaret">,
     EnumValues<"OptionEnumValues(s_stop_show_column_values)">,

diff  --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index 669dd90cd324f..65acaa5fb9f4d 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -584,6 +584,13 @@ bool Debugger::SetUseSourceCache(bool b) {
   }
   return ret;
 }
+
+bool Debugger::GetMarkHiddenFrames() const {
+  const uint32_t idx = ePropertyMarkHiddenFrames;
+  return GetPropertyAtIndexAs<bool>(
+      idx, g_debugger_properties[idx].default_uint_value != 0);
+}
+
 bool Debugger::GetHighlightSource() const {
   const uint32_t idx = ePropertyHighlightSource;
   return GetPropertyAtIndexAs<bool>(

diff  --git a/lldb/source/Target/StackFrame.cpp 
b/lldb/source/Target/StackFrame.cpp
index 340607e14abed..3ef96a46517c9 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -1945,7 +1945,7 @@ bool StackFrame::DumpUsingFormat(Stream &strm,
 }
 
 void StackFrame::DumpUsingSettingsFormat(Stream *strm, bool show_unique,
-                                         const char *frame_marker) {
+                                         const llvm::StringRef frame_marker) {
   if (strm == nullptr)
     return;
 
@@ -2044,7 +2044,8 @@ bool StackFrame::HasCachedData() const {
 }
 
 bool StackFrame::GetStatus(Stream &strm, bool show_frame_info, bool 
show_source,
-                           bool show_unique, const char *frame_marker) {
+                           bool show_unique,
+                           const llvm::StringRef frame_marker) {
   if (show_frame_info) {
     strm.Indent();
     DumpUsingSettingsFormat(&strm, show_unique, frame_marker);

diff  --git a/lldb/source/Target/StackFrameList.cpp 
b/lldb/source/Target/StackFrameList.cpp
index e6112f8f3264b..e0c6aa0542f4d 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -27,6 +27,7 @@
 #include "lldb/Utility/LLDBLog.h"
 #include "lldb/Utility/Log.h"
 #include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/Support/ConvertUTF.h"
 
 #include <memory>
 
@@ -929,11 +930,43 @@ 
StackFrameList::GetStackFrameSPForStackFramePtr(StackFrame *stack_frame_ptr) {
   return ret_sp;
 }
 
+bool StackFrameList::IsNextFrameHidden(lldb_private::StackFrame &frame) {
+  uint32_t frame_idx = frame.GetFrameIndex();
+  StackFrameSP frame_sp = GetFrameAtIndex(frame_idx + 1);
+  if (!frame_sp)
+    return false;
+  return frame_sp->IsHidden();
+}
+
+bool StackFrameList::IsPreviousFrameHidden(lldb_private::StackFrame &frame) {
+  uint32_t frame_idx = frame.GetFrameIndex();
+  if (frame_idx == 0)
+    return false;
+  StackFrameSP frame_sp = GetFrameAtIndex(frame_idx - 1);
+  if (!frame_sp)
+    return false;
+  return frame_sp->IsHidden();
+}
+
+std::string StackFrameList::FrameMarker(lldb::StackFrameSP frame_sp,
+                                        lldb::StackFrameSP selected_frame_sp) {
+  if (frame_sp == selected_frame_sp)
+    return Terminal::SupportsUnicode() ? u8" * " : u8"* ";
+  else if (!Terminal::SupportsUnicode())
+    return u8"  ";
+  else if (IsPreviousFrameHidden(*frame_sp))
+    return u8"﹉ ";
+  else if (IsNextFrameHidden(*frame_sp))
+    return u8"﹍ ";
+  return u8"   ";
+}
+
 size_t StackFrameList::GetStatus(Stream &strm, uint32_t first_frame,
                                  uint32_t num_frames, bool show_frame_info,
                                  uint32_t num_frames_with_source,
                                  bool show_unique, bool show_hidden,
-                                 const char *selected_frame_marker) {
+                                 bool show_hidden_marker,
+                                 bool show_selected_frame) {
   size_t num_frames_displayed = 0;
 
   if (num_frames == 0)
@@ -951,25 +984,17 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t 
first_frame,
 
   StackFrameSP selected_frame_sp =
       m_thread.GetSelectedFrame(DoNoSelectMostRelevantFrame);
-  const char *unselected_marker = nullptr;
   std::string buffer;
-  if (selected_frame_marker) {
-    size_t len = strlen(selected_frame_marker);
-    buffer.insert(buffer.begin(), len, ' ');
-    unselected_marker = buffer.c_str();
-  }
-  const char *marker = nullptr;
+  std::string marker;
   for (frame_idx = first_frame; frame_idx < last_frame; ++frame_idx) {
     frame_sp = GetFrameAtIndex(frame_idx);
     if (!frame_sp)
       break;
 
-    if (selected_frame_marker != nullptr) {
-      if (frame_sp == selected_frame_sp)
-        marker = selected_frame_marker;
-      else
-        marker = unselected_marker;
-    }
+    if (show_selected_frame)
+      marker = FrameMarker(frame_sp, selected_frame_sp);
+    else
+      marker = FrameMarker(frame_sp, nullptr);
 
     // Hide uninteresting frames unless it's the selected frame.
     if (!show_hidden && frame_sp != selected_frame_sp && frame_sp->IsHidden())
@@ -983,7 +1008,6 @@ size_t StackFrameList::GetStatus(Stream &strm, uint32_t 
first_frame,
             m_thread.GetID(), num_frames_displayed))
       break;
 
-
     if (!frame_sp->GetStatus(strm, show_frame_info,
                              num_frames_with_source > (first_frame - 
frame_idx),
                              show_unique, marker))

diff  --git a/lldb/source/Target/Thread.cpp b/lldb/source/Target/Thread.cpp
index 70d8650662348..1e3c7867eeca1 100644
--- a/lldb/source/Target/Thread.cpp
+++ b/lldb/source/Target/Thread.cpp
@@ -1888,9 +1888,9 @@ size_t Thread::GetStatus(Stream &strm, uint32_t 
start_frame,
                          uint32_t num_frames, uint32_t num_frames_with_source,
                          bool stop_format, bool show_hidden, bool only_stacks) 
{
 
+  ExecutionContext exe_ctx(shared_from_this());
+  Target *target = exe_ctx.GetTargetPtr();
   if (!only_stacks) {
-    ExecutionContext exe_ctx(shared_from_this());
-    Target *target = exe_ctx.GetTargetPtr();
     Process *process = exe_ctx.GetProcessPtr();
     strm.Indent();
     bool is_selected = false;
@@ -1924,16 +1924,19 @@ size_t Thread::GetStatus(Stream &strm, uint32_t 
start_frame,
 
     const bool show_frame_info = true;
     const bool show_frame_unique = only_stacks;
-    const char *selected_frame_marker = nullptr;
+    bool show_selected_frame = false;
     if (num_frames == 1 || only_stacks ||
         (GetID() != 
GetProcess()->GetThreadList().GetSelectedThread()->GetID()))
       strm.IndentMore();
     else
-      selected_frame_marker = "* ";
+      show_selected_frame = true;
 
+    bool show_hidden_marker =
+        target && target->GetDebugger().GetMarkHiddenFrames();
     num_frames_shown = GetStackFrameList()->GetStatus(
         strm, start_frame, num_frames, show_frame_info, num_frames_with_source,
-        show_frame_unique, show_hidden, selected_frame_marker);
+        show_frame_unique, show_hidden, show_hidden_marker,
+        show_selected_frame);
     if (num_frames == 1)
       strm.IndentLess();
     strm.IndentLess();
@@ -2033,9 +2036,13 @@ size_t Thread::GetStackFrameStatus(Stream &strm, 
uint32_t first_frame,
                                    uint32_t num_frames, bool show_frame_info,
                                    uint32_t num_frames_with_source,
                                    bool show_hidden) {
-  return GetStackFrameList()->GetStatus(strm, first_frame, num_frames,
-                                        show_frame_info, 
num_frames_with_source,
-                                        /*show_unique*/ false, show_hidden);
+  ExecutionContext exe_ctx(shared_from_this());
+  Target *target = exe_ctx.GetTargetPtr();
+  bool show_hidden_marker =
+      target && target->GetDebugger().GetMarkHiddenFrames();
+  return GetStackFrameList()->GetStatus(
+      strm, first_frame, num_frames, show_frame_info, num_frames_with_source,
+      /*show_unique*/ false, show_hidden, show_hidden_marker);
 }
 
 Unwind &Thread::GetUnwinder() {

diff  --git a/lldb/test/API/terminal/hidden_frame_markers/Makefile 
b/lldb/test/API/terminal/hidden_frame_markers/Makefile
new file mode 100644
index 0000000000000..99998b20bcb05
--- /dev/null
+++ b/lldb/test/API/terminal/hidden_frame_markers/Makefile
@@ -0,0 +1,3 @@
+CXX_SOURCES := main.cpp
+
+include Makefile.rules

diff  --git 
a/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py 
b/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
new file mode 100644
index 0000000000000..178d97fce17c2
--- /dev/null
+++ b/lldb/test/API/terminal/hidden_frame_markers/TestHiddenFrameMarkers.py
@@ -0,0 +1,97 @@
+"""
+Test that hidden frames are delimited with markers.
+"""
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class HiddenFrameMarkerTest(TestBase):
+    @unicode_test
+    def test_hidden_frame_markers(self):
+        """Test that hidden frame markers are rendered in backtraces"""
+        self.build()
+        lldbutil.run_to_source_breakpoint(
+            self, "// break here", lldb.SBFileSpec("main.cpp")
+        )
+        self.expect(
+            "bt",
+            substrs=[
+                "   * frame #0:",
+                "  ﹍ frame #1:",
+                "  ﹉ frame #7:",
+                "     frame #8:",
+                "     frame #9:",
+            ],
+        )
+
+        self.runCmd("f 1")
+        self.expect(
+            "bt",
+            substrs=[
+                "     frame #0:",
+                "   * frame #1:",
+                "  ﹉ frame #7:",
+                "     frame #8:",
+                "     frame #9:",
+            ],
+        )
+
+        self.runCmd("f 7")
+        self.expect(
+            "bt",
+            substrs=[
+                "     frame #0:",
+                "  ﹍ frame #1:",
+                "   * frame #7:",
+                "     frame #8:",
+                "     frame #9:",
+            ],
+        )
+
+    def test_hidden_frame_markers(self):
+        """
+        Test that hidden frame markers are not rendered in backtraces when
+        mark-hidden-frames is set to false
+        """
+        self.build()
+        self.runCmd("settings set mark-hidden-frames 0")
+        lldbutil.run_to_source_breakpoint(
+            self, "// break here", lldb.SBFileSpec("main.cpp")
+        )
+        self.expect(
+            "bt",
+            substrs=[
+                "  * frame #0:",
+                "    frame #1:",
+                "    frame #7:",
+                "    frame #8:",
+                "    frame #9:",
+            ],
+        )
+
+        self.runCmd("f 1")
+        self.expect(
+            "bt",
+            substrs=[
+                "    frame #0:",
+                "  * frame #1:",
+                "    frame #7:",
+                "    frame #8:",
+                "    frame #9:",
+            ],
+        )
+
+        self.runCmd("f 7")
+        self.expect(
+            "bt",
+            substrs=[
+                "    frame #0:",
+                "    frame #1:",
+                "  * frame #7:",
+                "    frame #8:",
+                "    frame #9:",
+            ],
+        )

diff  --git a/lldb/test/API/terminal/hidden_frame_markers/main.cpp 
b/lldb/test/API/terminal/hidden_frame_markers/main.cpp
new file mode 100644
index 0000000000000..c0b7e0884538a
--- /dev/null
+++ b/lldb/test/API/terminal/hidden_frame_markers/main.cpp
@@ -0,0 +1,12 @@
+#include <functional>
+#include <iostream>
+
+static void target() {
+  int a = 0; // break here
+}
+
+int main() {
+  std::function<void()> fn = [] { target(); };
+  fn();
+  return 0;
+}


        
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to