[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 23:22:30 PST 2025
https://github.com/medismailben updated https://github.com/llvm/llvm-project/pull/170762
>From e8bcacf32d5797ccfa3cfafa8cfb7a1e3817826b Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Thu, 4 Dec 2025 23:20:25 -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 | 36 +++-
lldb/source/Core/FormatEntity.cpp | 170 +++++++++++-------
.../Process/scripted/ScriptedFrame.cpp | 12 +-
lldb/source/Target/StackFrame.cpp | 9 +-
lldb/source/Target/StackFrameList.cpp | 5 +
.../TestScriptedFrameProvider.py | 125 +++++++++++++
.../scripted_frame_provider/python_helper.py | 36 ++++
.../test_frame_providers.py | 96 ++++++++++
.../dummy_scripted_process.py | 25 +++
10 files changed, 438 insertions(+), 77 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..8d8343aedb3e2 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -57,10 +57,20 @@ let Definition = "debugger" in {
Global,
DefaultStringValue<"{${function.initial-function}{${module.file.basename}`}{${function.name-without-args}}:\\\\n}{${function.changed}\\\\n{${module.file.basename}`}{${function.name-without-args}}:\\\\n}{${ansi.fg.yellow}${current-pc-arrow}${ansi.normal} }${addr-file-or-load}{ <${function.concrete-only-addr-offset-no-padding}>}: ">,
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">,
- Desc<"The default frame format string to use when displaying stack frame information for threads.">;
+ 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">,
+ Desc<"The default frame format string to use when displaying stack "
+ "frame information for threads.">;
def NotiftVoid: Property<"notify-void", "Boolean">,
Global,
DefaultFalse,
@@ -233,10 +243,20 @@ let Definition = "debugger" in {
Global,
DefaultTrue,
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">,
- Desc<"The default frame format string to use when displaying stack frame information for threads from thread backtrace unique.">;
+ 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">,
+ 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,
DefaultFalse,
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..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..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