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

Med Ismail Bennani via lldb-commits lldb-commits at lists.llvm.org
Thu Dec 4 22:55:22 PST 2025


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

>From 24e2f000a99ad6dca8da083d087a86bf514faf62 Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 4 Dec 2025 22:54:53 -0800
Subject: [PATCH] [lldb] Add support for PC-less scripted frames

This adds support for scripted frames without valid PC addresses, allowing
them to display properly without showing 0xffffffffffffffff.

Changes include:
- Make StackFrame::GetSymbolContext() resilient to PC-less frames by adding
  an early return when the lookup address is invalid, preserving any
  already-populated SymbolContext fields.
- Populate module and compile unit for scripted frames by searching existing
  modules for matching LineEntry files, falling back to the scripted module
- Create synthetic CompileUnits with language type deduced from the script
- Fix frame formatting to avoid extra spaces for PC-less frames by removing
  the space from the frame format string and adding it conditionally in
  FormatEntity::Format only when the frame has a valid PC.
- Add fallbacks in FormatEntity for FunctionName, FunctionNameNoArgs, and
  FunctionNameWithArgs to use StackFrame methods when SymbolContext lookup
  fails, enabling proper function name display for scripted frames.
- Update test to include a scripted frame pointing to Python source with a
  LineEntry referencing the Python file containing my_python_function().

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             | 170 +++++++++++-------
 .../Process/scripted/ScriptedFrame.cpp        |  12 +-
 lldb/source/Target/StackFrame.cpp             |   9 +-
 lldb/source/Target/StackFrameList.cpp         |   4 +
 .../TestScriptedFrameProvider.py              | 125 +++++++++++++
 .../scripted_frame_provider/python_helper.py  |  36 ++++
 .../test_frame_providers.py                   |  96 ++++++++++
 .../dummy_scripted_process.py                 |  25 +++
 10 files changed, 411 insertions(+), 71 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..89bd5f03711e8 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..9ddbf59ebdfe1 100644
--- a/lldb/source/Core/FormatEntity.cpp
+++ b/lldb/source/Core/FormatEntity.cpp
@@ -1636,6 +1636,14 @@ bool FormatEntity::Format(const Entry &entry, Stream &s,
     if (sc) {
       Module *module = sc->module_sp.get();
       if (module) {
+        // Add a space before module name if frame has a valid PC
+        // (so "PC module" but ": module" for PC-less frames)
+        if (exe_ctx) {
+          StackFrame *frame = exe_ctx->GetFramePtr();
+          if (frame && frame->GetFrameCodeAddress().IsValid()) {
+            s.PutChar(' ');
+          }
+        }
         if (DumpFile(s, module->GetFileSpec(), (FileKind)entry.number))
           return true;
       }
@@ -1684,10 +1692,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 +1817,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 +1928,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 +1994,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 +2020,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..50f8c47c84bb4 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,8 @@ 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..06e55e79d538c 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -426,3 +426,128 @@ def test_circular_dependency_fix(self):
             # 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..6177f4345321c 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,98 @@ 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..4ebf940708223 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,22 @@ 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