[Lldb-commits] [lldb] [lldb] Add support for PC-less scripted frames (PR #170805)

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Fri Dec 5 12:21:23 PST 2025


https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/170805

>From 1fdd0de7881070033e70f3ea6846e1d24b61c729 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Fri, 5 Dec 2025 12:20:40 -0800
Subject: [PATCH] [lldb] Add support for PC-less scripted frames

Scripted frames that materialize Python functions or other non-native
code are PC-less by design, meaning they don't have valid program counter
values. Previously, these frames would display invalid addresses
(0xffffffffffffffff) in backtrace output.

This patch updates FormatEntity to detect and suppress invalid address
display for PC-less frames, adds fallback to frame methods when symbol
context is unavailable, and modifies StackFrame::GetSymbolContext to
skip PC-based symbol resolution for invalid addresses.

The changes enable PC-less frames to display cleanly with proper
function names, file paths, and line numbers, and allow for source
display of foreign sources (like Python). Includes comprehensive
test coverage demonstrating frames pointing to Python source files.

Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
 lldb/include/lldb/Target/StackID.h            |   1 +
 lldb/source/Core/CoreProperties.td            |   4 +-
 lldb/source/Core/FormatEntity.cpp             | 162 ++++++++------
 .../Process/scripted/ScriptedFrame.cpp        |  12 +-
 lldb/source/Target/StackFrame.cpp             |   9 +-
 lldb/source/Target/StackFrameList.cpp         |   5 +
 .../TestScriptedFrameProvider.py              | 207 ++++++++++++++----
 .../scripted_frame_provider/python_helper.py  |  36 +++
 .../test_frame_providers.py                   |  94 ++++++++
 .../dummy_scripted_process.py                 |  29 +++
 10 files changed, 440 insertions(+), 119 deletions(-)
 create mode 100644 lldb/test/API/functionalities/scripted_frame_provider/python_helper.py

diff --git a/lldb/include/lldb/Target/StackID.h b/lldb/include/lldb/Target/StackID.h
index 18461533d648a..3f6a83b5e2fa5 100644
--- a/lldb/include/lldb/Target/StackID.h
+++ b/lldb/include/lldb/Target/StackID.h
@@ -52,6 +52,7 @@ class StackID {
 
 protected:
   friend class StackFrame;
+  friend class SyntheticStackFrameList;
 
   void SetPC(lldb::addr_t pc, Process *process);
   void SetCFA(lldb::addr_t cfa, Process *process);
diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td
index 1be911c291703..2b12902582740 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -59,7 +59,7 @@ let Definition = "debugger" in {
     Desc<"The default disassembly format string to use when disassembling instruction sequences.">;
   def FrameFormat: Property<"frame-format", "FormatEntity">,
     Global,
-    DefaultStringValue<"frame #${frame.index}: ${ansi.fg.cyan}${frame.pc}${ansi.normal}{ ${module.file.basename}{`${function.name-with-args}{${frame.no-debug}${function.pc-offset}}}}{ at ${ansi.fg.cyan}${line.file.basename}${ansi.normal}:${ansi.fg.yellow}${line.number}${ansi.normal}{:${ansi.fg.yellow}${line.column}${ansi.normal}}}${frame.kind}{${function.is-optimized} [opt]}{${function.is-inlined} [inlined]}{${frame.is-artificial} [artificial]}\\\\n">,
+    DefaultStringValue<"frame #${frame.index}: ${ansi.fg.cyan}${frame.pc}${ansi.normal}{ ${module.file.basename}{`}}{${function.name-with-args}{${frame.no-debug}${function.pc-offset}}}{ at ${ansi.fg.cyan}${line.file.basename}${ansi.normal}:${ansi.fg.yellow}${line.number}${ansi.normal}{:${ansi.fg.yellow}${line.column}${ansi.normal}}}${frame.kind}{${function.is-optimized} [opt]}{${function.is-inlined} [inlined]}{${frame.is-artificial} [artificial]}\\\\n">,
     Desc<"The default frame format string to use when displaying stack frame information for threads.">;
   def NotiftVoid: Property<"notify-void", "Boolean">,
     Global,
@@ -235,7 +235,7 @@ let Definition = "debugger" in {
     Desc<"If true, LLDB will automatically escape non-printable and escape characters when formatting strings.">;
   def FrameFormatUnique: Property<"frame-format-unique", "FormatEntity">,
     Global,
-    DefaultStringValue<"frame #${frame.index}: ${ansi.fg.cyan}${frame.pc}${ansi.normal}{ ${module.file.basename}{`${function.name-without-args}{${frame.no-debug}${function.pc-offset}}}}{ at ${ansi.fg.cyan}${line.file.basename}${ansi.normal}:${ansi.fg.yellow}${line.number}${ansi.normal}{:${ansi.fg.yellow}${line.column}${ansi.normal}}}${frame.kind}{${function.is-optimized} [opt]}{${function.is-inlined} [inlined]}{${frame.is-artificial} [artificial]}\\\\n">,
+    DefaultStringValue<"frame #${frame.index}: ${ansi.fg.cyan}${frame.pc}${ansi.normal}{ ${module.file.basename}{`}}{${function.name-without-args}{${frame.no-debug}${function.pc-offset}}}{ at ${ansi.fg.cyan}${line.file.basename}${ansi.normal}:${ansi.fg.yellow}${line.number}${ansi.normal}{:${ansi.fg.yellow}${line.column}${ansi.normal}}}${frame.kind}{${function.is-optimized} [opt]}{${function.is-inlined} [inlined]}{${frame.is-artificial} [artificial]}\\\\n">,
     Desc<"The default frame format string to use when displaying stack frame information for threads from thread backtrace unique.">;
   def ShowAutosuggestion: Property<"show-autosuggestion", "Boolean">,
     Global,
diff --git a/lldb/source/Core/FormatEntity.cpp b/lldb/source/Core/FormatEntity.cpp
index c528a14fa76d0..3fcdae6e0bf4a 100644
--- a/lldb/source/Core/FormatEntity.cpp
+++ b/lldb/source/Core/FormatEntity.cpp
@@ -1684,10 +1684,11 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
       StackFrame *frame = exe_ctx->GetFramePtr();
       if (frame) {
         const Address &pc_addr = frame->GetFrameCodeAddress();
-        if (pc_addr.IsValid() || frame->IsSynthetic()) {
+        if (pc_addr.IsValid()) {
           if (DumpAddressAndContent(s, sc, exe_ctx, pc_addr, false))
             return true;
-        }
+        } else if (frame->IsSynthetic())
+          return true;
       }
     }
     return false;
@@ -1808,70 +1809,91 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
     return initial_function;
 
   case Entry::Type::FunctionName: {
-    if (!sc)
-      return false;
+    if (sc) {
+      Language *language_plugin = nullptr;
+      bool language_plugin_handled = false;
+      StreamString ss;
 
-    Language *language_plugin = nullptr;
-    bool language_plugin_handled = false;
-    StreamString ss;
+      if (sc->function)
+        language_plugin = Language::FindPlugin(sc->function->GetLanguage());
+      else if (sc->symbol)
+        language_plugin = Language::FindPlugin(sc->symbol->GetLanguage());
 
-    if (sc->function)
-      language_plugin = Language::FindPlugin(sc->function->GetLanguage());
-    else if (sc->symbol)
-      language_plugin = Language::FindPlugin(sc->symbol->GetLanguage());
+      if (language_plugin)
+        language_plugin_handled = language_plugin->GetFunctionDisplayName(
+            *sc, exe_ctx, Language::FunctionNameRepresentation::eName, ss);
 
-    if (language_plugin)
-      language_plugin_handled = language_plugin->GetFunctionDisplayName(
-          *sc, exe_ctx, Language::FunctionNameRepresentation::eName, ss);
+      if (language_plugin_handled) {
+        s << ss.GetString();
+        return true;
+      }
 
-    if (language_plugin_handled) {
-      s << ss.GetString();
-      return true;
+      const char *name = sc->GetPossiblyInlinedFunctionName()
+                             .GetName(Mangled::NamePreference::ePreferDemangled)
+                             .AsCString();
+      if (name) {
+        s.PutCString(name);
+        return true;
+      }
     }
 
-    const char *name = sc->GetPossiblyInlinedFunctionName()
-                           .GetName(Mangled::NamePreference::ePreferDemangled)
-                           .AsCString();
-    if (!name)
-      return false;
-
-    s.PutCString(name);
-
-    return true;
+    // Fallback to frame methods if available.
+    if (exe_ctx) {
+      StackFrame *frame = exe_ctx->GetFramePtr();
+      if (frame) {
+        const char *name = frame->GetFunctionName();
+        if (name) {
+          s.PutCString(name);
+          return true;
+        }
+      }
+    }
+    return false;
   }
 
   case Entry::Type::FunctionNameNoArgs: {
-    if (!sc)
-      return false;
-
-    Language *language_plugin = nullptr;
-    bool language_plugin_handled = false;
-    StreamString ss;
-    if (sc->function)
-      language_plugin = Language::FindPlugin(sc->function->GetLanguage());
-    else if (sc->symbol)
-      language_plugin = Language::FindPlugin(sc->symbol->GetLanguage());
-
-    if (language_plugin)
-      language_plugin_handled = language_plugin->GetFunctionDisplayName(
-          *sc, exe_ctx, Language::FunctionNameRepresentation::eNameWithNoArgs,
-          ss);
+    if (sc) {
+      Language *language_plugin = nullptr;
+      bool language_plugin_handled = false;
+      StreamString ss;
+      if (sc->function)
+        language_plugin = Language::FindPlugin(sc->function->GetLanguage());
+      else if (sc->symbol)
+        language_plugin = Language::FindPlugin(sc->symbol->GetLanguage());
+
+      if (language_plugin)
+        language_plugin_handled = language_plugin->GetFunctionDisplayName(
+            *sc, exe_ctx, Language::FunctionNameRepresentation::eNameWithNoArgs,
+            ss);
+
+      if (language_plugin_handled) {
+        s << ss.GetString();
+        return true;
+      }
 
-    if (language_plugin_handled) {
-      s << ss.GetString();
-      return true;
+      const char *name =
+          sc->GetPossiblyInlinedFunctionName()
+              .GetName(
+                  Mangled::NamePreference::ePreferDemangledWithoutArguments)
+              .AsCString();
+      if (name) {
+        s.PutCString(name);
+        return true;
+      }
     }
 
-    const char *name =
-        sc->GetPossiblyInlinedFunctionName()
-            .GetName(Mangled::NamePreference::ePreferDemangledWithoutArguments)
-            .AsCString();
-    if (!name)
-      return false;
-
-    s.PutCString(name);
-
-    return true;
+    // Fallback to frame methods if available.
+    if (exe_ctx) {
+      StackFrame *frame = exe_ctx->GetFramePtr();
+      if (frame) {
+        const char *name = frame->GetFunctionName();
+        if (name) {
+          s.PutCString(name);
+          return true;
+        }
+      }
+    }
+    return false;
   }
 
   case Entry::Type::FunctionPrefix:
@@ -1898,13 +1920,26 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
   }
 
   case Entry::Type::FunctionNameWithArgs: {
-    if (!sc)
-      return false;
+    if (sc) {
+      if (FormatFunctionNameForLanguage(s, exe_ctx, sc))
+        return true;
 
-    if (FormatFunctionNameForLanguage(s, exe_ctx, sc))
-      return true;
+      if (HandleFunctionNameWithArgs(s, exe_ctx, *sc))
+        return true;
+    }
 
-    return HandleFunctionNameWithArgs(s, exe_ctx, *sc);
+    // Fallback to frame methods if available.
+    if (exe_ctx) {
+      StackFrame *frame = exe_ctx->GetFramePtr();
+      if (frame) {
+        const char *name = frame->GetDisplayFunctionName();
+        if (name) {
+          s.PutCString(name);
+          return true;
+        }
+      }
+    }
+    return false;
   }
   case Entry::Type::FunctionMangledName: {
     if (!sc)
@@ -1951,6 +1986,8 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
                                           frame->GetFrameCodeAddress(), false,
                                           false, false))
           return true;
+        else if (frame->IsSynthetic())
+          return true;
       }
     }
     return false;
@@ -1975,11 +2012,8 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
 
   case Entry::Type::LineEntryFile:
     if (sc && sc->line_entry.IsValid()) {
-      Module *module = sc->module_sp.get();
-      if (module) {
-        if (DumpFile(s, sc->line_entry.GetFile(), (FileKind)entry.number))
-          return true;
-      }
+      if (DumpFile(s, sc->line_entry.GetFile(), (FileKind)entry.number))
+        return true;
     }
     return false;
 
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
index 265bc28a8957f..748ca5cf03414 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
@@ -11,10 +11,15 @@
 
 #include "lldb/Core/Address.h"
 #include "lldb/Core/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleList.h"
+#include "lldb/Host/FileSystem.h"
 #include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
 #include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Symbol/CompileUnit.h"
 #include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Symbol/SymbolFile.h"
 #include "lldb/Target/ExecutionContext.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/RegisterContext.h"
@@ -98,9 +103,8 @@ ScriptedFrame::Create(ThreadSP thread_sp,
 
   std::optional<SymbolContext> maybe_sym_ctx =
       scripted_frame_interface->GetSymbolContext();
-  if (maybe_sym_ctx) {
+  if (maybe_sym_ctx)
     sc = *maybe_sym_ctx;
-  }
 
   StructuredData::DictionarySP reg_info =
       scripted_frame_interface->GetRegisterInfo();
@@ -162,7 +166,7 @@ const char *ScriptedFrame::GetFunctionName() {
   CheckInterpreterAndScriptObject();
   std::optional<std::string> function_name = GetInterface()->GetFunctionName();
   if (!function_name)
-    return nullptr;
+    return StackFrame::GetFunctionName();
   return ConstString(function_name->c_str()).AsCString();
 }
 
@@ -171,7 +175,7 @@ const char *ScriptedFrame::GetDisplayFunctionName() {
   std::optional<std::string> function_name =
       GetInterface()->GetDisplayFunctionName();
   if (!function_name)
-    return nullptr;
+    return StackFrame::GetDisplayFunctionName();
   return ConstString(function_name->c_str()).AsCString();
 }
 
diff --git a/lldb/source/Target/StackFrame.cpp b/lldb/source/Target/StackFrame.cpp
index ca3d4a1a29b59..3bbb851b88007 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -331,6 +331,13 @@ StackFrame::GetSymbolContext(SymbolContextItem resolve_scope) {
     // following the function call instruction...
     Address lookup_addr(GetFrameCodeAddressForSymbolication());
 
+    // For PC-less frames (e.g., scripted frames), skip PC-based symbol
+    // resolution and preserve any already-populated SymbolContext fields.
+    if (!lookup_addr.IsValid()) {
+      m_flags.Set(resolve_scope | resolved);
+      return m_sc;
+    }
+
     if (m_sc.module_sp) {
       // We have something in our stack frame symbol context, lets check if we
       // haven't already tried to lookup one of those things. If we haven't
@@ -2057,7 +2064,7 @@ bool StackFrame::GetStatus(Stream &strm, bool show_frame_info, bool show_source,
       disasm_display = debugger.GetStopDisassemblyDisplay();
 
       GetSymbolContext(eSymbolContextCompUnit | eSymbolContextLineEntry);
-      if (m_sc.comp_unit && m_sc.line_entry.IsValid()) {
+      if (m_sc.comp_unit || m_sc.line_entry.IsValid()) {
         have_debuginfo = true;
         if (source_lines_before > 0 || source_lines_after > 0) {
           SupportFileNSP source_file_sp = m_sc.line_entry.file_sp;
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 5d1a8a8370414..896a760f61d26 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -64,6 +64,8 @@ SyntheticStackFrameList::SyntheticStackFrameList(
 
 bool SyntheticStackFrameList::FetchFramesUpTo(
     uint32_t end_idx, InterruptionControl allow_interrupt) {
+
+  size_t num_synthetic_frames = 0;
   // Check if the thread has a synthetic frame provider.
   if (auto provider_sp = m_thread.GetFrameProvider()) {
     // Use the synthetic frame provider to generate frames lazily.
@@ -81,6 +83,9 @@ bool SyntheticStackFrameList::FetchFramesUpTo(
         break;
       }
       StackFrameSP frame_sp = *frame_or_err;
+      if (frame_sp->IsSynthetic())
+        frame_sp->GetStackID().SetCFA(num_synthetic_frames++,
+                                      GetThread().GetProcess().get());
       // Set the frame list weak pointer so ExecutionContextRef can resolve
       // the frame without calling Thread::GetStackFrameList().
       frame_sp->m_frame_list_wp = shared_from_this();
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
index caed94f5f93da..abadbe4ec5a93 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -25,11 +25,11 @@ def test_replace_all_frames(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # Import the test frame provider
+        # Import the test frame provider.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
-        # Attach the Replace provider
+        # Attach the Replace provider.
         error = lldb.SBError()
         provider_id = target.RegisterScriptedFrameProvider(
             "test_frame_providers.ReplaceFrameProvider",
@@ -39,10 +39,10 @@ def test_replace_all_frames(self):
         self.assertTrue(error.Success(), f"Failed to register provider: {error}")
         self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
 
-        # Verify we have exactly 3 synthetic frames
+        # Verify we have exactly 3 synthetic frames.
         self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
 
-        # Verify frame indices and PCs (dictionary-based frames don't have custom function names)
+        # Verify frame indices and PCs (dictionary-based frames don't have custom function names).
         frame0 = thread.GetFrameAtIndex(0)
         self.assertIsNotNone(frame0)
         self.assertEqual(frame0.GetPC(), 0x1000)
@@ -62,13 +62,13 @@ def test_prepend_frames(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # Get original frame count and PC
+        # Get original frame count and PC.
         original_frame_count = thread.GetNumFrames()
         self.assertGreaterEqual(
             original_frame_count, 2, "Should have at least 2 real frames"
         )
 
-        # Import and attach Prepend provider
+        # Import and attach Prepend provider.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
@@ -81,18 +81,18 @@ def test_prepend_frames(self):
         self.assertTrue(error.Success(), f"Failed to register provider: {error}")
         self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
 
-        # Verify we have 2 more frames
+        # Verify we have 2 more frames.
         new_frame_count = thread.GetNumFrames()
         self.assertEqual(new_frame_count, original_frame_count + 2)
 
-        # Verify first 2 frames are synthetic (check PCs, not function names)
+        # Verify first 2 frames are synthetic (check PCs, not function names).
         frame0 = thread.GetFrameAtIndex(0)
         self.assertEqual(frame0.GetPC(), 0x9000)
 
         frame1 = thread.GetFrameAtIndex(1)
         self.assertEqual(frame1.GetPC(), 0xA000)
 
-        # Verify frame 2 is the original real frame 0
+        # Verify frame 2 is the original real frame 0.
         frame2 = thread.GetFrameAtIndex(2)
         self.assertIn("thread_func", frame2.GetFunctionName())
 
@@ -103,10 +103,10 @@ def test_append_frames(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # Get original frame count
+        # Get original frame count.
         original_frame_count = thread.GetNumFrames()
 
-        # Import and attach Append provider
+        # Import and attach Append provider.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
@@ -119,11 +119,11 @@ def test_append_frames(self):
         self.assertTrue(error.Success(), f"Failed to register provider: {error}")
         self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
 
-        # Verify we have 1 more frame
+        # Verify we have 1 more frame.
         new_frame_count = thread.GetNumFrames()
         self.assertEqual(new_frame_count, original_frame_count + 1)
 
-        # Verify first frames are still real
+        # Verify first frames are still real.
         frame0 = thread.GetFrameAtIndex(0)
         self.assertIn("thread_func", frame0.GetFunctionName())
 
@@ -137,7 +137,7 @@ def test_scripted_frame_objects(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # Import the provider that returns ScriptedFrame objects
+        # Import the provider that returns ScriptedFrame objects.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
@@ -150,12 +150,12 @@ def test_scripted_frame_objects(self):
         self.assertTrue(error.Success(), f"Failed to register provider: {error}")
         self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
 
-        # Verify we have 5 frames
+        # Verify we have 5 frames.
         self.assertEqual(
             thread.GetNumFrames(), 5, "Should have 5 custom scripted frames"
         )
 
-        # Verify frame properties from CustomScriptedFrame
+        # Verify frame properties from CustomScriptedFrame.
         frame0 = thread.GetFrameAtIndex(0)
         self.assertIsNotNone(frame0)
         self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
@@ -179,17 +179,17 @@ def test_applies_to_thread(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # We should have at least 2 threads (worker threads) at the breakpoint
+        # We should have at least 2 threads (worker threads) at the breakpoint.
         num_threads = process.GetNumThreads()
         self.assertGreaterEqual(
             num_threads, 2, "Should have at least 2 threads at breakpoint"
         )
 
-        # Import the test frame provider
+        # Import the test frame provider.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
-        # Collect original thread info before applying provider
+        # Collect original thread info before applying provider.
         thread_info = {}
         for i in range(num_threads):
             t = process.GetThreadAtIndex(i)
@@ -198,7 +198,7 @@ def test_applies_to_thread(self):
                 "pc": t.GetFrameAtIndex(0).GetPC(),
             }
 
-        # Register the ThreadFilterFrameProvider which only applies to thread ID 1
+        # Register the ThreadFilterFrameProvider which only applies to thread ID 1.
         error = lldb.SBError()
         provider_id = target.RegisterScriptedFrameProvider(
             "test_frame_providers.ThreadFilterFrameProvider",
@@ -208,9 +208,9 @@ def test_applies_to_thread(self):
         self.assertTrue(error.Success(), f"Failed to register provider: {error}")
         self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
 
-        # Check each thread
+        # Check each thread.
         thread_id_1_found = False
-        # On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified
+        # On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified.
         is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
         expected_synthetic_pc = 0xFFFE if is_arm_32bit else 0xFFFF
 
@@ -219,7 +219,7 @@ def test_applies_to_thread(self):
             thread_id = t.GetIndexID()
 
             if thread_id == 1:
-                # Thread with ID 1 should have synthetic frame
+                # Thread with ID 1 should have synthetic frame.
                 thread_id_1_found = True
                 self.assertEqual(
                     t.GetNumFrames(),
@@ -232,7 +232,7 @@ def test_applies_to_thread(self):
                     f"Thread with ID 1 should have synthetic PC {expected_synthetic_pc:#x}",
                 )
             else:
-                # Other threads should keep their original frames
+                # Other threads should keep their original frames.
                 self.assertEqual(
                     t.GetNumFrames(),
                     thread_info[thread_id]["frame_count"],
@@ -244,7 +244,7 @@ def test_applies_to_thread(self):
                     f"Thread with ID {thread_id} should have its original PC",
                 )
 
-        # We should have found at least one thread with ID 1
+        # We should have found at least one thread with ID 1.
         self.assertTrue(
             thread_id_1_found,
             "Should have found a thread with ID 1 to test filtering",
@@ -257,15 +257,15 @@ def test_remove_frame_provider_by_id(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # Import the test frame providers
+        # Import the test frame providers.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
-        # Get original frame count
+        # Get original frame count.
         original_frame_count = thread.GetNumFrames()
         original_pc = thread.GetFrameAtIndex(0).GetPC()
 
-        # Register the first provider and get its ID
+        # Register the first provider and get its ID.
         error = lldb.SBError()
         provider_id_1 = target.RegisterScriptedFrameProvider(
             "test_frame_providers.ReplaceFrameProvider",
@@ -274,13 +274,13 @@ def test_remove_frame_provider_by_id(self):
         )
         self.assertTrue(error.Success(), f"Failed to register provider 1: {error}")
 
-        # Verify first provider is active (3 synthetic frames)
+        # Verify first provider is active (3 synthetic frames).
         self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
         self.assertEqual(
             thread.GetFrameAtIndex(0).GetPC(), 0x1000, "Should have first provider's PC"
         )
 
-        # Register a second provider and get its ID
+        # Register a second provider and get its ID.
         provider_id_2 = target.RegisterScriptedFrameProvider(
             "test_frame_providers.PrependFrameProvider",
             lldb.SBStructuredData(),
@@ -299,10 +299,10 @@ def test_remove_frame_provider_by_id(self):
             result, f"Should successfully remove provider with ID {provider_id_1}"
         )
 
-        # After removing the first provider, the second provider should still be active
-        # The PrependFrameProvider adds 2 frames before the real stack
+        # After removing the first provider, the second provider should still be
+        # active. The PrependFrameProvider adds 2 frames before the real stack.
         # Since ReplaceFrameProvider had 3 frames, and we removed it, we should now
-        # have the original frames (from real stack) with PrependFrameProvider applied
+        # have the original frames (from real stack) with PrependFrameProvider applied.
         new_frame_count = thread.GetNumFrames()
         self.assertEqual(
             new_frame_count,
@@ -310,7 +310,7 @@ def test_remove_frame_provider_by_id(self):
             "Should have original frames + 2 prepended frames",
         )
 
-        # First two frames should be from PrependFrameProvider
+        # First two frames should be from PrependFrameProvider.
         self.assertEqual(
             thread.GetFrameAtIndex(0).GetPC(),
             0x9000,
@@ -322,13 +322,13 @@ def test_remove_frame_provider_by_id(self):
             "Second frame should be from PrependFrameProvider",
         )
 
-        # Remove the second provider
+        # Remove the second provider.
         result = target.RemoveScriptedFrameProvider(provider_id_2)
         self.assertSuccess(
             result, f"Should successfully remove provider with ID {provider_id_2}"
         )
 
-        # After removing both providers, frames should be back to original
+        # After removing both providers, frames should be back to original.
         self.assertEqual(
             thread.GetNumFrames(),
             original_frame_count,
@@ -340,7 +340,7 @@ def test_remove_frame_provider_by_id(self):
             "Should restore original PC",
         )
 
-        # Try to remove a provider that doesn't exist
+        # Try to remove a provider that doesn't exist.
         result = target.RemoveScriptedFrameProvider(999999)
         self.assertTrue(result.Fail(), "Should fail to remove non-existent provider")
 
@@ -364,19 +364,19 @@ def test_circular_dependency_fix(self):
             self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
         )
 
-        # Get original frame count and PC
+        # Get original frame count and PC.
         original_frame_count = thread.GetNumFrames()
         original_pc = thread.GetFrameAtIndex(0).GetPC()
         self.assertGreaterEqual(
             original_frame_count, 2, "Should have at least 2 real frames"
         )
 
-        # Import the provider that accesses input frames in __init__
+        # Import the provider that accesses input frames in __init__.
         script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
         self.runCmd("command script import " + script_path)
 
-        # Register the CircularDependencyTestProvider
-        # Before the fix, this would crash or hang due to circular dependency
+        # Register the CircularDependencyTestProvider.
+        # Before the fix, this would crash or hang due to circular dependency.
         error = lldb.SBError()
         provider_id = target.RegisterScriptedFrameProvider(
             "test_frame_providers.CircularDependencyTestProvider",
@@ -388,8 +388,8 @@ def test_circular_dependency_fix(self):
         self.assertTrue(error.Success(), f"Failed to register provider: {error}")
         self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
 
-        # Verify the provider worked correctly
-        # Should have 1 synthetic frame + all original frames
+        # Verify the provider worked correctly,
+        # Should have 1 synthetic frame + all original frames.
         new_frame_count = thread.GetNumFrames()
         self.assertEqual(
             new_frame_count,
@@ -397,11 +397,11 @@ def test_circular_dependency_fix(self):
             "Should have original frames + 1 synthetic frame",
         )
 
-        # On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified
+        # On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified.
         is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
         expected_synthetic_pc = 0xDEADBEEE if is_arm_32bit else 0xDEADBEEF
 
-        # First frame should be synthetic
+        # First frame should be synthetic.
         frame0 = thread.GetFrameAtIndex(0)
         self.assertIsNotNone(frame0)
         self.assertEqual(
@@ -410,7 +410,7 @@ def test_circular_dependency_fix(self):
             f"First frame should be synthetic frame with PC {expected_synthetic_pc:#x}",
         )
 
-        # Second frame should be the original first frame
+        # Second frame should be the original first frame.
         frame1 = thread.GetFrameAtIndex(1)
         self.assertIsNotNone(frame1)
         self.assertEqual(
@@ -419,10 +419,121 @@ def test_circular_dependency_fix(self):
             "Second frame should be original first frame",
         )
 
-        # Verify we can still call methods on frames (no circular dependency!)
+        # Verify we can still call methods on frames (no circular dependency!).
         for i in range(min(3, new_frame_count)):
             frame = thread.GetFrameAtIndex(i)
             self.assertIsNotNone(frame)
-            # These calls should not trigger circular dependency
+            # These calls should not trigger circular dependency.
             pc = frame.GetPC()
             self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")
+
+    def test_python_source_frames(self):
+        """Test that frames can point to Python source files and display properly."""
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
+        )
+
+        # Get original frame count.
+        original_frame_count = thread.GetNumFrames()
+        self.assertGreaterEqual(
+            original_frame_count, 2, "Should have at least 2 real frames"
+        )
+
+        # Import the provider.
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        # Register the PythonSourceFrameProvider.
+        error = lldb.SBError()
+        provider_id = target.RegisterScriptedFrameProvider(
+            "test_frame_providers.PythonSourceFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")
+
+        # Verify we have 3 more frames (Python frames).
+        new_frame_count = thread.GetNumFrames()
+        self.assertEqual(
+            new_frame_count,
+            original_frame_count + 3,
+            "Should have original frames + 3 Python frames",
+        )
+
+        # Verify first three frames are Python source frames.
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertIsNotNone(frame0)
+        self.assertEqual(
+            frame0.GetFunctionName(),
+            "compute_fibonacci",
+            "First frame should be compute_fibonacci",
+        )
+        self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
+        # PC-less frames should show invalid address.
+        self.assertEqual(
+            frame0.GetPC(),
+            lldb.LLDB_INVALID_ADDRESS,
+            "PC-less frame should have LLDB_INVALID_ADDRESS",
+        )
+
+        frame1 = thread.GetFrameAtIndex(1)
+        self.assertIsNotNone(frame1)
+        self.assertEqual(
+            frame1.GetFunctionName(),
+            "process_data",
+            "Second frame should be process_data",
+        )
+        self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic")
+
+        frame2 = thread.GetFrameAtIndex(2)
+        self.assertIsNotNone(frame2)
+        self.assertEqual(frame2.GetFunctionName(), "main", "Third frame should be main")
+        self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")
+
+        # Verify line entry information is present.
+        line_entry0 = frame0.GetLineEntry()
+        self.assertTrue(line_entry0.IsValid(), "Frame 0 should have a valid line entry")
+        self.assertEqual(line_entry0.GetLine(), 7, "Frame 0 should point to line 7")
+        file_spec0 = line_entry0.GetFileSpec()
+        self.assertTrue(file_spec0.IsValid(), "Frame 0 should have valid file spec")
+        self.assertEqual(
+            file_spec0.GetFilename(),
+            "python_helper.py",
+            "Frame 0 should point to python_helper.py",
+        )
+
+        line_entry1 = frame1.GetLineEntry()
+        self.assertTrue(line_entry1.IsValid(), "Frame 1 should have a valid line entry")
+        self.assertEqual(line_entry1.GetLine(), 16, "Frame 1 should point to line 16")
+
+        line_entry2 = frame2.GetLineEntry()
+        self.assertTrue(line_entry2.IsValid(), "Frame 2 should have a valid line entry")
+        self.assertEqual(line_entry2.GetLine(), 27, "Frame 2 should point to line 27")
+
+        # Verify the frames display properly in backtrace.
+        # This tests that PC-less frames don't show 0xffffffffffffffff.
+        self.runCmd("bt")
+        output = self.res.GetOutput()
+
+        # Should show function names.
+        self.assertIn("compute_fibonacci", output)
+        self.assertIn("process_data", output)
+        self.assertIn("main", output)
+
+        # Should show Python file.
+        self.assertIn("python_helper.py", output)
+
+        # Should show line numbers.
+        self.assertIn(":7", output)  # compute_fibonacci line.
+        self.assertIn(":16", output)  # process_data line.
+        self.assertIn(":27", output)  # main line.
+
+        # Should NOT show invalid address (0xffffffffffffffff).
+        self.assertNotIn("0xffffffffffffffff", output.lower())
+
+        # Verify frame 3 is the original real frame 0.
+        frame3 = thread.GetFrameAtIndex(3)
+        self.assertIsNotNone(frame3)
+        self.assertIn("thread_func", frame3.GetFunctionName())
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/python_helper.py b/lldb/test/API/functionalities/scripted_frame_provider/python_helper.py
new file mode 100644
index 0000000000000..27f38165608db
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_frame_provider/python_helper.py
@@ -0,0 +1,36 @@
+"""
+Sample Python module to demonstrate Python source display in scripted frames.
+"""
+
+
+def compute_fibonacci(n):
+    """Compute the nth Fibonacci number."""
+    if n <= 1:
+        return n
+    a, b = 0, 1
+    for _ in range(n - 1):
+        a, b = b, a + b
+    return b
+
+
+def process_data(data):
+    """Process some data and return result."""
+    result = []
+    for item in data:
+        if isinstance(item, int):
+            result.append(item * 2)
+        elif isinstance(item, str):
+            result.append(item.upper())
+    return result
+
+
+def main():
+    """Main entry point for testing."""
+    fib_10 = compute_fibonacci(10)
+    data = [1, 2, "hello", 3, "world"]
+    processed = process_data(data)
+    return fib_10, processed
+
+
+if __name__ == "__main__":
+    main()
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
index b9731fdc0a197..76f859760bf4f 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
@@ -10,6 +10,7 @@
 index to create stackframes
 """
 
+import os
 import lldb
 from lldb.plugins.scripted_process import ScriptedFrame
 from lldb.plugins.scripted_frame_provider import ScriptedFrameProvider
@@ -220,3 +221,96 @@ def get_frame_at_index(self, index):
             # Pass through original frames at indices 1, 2, 3, ...
             return index - 1
         return None
+
+
+class PythonSourceFrame(ScriptedFrame):
+    """Scripted frame that points to Python source code."""
+
+    def __init__(self, thread, idx, function_name, python_file, line_number):
+        args = lldb.SBStructuredData()
+        super().__init__(thread, args)
+
+        self.idx = idx
+        self.function_name = function_name
+        self.python_file = python_file
+        self.line_number = line_number
+
+    def get_id(self):
+        """Return the frame index."""
+        return self.idx
+
+    def get_pc(self):
+        """PC-less frame - return invalid address."""
+        return lldb.LLDB_INVALID_ADDRESS
+
+    def get_function_name(self):
+        """Return the function name."""
+        return self.function_name
+
+    def get_symbol_context(self):
+        """Return a symbol context with LineEntry pointing to Python source."""
+        # Create a LineEntry pointing to the Python source file
+        line_entry = lldb.SBLineEntry()
+        line_entry.SetFileSpec(lldb.SBFileSpec(self.python_file, True))
+        line_entry.SetLine(self.line_number)
+        line_entry.SetColumn(0)
+
+        # Create a symbol context with the line entry
+        sym_ctx = lldb.SBSymbolContext()
+        sym_ctx.SetLineEntry(line_entry)
+
+        return sym_ctx
+
+    def is_artificial(self):
+        """Not artificial."""
+        return False
+
+    def is_hidden(self):
+        """Not hidden."""
+        return False
+
+    def get_register_context(self):
+        """No register context for PC-less frames."""
+        return None
+
+
+class PythonSourceFrameProvider(ScriptedFrameProvider):
+    """
+    Provider that demonstrates Python source display in scripted frames.
+
+    This provider prepends frames pointing to Python source code, showing
+    that PC-less frames can display Python source files with proper line
+    numbers and module/compile unit information.
+    """
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+        # Find the python_helper.py file
+        current_dir = os.path.dirname(os.path.abspath(__file__))
+        self.python_file = os.path.join(current_dir, "python_helper.py")
+
+    @staticmethod
+    def get_description():
+        """Return a description of this provider."""
+        return "Provider that prepends frames pointing to Python source"
+
+    def get_frame_at_index(self, index):
+        """Return Python source frames followed by original frames."""
+        if index == 0:
+            # Frame pointing to compute_fibonacci function (line 7)
+            return PythonSourceFrame(
+                self.thread, 0, "compute_fibonacci", self.python_file, 7
+            )
+        elif index == 1:
+            # Frame pointing to process_data function (line 16)
+            return PythonSourceFrame(
+                self.thread, 1, "process_data", self.python_file, 16
+            )
+        elif index == 2:
+            # Frame pointing to main function (line 27)
+            return PythonSourceFrame(self.thread, 2, "main", self.python_file, 27)
+        elif index - 3 < len(self.input_frames):
+            # Pass through original frames
+            return index - 3
+        return None
diff --git a/lldb/test/API/functionalities/scripted_process/dummy_scripted_process.py b/lldb/test/API/functionalities/scripted_process/dummy_scripted_process.py
index a9459682e70a8..f676cfbf875ef 100644
--- a/lldb/test/API/functionalities/scripted_process/dummy_scripted_process.py
+++ b/lldb/test/API/functionalities/scripted_process/dummy_scripted_process.py
@@ -8,6 +8,15 @@
 from lldb.plugins.scripted_process import ScriptedFrame
 
 
+def my_python_function(x, y):
+    """A sample Python function to demonstrate Python source display in scripted frames."""
+    result = x + y
+    if result > 100:
+        return result * 2
+    else:
+        return result
+
+
 class DummyStopHook:
     def __init__(self, target, args):
         self.target = target
@@ -88,6 +97,26 @@ def __init__(self, process, args):
             DummyScriptedFrame(self, args, len(self.frames), "baz", sym_ctx)
         )
 
+        # Add a frame with Python source
+        code = my_python_function.__code__
+        lineno = code.co_firstlineno
+        col_offset = getattr(
+            code, "co_firstcol_offset", 0
+        )  # Python ≥3.11 has column info
+        py_le = lldb.SBLineEntry()
+        py_le.SetFileSpec(lldb.SBFileSpec(__file__, True))
+        py_le.SetLine(lineno)  # Line where my_python_function is defined
+        py_le.SetColumn(col_offset)
+
+        py_sym_ctx = lldb.SBSymbolContext()
+        py_sym_ctx.SetLineEntry(py_le)
+
+        self.frames.append(
+            DummyScriptedFrame(
+                self, args, len(self.frames), "my_python_function", py_sym_ctx
+            )
+        )
+
     def get_thread_id(self) -> int:
         return 0x19
 



More information about the lldb-commits mailing list