Skip to content

Conversation

@medismailben
Copy link
Member

@medismailben medismailben commented Oct 3, 2025

This patch extends ScriptedFrame to work with real (non-scripted) threads,
enabling frame providers to synthesize frames for native processes.

Previously, ScriptedFrame only worked within ScriptedProcess/ScriptedThread
contexts. This patch decouples ScriptedFrame from ScriptedThread, allowing
users to augment or replace stack frames in real debugging sessions for use
cases like custom calling conventions, reconstructing corrupted frames from
core files, or adding diagnostic frames.

Key changes:

  • ScriptedFrame::Create() now accepts ThreadSP instead of requiring
    ScriptedThread, extracting architecture from the target triple rather
    than ScriptedProcess.arch

  • Added SBTarget::RegisterScriptedFrameProvider() and
    ClearScriptedFrameProvider() APIs, with Target storing a
    SyntheticFrameProviderDescriptor template for new threads

  • Added "target frame-provider register/clear" commands for CLI access

  • Thread class gains LoadScriptedFrameProvider(), ClearScriptedFrameProvider(),
    and GetFrameProvider() methods for per-thread frame provider management

  • New SyntheticStackFrameList overrides FetchFramesUpTo() to lazily provide
    frames from either the frame provider or the real stack

This enables practical use of the SyntheticFrameProvider infrastructure in
real debugging workflows.

rdar://161834688

Signed-off-by: Med Ismail Bennani [email protected]

@llvmbot llvmbot added the lldb label Oct 3, 2025
@medismailben medismailben changed the title [lldb] Introduce ScritedFrameProvider [lldb] Introduce ScriptedFrameProvider Oct 3, 2025
@medismailben medismailben requested a review from jimingham October 3, 2025 15:57
@llvmbot
Copy link
Member

llvmbot commented Oct 3, 2025

@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)

Changes

Patch is 27.35 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/161870.diff

19 Files Affected:

  • (modified) lldb/include/lldb/API/SBThread.h (+2)
  • (added) lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h (+27)
  • (modified) lldb/include/lldb/Interpreter/ScriptInterpreter.h (+5)
  • (added) lldb/include/lldb/Interpreter/ScriptedFrameProvider.h (+51)
  • (modified) lldb/include/lldb/Target/Thread.h (+9)
  • (modified) lldb/include/lldb/lldb-forward.h (+6)
  • (modified) lldb/source/API/SBThread.cpp (+27)
  • (modified) lldb/source/Commands/CommandObjectFrame.cpp (+95)
  • (modified) lldb/source/Commands/CommandObjectThread.cpp (+1)
  • (modified) lldb/source/Interpreter/CMakeLists.txt (+1)
  • (added) lldb/source/Interpreter/ScriptedFrameProvider.cpp (+86)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt (+1)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h (+1)
  • (added) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp (+57)
  • (added) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.h (+41)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h (+4)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp (+5)
  • (modified) lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h (+3)
  • (modified) lldb/source/Target/Thread.cpp (+30)
diff --git a/lldb/include/lldb/API/SBThread.h b/lldb/include/lldb/API/SBThread.h
index e9fe5858d125e..7b91228528bc7 100644
--- a/lldb/include/lldb/API/SBThread.h
+++ b/lldb/include/lldb/API/SBThread.h
@@ -229,6 +229,8 @@ class LLDB_API SBThread {
 
   SBValue GetSiginfo();
 
+  void RegisterFrameProvider(const char *class_name, SBStructuredData &args_data);
+
 private:
   friend class SBBreakpoint;
   friend class SBBreakpointLocation;
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
new file mode 100644
index 0000000000000..7618d5e15d563
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -0,0 +1,27 @@
+//===-- ScriptedFrameProviderInterface.h ------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
+
+#include "lldb/lldb-private.h"
+
+#include "ScriptedInterface.h"
+
+namespace lldb_private {
+class ScriptedFrameProviderInterface : public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, lldb::ThreadSP thread_sp,
+                     StructuredData::DictionarySP args_sp) = 0;
+
+  virtual StructuredData::ArraySP GetStackFrames() { return {}; }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDFRAMEPROVIDERINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 024bbc90a9a39..76ade002089bb 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -27,6 +27,7 @@
 #include "lldb/Host/StreamFile.h"
 #include "lldb/Interpreter/Interfaces/OperatingSystemInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
@@ -536,6 +537,10 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedFrameProviderInterfaceSP CreateScriptedFrameProviderInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() {
     return {};
diff --git a/lldb/include/lldb/Interpreter/ScriptedFrameProvider.h b/lldb/include/lldb/Interpreter/ScriptedFrameProvider.h
new file mode 100644
index 0000000000000..6c4053f11eeb3
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/ScriptedFrameProvider.h
@@ -0,0 +1,51 @@
+//===-- ScriptedFrameProvider.h --------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_INTERPRETER_SCRIPTEDFRAMEPROVIDER_H
+#define LLDB_INTERPRETER_SCRIPTEDFRAMEPROVIDER_H
+
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/lldb-forward.h"
+#include "llvm/Support/Error.h"
+
+namespace lldb_private {
+
+class ScriptedFrameProvider {
+public:
+  /// Constructor that initializes the scripted frame provider.
+  ///
+  /// \param[in] thread_sp
+  ///     The thread for which to provide scripted frames.
+  ///
+  /// \param[in] scripted_metadata
+  ///     The metadata containing the class name and arguments for the
+  ///     scripted frame provider.
+  ///
+  /// \param[out] error
+  ///     Status object to report any errors during initialization.
+  ScriptedFrameProvider(lldb::ThreadSP thread_sp,
+                        const ScriptedMetadata &scripted_metadata,
+                        Status &error);
+  ~ScriptedFrameProvider();
+
+  /// Get the stack frames from the scripted frame provider.
+  ///
+  /// \return
+  ///     An Expected containing the StackFrameListSP if successful,
+  ///     otherwise an error describing what went wrong.
+  llvm::Expected<lldb::StackFrameListSP> GetStackFrames();
+
+private:
+  lldb::ThreadSP m_thread_sp;
+  lldb::ScriptedFrameProviderInterfaceSP m_interface_sp;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_SCRIPTEDFRAMEPROVIDER_H
diff --git a/lldb/include/lldb/Target/Thread.h b/lldb/include/lldb/Target/Thread.h
index 688c056da2633..d0c33f557b12b 100644
--- a/lldb/include/lldb/Target/Thread.h
+++ b/lldb/include/lldb/Target/Thread.h
@@ -1294,6 +1294,10 @@ class Thread : public std::enable_shared_from_this<Thread>,
   ///     The PC value before execution was resumed.  May not be available;
   ///     an empty std::optional is returned in that case.
   std::optional<lldb::addr_t> GetPreviousFrameZeroPC();
+                 
+   void SetScriptedFrameProvider(const ScriptedMetadata &scripted_metadata);
+
+   void ClearScriptedFrameProvider();
 
 protected:
   friend class ThreadPlan;
@@ -1338,6 +1342,8 @@ class Thread : public std::enable_shared_from_this<Thread>,
 
   lldb::StackFrameListSP GetStackFrameList();
 
+  llvm::Expected<lldb::StackFrameListSP> GetScriptedFrameList();
+
   void SetTemporaryResumeState(lldb::StateType new_state) {
     m_temporary_resume_state = new_state;
   }
@@ -1400,6 +1406,9 @@ class Thread : public std::enable_shared_from_this<Thread>,
   /// The Thread backed by this thread, if any.
   lldb::ThreadWP m_backed_thread;
 
+  /// The Scripted Frame Provider, if any.
+  lldb::ScriptedFrameProviderSP m_frame_provider_sp;
+
 private:
   bool m_extended_info_fetched; // Have we tried to retrieve the m_extended_info
                                 // for this thread?
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index af5656b3dcad1..85045a803b07a 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -188,6 +188,8 @@ class Scalar;
 class ScriptInterpreter;
 class ScriptInterpreterLocker;
 class ScriptedFrameInterface;
+class ScriptedFrameProvider;
+class ScriptedFrameProviderInterface;
 class ScriptedMetadata;
 class ScriptedBreakpointInterface;
 class ScriptedPlatformInterface;
@@ -411,6 +413,10 @@ typedef std::shared_ptr<lldb_private::ScriptSummaryFormat>
 typedef std::shared_ptr<lldb_private::ScriptInterpreter> ScriptInterpreterSP;
 typedef std::shared_ptr<lldb_private::ScriptedFrameInterface>
     ScriptedFrameInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedFrameProvider>
+    ScriptedFrameProviderSP;
+typedef std::shared_ptr<lldb_private::ScriptedFrameProviderInterface>
+    ScriptedFrameProviderInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedMetadata> ScriptedMetadataSP;
 typedef std::unique_ptr<lldb_private::ScriptedPlatformInterface>
     ScriptedPlatformInterfaceUP;
diff --git a/lldb/source/API/SBThread.cpp b/lldb/source/API/SBThread.cpp
index 4e4aa48bc9a2e..a18d540f2a017 100644
--- a/lldb/source/API/SBThread.cpp
+++ b/lldb/source/API/SBThread.cpp
@@ -39,6 +39,7 @@
 #include "lldb/Target/ThreadPlanStepOut.h"
 #include "lldb/Target/ThreadPlanStepRange.h"
 #include "lldb/Utility/Instrumentation.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/State.h"
 #include "lldb/Utility/Stream.h"
 #include "lldb/Utility/StructuredData.h"
@@ -1324,3 +1325,29 @@ SBValue SBThread::GetSiginfo() {
     return SBValue();
   return thread_sp->GetSiginfoValue();
 }
+
+void SBThread::RegisterFrameProvider(const char *class_name,
+                                     SBStructuredData &dict) {
+  LLDB_INSTRUMENT_VA(this, class_name, args_data);
+
+  ThreadSP thread_sp = m_opaque_sp->GetThreadSP();
+  if (!thread_sp)
+    return;
+  
+  if (!dict.IsValid() || !dict.m_impl_up)
+    return;
+
+  StructuredData::ObjectSP obj_sp = dict.m_impl_up->GetObjectSP();
+
+  if (!obj_sp)
+    return;
+
+  StructuredData::DictionarySP dict_sp =
+      std::make_shared<StructuredData::Dictionary>(obj_sp);
+  if (!dict_sp || dict_sp->GetType() == lldb::eStructuredDataTypeInvalid)
+    return;
+
+
+  ScriptedMetadata metadata(class_name, dict_sp);
+  thread_sp->SetScriptedFrameProvider(metadata);
+}
diff --git a/lldb/source/Commands/CommandObjectFrame.cpp b/lldb/source/Commands/CommandObjectFrame.cpp
index 88a02dce35b9d..02d62aa9249d1 100644
--- a/lldb/source/Commands/CommandObjectFrame.cpp
+++ b/lldb/source/Commands/CommandObjectFrame.cpp
@@ -16,6 +16,7 @@
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/OptionArgParser.h"
 #include "lldb/Interpreter/OptionGroupFormat.h"
+#include "lldb/Interpreter/OptionGroupPythonClassWithDict.h"
 #include "lldb/Interpreter/OptionGroupValueObjectDisplay.h"
 #include "lldb/Interpreter/OptionGroupVariable.h"
 #include "lldb/Interpreter/Options.h"
@@ -29,6 +30,7 @@
 #include "lldb/Target/Target.h"
 #include "lldb/Target/Thread.h"
 #include "lldb/Utility/Args.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/ValueObject/ValueObject.h"
 
 #include <memory>
@@ -1223,6 +1225,97 @@ class CommandObjectFrameRecognizer : public CommandObjectMultiword {
   ~CommandObjectFrameRecognizer() override = default;
 };
 
+#pragma mark CommandObjectFrameProvider
+
+#define LLDB_OPTIONS_frame_provider_register
+#include "CommandOptions.inc"
+
+class CommandObjectFrameProviderRegister : public CommandObjectParsed {
+public:
+  CommandObjectFrameProviderRegister(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "frame provider register",
+            "Register frame provider into current thread.",
+            nullptr, eCommandRequiresThread),
+
+        m_class_options("frame provider", true, 'C', 'k', 'v', 0) {
+    m_all_options.Append(&m_class_options, LLDB_OPT_SET_1 | LLDB_OPT_SET_2,
+                         LLDB_OPT_SET_ALL);
+    m_all_options.Finalize();
+
+    AddSimpleArgumentList(eArgTypeRunArgs, eArgRepeatOptional);
+  }
+
+  ~CommandObjectFrameProviderRegister() override = default;
+
+  Options *GetOptions() override { return &m_all_options; }
+
+  std::optional<std::string> GetRepeatCommand(Args &current_command_args,
+                                              uint32_t index) override {
+    // No repeat for "process launch"...
+    return std::string("");
+  }
+
+protected:
+  void DoExecute(Args &launch_args, CommandReturnObject &result) override {
+    ScriptedMetadata metadata(m_class_options.GetName(), m_class_options.GetStructuredData());
+
+    Thread *thread = m_exe_ctx.GetThreadPtr();
+    if (!thread) {
+      result.AppendError("invalid thread");
+      return;
+    }
+
+    thread->SetScriptedFrameProvider(metadata);
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+    result.AppendMessageWithFormat(
+        "Successfully registered scripted frame provider '%s'\n",
+        m_class_options.GetName().c_str());
+  }
+
+  OptionGroupPythonClassWithDict m_class_options;
+  OptionGroupOptions m_all_options;
+};
+
+class CommandObjectFrameProviderClear : public CommandObjectParsed {
+public:
+  CommandObjectFrameProviderClear(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "frame provider clear",
+                            "Delete registered frame provider.", nullptr) {}
+
+  ~CommandObjectFrameProviderClear() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Thread *thread = m_exe_ctx.GetThreadPtr();
+    if (!thread) {
+      result.AppendError("invalid thread");
+      return;
+    }
+    
+    thread->ClearScriptedFrameProvider();
+    
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+class CommandObjectFrameProvider : public CommandObjectMultiword {
+public:
+  CommandObjectFrameProvider(CommandInterpreter &interpreter)
+      : CommandObjectMultiword(
+            interpreter, "frame provider",
+            "Commands for registering and viewing frame providers.",
+            "frame provider [<sub-command-options>] ") {
+    LoadSubCommand("register", CommandObjectSP(new CommandObjectFrameProviderRegister(
+                              interpreter)));
+    LoadSubCommand(
+        "clear",
+        CommandObjectSP(new CommandObjectFrameProviderClear(interpreter)));
+  }
+
+  ~CommandObjectFrameProvider() override = default;
+};
+
 #pragma mark CommandObjectMultiwordFrame
 
 // CommandObjectMultiwordFrame
@@ -1243,6 +1336,8 @@ CommandObjectMultiwordFrame::CommandObjectMultiwordFrame(
   LoadSubCommand("variable",
                  CommandObjectSP(new CommandObjectFrameVariable(interpreter)));
 #if LLDB_ENABLE_PYTHON
+  LoadSubCommand("provider", CommandObjectSP(new CommandObjectFrameProvider(
+                                   interpreter)));
   LoadSubCommand("recognizer", CommandObjectSP(new CommandObjectFrameRecognizer(
                                    interpreter)));
 #endif
diff --git a/lldb/source/Commands/CommandObjectThread.cpp b/lldb/source/Commands/CommandObjectThread.cpp
index bbec714642ec9..0092151a13dd8 100644
--- a/lldb/source/Commands/CommandObjectThread.cpp
+++ b/lldb/source/Commands/CommandObjectThread.cpp
@@ -35,6 +35,7 @@
 #include "lldb/Target/ThreadPlanStepInRange.h"
 #include "lldb/Target/Trace.h"
 #include "lldb/Target/TraceDumper.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/State.h"
 #include "lldb/ValueObject/ValueObject.h"
 
diff --git a/lldb/source/Interpreter/CMakeLists.txt b/lldb/source/Interpreter/CMakeLists.txt
index 8af7373702c38..ab877ddeecba8 100644
--- a/lldb/source/Interpreter/CMakeLists.txt
+++ b/lldb/source/Interpreter/CMakeLists.txt
@@ -53,6 +53,7 @@ add_lldb_library(lldbInterpreter NO_PLUGIN_DEPENDENCIES
   OptionGroupWatchpoint.cpp
   Options.cpp
   Property.cpp
+  ScriptedFrameProvider.cpp
   ScriptInterpreter.cpp
 
   ADDITIONAL_HEADER_DIRS
diff --git a/lldb/source/Interpreter/ScriptedFrameProvider.cpp b/lldb/source/Interpreter/ScriptedFrameProvider.cpp
new file mode 100644
index 0000000000000..b35ed5d20f6ba
--- /dev/null
+++ b/lldb/source/Interpreter/ScriptedFrameProvider.cpp
@@ -0,0 +1,86 @@
+//===-- ScriptedFrameProvider.cpp ----------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Interpreter/ScriptedFrameProvider.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Target/Process.h"
+#include "lldb/Target/Thread.h"
+#include "lldb/Utility/ScriptedMetadata.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/Support/Error.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+ScriptedFrameProvider::ScriptedFrameProvider(
+    ThreadSP thread_sp, const ScriptedMetadata &scripted_metadata,
+    Status &error)
+    : m_thread_sp(thread_sp), m_interface_sp(nullptr) {
+  if (!m_thread_sp) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Invalid thread");
+    return;
+  }
+
+  ProcessSP process_sp = m_thread_sp->GetProcess();
+  if (!process_sp) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Invalid process");
+    return;
+  }
+
+  ScriptInterpreter *script_interp =
+      process_sp->GetTarget().GetDebugger().GetScriptInterpreter();
+  if (!script_interp) {
+    error = Status::FromErrorString("cannot create scripted frame provider: No "
+                                    "script interpreter installed");
+    return;
+  }
+
+  m_interface_sp = script_interp->CreateScriptedFrameProviderInterface();
+  if (!m_interface_sp) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Script interpreter couldn't "
+        "create Scripted Frame Provider Interface");
+    return;
+  }
+
+  auto obj_or_err = m_interface_sp->CreatePluginObject(
+      scripted_metadata.GetClassName(), m_thread_sp,
+      scripted_metadata.GetArgsSP());
+  if (!obj_or_err) {
+    error = Status::FromError(obj_or_err.takeError());
+    return;
+  }
+
+  StructuredData::ObjectSP object_sp = *obj_or_err;
+  if (!object_sp || !object_sp->IsValid()) {
+    error = Status::FromErrorString(
+        "cannot create scripted frame provider: Failed to create valid script "
+        "object");
+    return;
+  }
+
+  error.Clear();
+}
+
+ScriptedFrameProvider::~ScriptedFrameProvider() = default;
+
+llvm::Expected<StackFrameListSP> ScriptedFrameProvider::GetStackFrames() {
+  if (!m_interface_sp)
+    return llvm::createStringError(
+        "cannot get stack frames: Scripted frame provider not initialized");
+
+  auto frames = m_interface_sp->GetStackFrames();
+
+  // TODO: Convert StructuredData::ArraySP to StackFrameListSP
+  // This is a placeholder for now
+  return nullptr;
+}
\ No newline at end of file
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
index 09103573b89c5..50569cdefaafa 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
@@ -23,6 +23,7 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces PLUGIN
   OperatingSystemPythonInterface.cpp
   ScriptInterpreterPythonInterfaces.cpp
   ScriptedFramePythonInterface.cpp
+  ScriptedFrameProviderPythonInterface.cpp
   ScriptedPlatformPythonInterface.cpp
   ScriptedProcessPythonInterface.cpp
   ScriptedPythonInterface.cpp
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 3814f46615078..0b9c7eb107bf5 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -18,6 +18,7 @@
 #include "OperatingSystemPythonInterface.h"
 #include "ScriptedBreakpointPythonInterface.h"
 #include "ScriptedFramePythonInterface.h"
+#include "ScriptedFrameProviderPythonInterface.h"
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
new file mode 100644
index 0000000000000..b9a659c44e6f1
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedFrameProviderPythonInterface.cpp
@@ -0,0 +1,57 @@
+//===-- ScriptedFrameProviderPythonInterface.cpp -------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/Config.h"
+#include "lldb/Target/Thread.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#if LLDB_ENABLE_PYTHON
+
+// LLDB Python header must be included first
+#include "../lldb-python.h"
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedFrameProviderPythonInterface.h"
+#include <optional>
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+using Locker = ScriptInterpreterPythonImpl::Locker;
+
+ScriptedFrameProviderPythonInterface::Scripte...
[truncated]

@github-actions
Copy link

github-actions bot commented Oct 3, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@medismailben medismailben force-pushed the scripted-frame-provider branch 3 times, most recently from 89e7ab6 to 9b9d08e Compare October 4, 2025 13:08
@medismailben medismailben changed the title [lldb] Introduce ScriptedFrameProvider [lldb] Introduce ScriptedFrameProvider for real threads Oct 4, 2025
@medismailben medismailben force-pushed the scripted-frame-provider branch 2 times, most recently from 5fd3ebc to 70d72a2 Compare October 4, 2025 14:37
Copy link
Member

@vogelsgesang vogelsgesang left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being able to modify the frame list from a script would indeed be amazing - thanks for looking into this! 🙂

As it happens, I am currently looking into gdb's "frame filter" API, which serves a very similar use case - and I am wondering if we should take some inspiration from the gdb APIs here. Gdb's frame filter API differs from the API in this PR in the following points:

  • simpler API: gdb's API only has a single function filter(frame_iter) -> frame_iter function. (This PR currently introduces two functions: get_merge_strategy and get_stack_frames)
  • lazy stack unwinding: afaict, your patch leads to eager materialization of all stack frames. Given that stacks can be pretty deep, it would be preferable to only create those stack frames lazily, as the users inspects the stack. gdb achieves this by only advancing the returned iterator as needed
  • more flexible merging strategies: in gdb, I can use all of Python's iterator support (generator expressions, yield, ...)
  • global registration: in gdb, frame filters are registered globally for all threads. In this PR, the frame providers are registered for each thread individually
  • multiple frame filters: gdb supports registering multiple frame filters at the same time. I didn't use that mechanism, yet, but afaict they are simply chained

The relevant pieces of gdb's documentation:

My motivation for using frame filters in gdb are C++ couroutines. I want to add frames for asynchronous operations. For more background see LLVM's documentatin on Async Stack Traces, in particular the coro bt example. At least for my use case, gdb's design decisions (global registration, flexible merging strategy, ...) are a pretty good fit. But not sure, maybe your current design is a better fit for your particular use case? Which use case are you envisioning?

By the way: would discourse be better-suited for discussing the best approach here? Happy to copy my reply over to discourse, if you would start a thread

@medismailben
Copy link
Member Author

Hello! Thanks for your interest in this PR.

Being able to modify the frame list from a script would indeed be amazing - thanks for looking into this! 🙂

As it happens, I am currently looking into gdb's "frame filter" API, which serves a very similar use case - and I am wondering if we should take some inspiration from the gdb APIs here. Gdb's frame filter API differs from the API in this PR in the following points:

A non-goal for this PR and for lldb in general is copy what gdb or other debuggers do.

  • simpler API: gdb's API only has a single function filter(frame_iter) -> frame_iter function. (This PR currently introduces two functions: get_merge_strategy and get_stack_frames)
  • lazy stack unwinding: afaict, your patch leads to eager materialization of all stack frames. Given that stacks can be pretty deep, it would be preferable to only create those stack frames lazily, as the users inspects the stack. gdb achieves this by only advancing the returned iterator as needed
  • more flexible merging strategies: in gdb, I can use all of Python's iterator support (generator expressions, yield, ...)
  • global registration: in gdb, frame filters are registered globally for all threads. In this PR, the frame providers are registered for each thread individually
  • multiple frame filters: gdb supports registering multiple frame filters at the same time. I didn't use that mechanism, yet, but afaict they are simply chained

The relevant pieces of gdb's documentation:

My motivation for using frame filters in gdb are C++ couroutines. I want to add frames for asynchronous operations. For more background see LLVM's documentatin on [Async Stack Traces]f(https://clang.llvm.org/docs/DebuggingCoroutines.html#async-stack-traces), in particular the coro bt example. At least for my use case, gdb's design decisions (global registration, flexible merging strategy, ...) are a pretty good fit. But not sure, maybe your current design is a better fit for your particular use case? Which use case are you envisioning?

That sounds pretty exciting. I think ScriptedFrameProvider would be a great fit to support coroutines as long as the ABI doesn't spawn new threads for that. My current use case for this is to translate CPython frames into something that's more understandable, to hopefully improve debugging python / C++ interoperability in lldb.

By the way: would discourse be better-suited for discussing the best approach here? Happy to copy my reply over to discourse, if you would start a thread

@JDevlieghere
Copy link
Member

A non-goal for this PR and for lldb in general is copy what gdb or other debuggers do.

While compatibility is not generally a goal, there's also no reason to diverge for the sake of it. However, what I think Adrian is suggesting here, is that we can benefit from building on top of the learnings of an existing implementation. I think he raises some good points that are worth evaluating in the context of LLDB.

@medismailben medismailben force-pushed the scripted-frame-provider branch from 70d72a2 to ab4a9b8 Compare October 6, 2025 17:00
@vogelsgesang
Copy link
Member

vogelsgesang commented Oct 6, 2025

A non-goal for this PR and for lldb in general is copy what gdb or other debuggers do.

While compatibility is not generally a goal, there's also no reason to diverge for the sake of it. However, what I think Adrian is suggesting here, is that we can benefit from building on top of the learnings of an existing implementation.

yes. Thanks for summing it up!

Use cases

My (@vogelsgesang's) motivation for using frame filters in gdb are C++ coroutines.

I polished my existing script and now posted it in PR #162145. The most interesting piece is CppCoroutineFrameFilter. With this filter, I get the backtrace

(gdb) bt
#0  write_output(std::basic_string_view<char, std::char_traits<char> >) [clone .resume] (contents=...) at async-task-example.cpp:6
[async] greet () at async-task-example.cpp:12
[async] std::__n4861::coroutine_handle<std::__n4861::noop_coroutine_promise>::__frame::__dummy_resume_destroy() at <...>
#1  0x000055555555a0f8 in std::__n4861::coroutine_handle<task::promise_type>::resume (this=0x7fffffffd5a8) at /usr/include/c++/14/coroutine:242
#2  0x0000555555557bb9 in task::syncStart (this=0x7fffffffd5a8) at async-task-library.hpp:78
#3  0x00005555555552de in main () at async-task-example.cpp:18

The two lines prefixed with [async] are the additional frames injected by the yield from _create_coroutine_frames(parent_coro, inferior_frame) in the frame filter. (The C++ source code can also be found in the another section of the same guide, in case anyone wants to reproduce this example)

My (@medismailben's) current use case for this is to translate CPython frames into something that's more understandable, to hopefully improve debugging python / C++ interoperability in lldb.

Could you provide an example of a ScriptedFrameProvider implementation for pretty-printing the CPython frames? Or at least the intended bt output which you are envisioning for an example Python program?

I am currently slightly struggling with imagining the intended usage of the proposed APIs, and such an example might help me. (Of course only if possible and not blocked, e.g., due to intellectual property issues)

@medismailben medismailben force-pushed the scripted-frame-provider branch 2 times, most recently from ba84a60 to 9b2a82d Compare October 8, 2025 14:36
@medismailben
Copy link
Member Author

@vogelsgesang I've added a new SBFrameList class with iterators since there was so way to pass the existing StackFrameList in lazy way to the user script and updated the calls to get_stackframes to take it as an argument.

I'm still waiting on @jimingham feedback on this before move forward but let me know if this newer approach fits better into your coroutines workflow.

@medismailben medismailben force-pushed the scripted-frame-provider branch from 9b2a82d to 5b21aa8 Compare October 8, 2025 14:48
@vogelsgesang
Copy link
Member

vogelsgesang commented Oct 8, 2025

Thanks for making the frame iteration lazy!

let me know if this newer approach fits better into your coroutines workflow.

There are still two larger issues and one smaller:

  1. Lazy stack unwinding
  2. Merging strategy
  3. (smaller issue) Registration

Lazy stack unwinding

Unfortunately, I don't think that SBFrameList as currently in review is sufficient. The input parameter to get_stack_frames now allows me to lazily iterate over the input stack frames. But the result type of get_stack_frames is still List[Dict], i.e. I still have to generate all artificial frames in one go. Even if the user only displays the first 20 stack frames, for deep stacks I have to synthesize 100s or 1000s of Dict entries which the user will never see.

Merging strategy

None of the provided merging strategies (replace, prepend, append, replace-by-index) matches the need for coroutine stack traces. I need to splice in additional stack frames in the middle of real, physical stack frames.

I still think that the best, most future proof solution here would be to delegate the merging strategy to the script.

I.e. have the interface be get_stack_frames(Iterable[SBFrame]) -> Iterable[SBFrame], where SBFrame might be either a forwarded physical frame or an artificial ScriptedFrame. I guess ScriptedFrame should inherit from SBFrame? Or there should be a SBFrame.FromScriptedFrame factory function? Or something similar? In case none of this is possible, we could use get_stack_frames(Iterable[SBFrame]) -> Iterable[SBFrame | Dict | ScriptedFrame] or some similar signature

Registration

At least for my use case, I would like to globally register the frame filter for all threads.
The frame provider would then inspect the stack frames and see if any C++ coroutines were used. If so, it would inject the additional threads.
The end user wouldn't need to register the frame provider for individual threads, and things would just work "out of the box".

CPython Use Case

I think that at least "Lazy stack unwinding" and "Registration" would also be issues for your CPython use case.

You probably don't want to eagerly unwind the full stack to identify which frames to replace (afaik, the CPython frames could be arbitrarily far up in the call stack and you wouldn't have a way to terminate the stack traversal in get_stack_frames early?). As such, also your script could benefit from lazy unwinding?

Furthermore, it would probably also be more convenient for your users if your script automatically becomes active for all relevant CPython frames on all threads, without the user having associate individual threads with your frame provider?

But maybe I am still misunderstanding your use case? Can you share your CPython frame provider script?

@felipepiovezan
Copy link
Contributor

the need for coroutine stack traces. I need to splice in additional stack frames in the middle of real, physical stack frames.

@vogelsgesang Have you considered writing a custom unwinder for this? If you want stack frames from which you can perform step operations, I think you need to go down language-plugin-unwinder route. This is what we do for swift async functions (which have a lot of similarities to C++ coroutines).

@vogelsgesang
Copy link
Member

@vogelsgesang Have you considered writing a custom unwinder for this?

I didn't know about that API, yet. Where can I learn more about it?

Could those custom unwinders also solve @medismailben's CPython-frame-debugging use case?
If not, what would it take to make custom unwinders fit that use case?
Should we maybe extend the custom unwinder support, instead of introducing a new "frame provider" concept?

If you want stack frames from which you can perform step operations, I think you need to go down language-plugin-unwinder route.

That would be amazing. Is there any relevant documentation / examples where I could learn from? E.g., in Apple's open-source LLVM version? Maybe there are even publicly available design documents?

@jimingham
Copy link
Collaborator

jimingham commented Oct 10, 2025

There are two parts to this project. One is how to make synthetic stack frames that can be consed up and returned by some kind of scripted stack frame list entity. The other is how to structure the entity that uses synthetic stack frames to re-present the original StackFrameList of a thread. I'm mostly commenting on the second part of the design here. I agree with the other people commenting here that the latter part of the design is trying to help where it doesn't need to and ends up being confusing.

To start with, the StackFrameList is better thought of as a generator of StackFrames that answers "give me stack frame at index N" than something that has a list of all the frames in it. So instead of thinking about this as a static process of "inserting or replacing frames in a stack frame list" it would be clearer if this design where we're re-presenting extant StackFramesLists from a thread - not creating them from whole cloth like the ScriptedThreads do - were modeled as a sequence of generators that uses some incoming StackFrameList generator as the source for it's reply to "give me stack frame at index N". So this would look something like:

  1. Adding a SyntheticStackFrameList subclass of StackFrameList that is constructed from a source StackFrameList and a StackFrameList transmogrifier where the transmogrifier is something that takes a incoming StackFrameList and uses it and whatever internal logic it wants to implement the answer to "give me stack frame at index N" which is what gets asked of the base StackFrameList.

  2. A system in the Thread to hold a chain of StackFrameLists , and on request to produce "stack frame at index N" from any point in this list of transmogrified stack frames. We might at the start just have one "real" frame and one "transmogrifier" and extend that to a chain of them later, but we should keep the possibility of chaining them in mind. In either case, we want to support the ability to show users the raw "native" stack frame as well as the cooked one. So there should be a gesture to ask at various points in the chain.

As an aside, I don't think the notion of an unwinder is the right model. The unwinder's job is "Given Stack Frame N, produce THE frame one older than it." But for instance, way back in the day I did a C-Stack to Tcl stack transmogrifier (this was in gdbtk and some 30 years ago...) based on the fact that I could recognize the C stack frame sequence corresponding to one Tcl function invocation (the sequence was 3 or 4 frames long, I can't remember the details now). So in my transmogrifier, I ran through the incoming C stack and if I saw this pattern of C Frames, I elided them into a single frame that had the Tcl function source, and variables and presented that as a single frame, with all the indexes suitably adjusted for the elision.

So the notion we want here is not an unwinder but a transmogrifier that takes a source stack frame list and produces another StackFrameList, or really "intercepts the request for 'give me frame at index N" and uses its stackframelist StackFrameList to answer the question.

I don't think we need to be more specific about it than that, so I don't for instance see the various insertion modes as useful. The same transmogrifier might do any of those operations depending on what it sees in the StackFrameList it was given to work on. Instead, when you ask a thread for the "stack frame at index 5" from a thread, this would just ask the top-most transmogrifier to produce the stack up to frame 5. We don't need to care how it managed that.

If there are no stack frame provider(s) attached to the thread (or the user did thread backtrace --raw), that would just ask the native unwinder based stack frame (or if this was a Scripted Thread, the scripted thread's list) for Frame 5.

If there were a transmogrifier added then it would ask the ScriptedStackFrameList to use its transmogrifier and the StackFrameList it is based off for the requested frame. That would do whatever it wanted (I don't think we need to get involved with that) and produce frame 5. This would very naturally chain, since when you added a second ScriptedStackFrameList to the Thread, it would get the first Scripted StackFrameList as its source StackFrameList.

@jimingham
Copy link
Collaborator

BTW, I used the term generator when I probably shouldn't because that's more a term of art. I don't think "real generating iterators" are needed here, because we're really not building lists, we're always only answering the question "Give me the stack frame at index 5 or tell me it doesn't exist". Internally, if the StackFrameList was last asked about frame index 3, it knows that to provide 5 it has to start with 3 and generate 4 and then 5. But I can't see why we need fancier machinery than "the transmogrifier keeps track of how many frames it has generated so far, and does what it needs to to fill the gap between what it was last asked to fetch and what it was asked to fetch this time round.

@jimingham
Copy link
Collaborator

As far as registration goes, I think we should probably have a AppliesToThread type API as part of the ScriptedStackFrameList API. Some of these providers might very well know "All the frames I am going to recognize are named with some recognizable pattern". Or they might want to have a look at all frames.

However, there should also be a way to override this from the command line or with the SBThread API so if I say something like:

thread stack-provider add --python-class my-recognizer 5
then internally we override the Python my_recognizer.applies_to_thread with one that says "if thread index == 5". If the ScriptedStackFrameList production is expensive and you only care about one thread, we shouldn't make you pay the cost for the others, regardless of what the provider thinks.

Since we don't want to force unwinding except when we really have to, the AppliesToThread should only check static data about the thread (like its name, etc.). If your provider doesn't know whether it would apply without backtracing, it's probably better to just always ask it when backtracking the threads. That's presuming that the task of "I don't have anything to add to the raw stack frame" is cheap, which it should be.

If you don't implement applies_to_thread, then it will default to getting inserted on all new thread.

@jimingham
Copy link
Collaborator

jimingham commented Oct 10, 2025

And as far as stepping goes - which this PR doesn't really address - we will also need to have an API in the StackFrameList provider like

virtual SBThreadPlan StackFrameList::PerformSteppingOperation(uint64_t stack_index, lldb::eStepType step_type);
(*)

If there was a synthetic StackFrameList, it would decide what next meant to it in this frame, and return a thread plan that makes that happen using either "RunToBreakpoint" thread plans or any of the other primitive types.

For instance, if I've done my Tcl transmogrification, and I can tell that the state of the Tcl C frames that it is stopped at line 4 of some Tcl source, then I can cons up a thread plan to put a breakpoint on where the implementation code fetches the next source line or break on some "new context" observer function if the language I'm emulating provides that, and then run till that shows a changed source line.

I do think this job needs to be done in the Provider, and not hung off a particular SyntheticStackFrame. I might want "step out of one of my synthetic Tcl stack frames" to mean finding the Tcl frame that's next older on the C Stack (in Tcl the C & Tcl stacks mirror one another) and just run till we get back to that. I don't think the StackFrame I'm stepping out of can know how to do that on its own. That seems more like a job for the overall provider.

And if we have a general mechanism we don't have to know about any of how this works, that's the provider's job.

Then, since the scripted StackFrameList provider always ends at the "natural" stack frame list, when the oldest SyntheticStackFrameList requests PerformSteppingOperation, that will go to the "natural" StackFrameList, whose PerformSteppingOperation will just return the current StepOver, etc thread plans for that thread.

I think this shouldn't be that difficult to implement, and we already have support for writing scripted thread plans so the providers can do pretty much anything execution-control-wise that lldb can do internally. So this should be pretty flexible.

(*) This isn't the complete API since some of the stepping operations take parameters, so we'll need to come up with a way to express those as well.

@jimingham
Copy link
Collaborator

I left a bunch of individual comments. Beyond that I think a couple of UE improvements will really help. I think you should be able to enable and disable these - after all you might have some package that adds these and you want the other things in the package but don't want to turn on the frame providers now. If you clear them, you have to remember how you got them in the first place which might not be clear.
I also think that there needs to be a "description" method that the frame providers can implement. If target frame-providers list just shows a bunch of Python class names, that's might not be very helpful. There should be a description API that the frame providers implement (another static method, probably) that feeds the Dump() method you call when presenting the list of providers.

@medismailben medismailben force-pushed the scripted-frame-provider branch 6 times, most recently from d3e543f to 836bdae Compare November 11, 2025 18:50
@jimingham
Copy link
Collaborator

I have three small quibbles and then this is good. There was one place where you missed provider-name -> provider-id. I think target frame provider clear should support multiple id's but if you don't want to do that in this patch, then define the argument repeat type correctly and you won't have check for more than one by hand.
And in the code that's unwinding provided frames, you should check for WasInterrupted and bail if it was set.

@medismailben medismailben force-pushed the scripted-frame-provider branch from 836bdae to 106f215 Compare November 11, 2025 19:34
Copy link
Collaborator

@jimingham jimingham left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with the trivial provider -> providers swap.

@medismailben medismailben force-pushed the scripted-frame-provider branch from 106f215 to d5516f6 Compare November 11, 2025 20:09
This patch extends ScriptedFrame to work with real (non-scripted) threads,
enabling frame providers to synthesize frames for native processes.

Previously, ScriptedFrame only worked within ScriptedProcess/ScriptedThread
contexts. This patch decouples ScriptedFrame from ScriptedThread, allowing
users to augment or replace stack frames in real debugging sessions for use
cases like custom calling conventions, reconstructing corrupted frames from
core files, or adding diagnostic frames.

Key changes:

- ScriptedFrame::Create() now accepts ThreadSP instead of requiring
ScriptedThread, extracting architecture from the target triple rather
than ScriptedProcess.arch

- Added SBTarget::RegisterScriptedFrameProvider() and
ClearScriptedFrameProvider() APIs, with Target storing a
SyntheticFrameProviderDescriptor template for new threads

- Added "target frame-provider register/clear" commands for CLI access

- Thread class gains LoadScriptedFrameProvider(), ClearScriptedFrameProvider(),
and GetFrameProvider() methods for per-thread frame provider management

- New SyntheticStackFrameList overrides FetchFramesUpTo() to lazily provide
frames from either the frame provider or the real stack

This enables practical use of the SyntheticFrameProvider infrastructure in
real debugging workflows.

rdar://161834688

Signed-off-by: Med Ismail Bennani <[email protected]>
@medismailben medismailben force-pushed the scripted-frame-provider branch from d5516f6 to 88c30af Compare November 11, 2025 20:10
@medismailben medismailben enabled auto-merge (squash) November 11, 2025 20:11
@medismailben medismailben merged commit 1e467e4 into llvm:main Nov 11, 2025
8 of 9 checks passed
medismailben added a commit to medismailben/llvm-project that referenced this pull request Nov 11, 2025
This patch extends ScriptedFrame to work with real (non-scripted)
threads,
enabling frame providers to synthesize frames for native processes.

Previously, ScriptedFrame only worked within
ScriptedProcess/ScriptedThread
contexts. This patch decouples ScriptedFrame from ScriptedThread,
allowing
users to augment or replace stack frames in real debugging sessions for
use
cases like custom calling conventions, reconstructing corrupted frames
from
core files, or adding diagnostic frames.

Key changes:

- ScriptedFrame::Create() now accepts ThreadSP instead of requiring
ScriptedThread, extracting architecture from the target triple rather
than ScriptedProcess.arch

- Added SBTarget::RegisterScriptedFrameProvider() and
ClearScriptedFrameProvider() APIs, with Target storing a
SyntheticFrameProviderDescriptor template for new threads

- Added "target frame-provider register/clear" commands for CLI access

- Thread class gains LoadScriptedFrameProvider(),
ClearScriptedFrameProvider(),
and GetFrameProvider() methods for per-thread frame provider management

- New SyntheticStackFrameList overrides FetchFramesUpTo() to lazily
provide
frames from either the frame provider or the real stack

This enables practical use of the SyntheticFrameProvider infrastructure
in
real debugging workflows.

rdar://161834688

Signed-off-by: Med Ismail Bennani <[email protected]>

Signed-off-by: Med Ismail Bennani <[email protected]>
(cherry picked from commit 1e467e4)
@llvm-ci
Copy link
Collaborator

llvm-ci commented Nov 12, 2025

LLVM Buildbot has detected a new failure on builder lldb-arm-ubuntu running on linaro-lldb-arm-ubuntu while building lldb at step 6 "test".

Full details are available at: https://lab.llvm.org/buildbot/#/builders/18/builds/22598

Here is the relevant piece of the build log for the reference
Step 6 (test) failure: build (failure)
...
PASS: lldb-api :: functionalities/reverse-execution/TestReverseContinueWatchpoints.py (610 of 2577)
PASS: lldb-api :: functionalities/reverse-execution/TestReverseContinueBreakpoints.py (611 of 2577)
UNSUPPORTED: lldb-api :: functionalities/scripted_process/TestStackCoreScriptedProcess.py (612 of 2577)
PASS: lldb-api :: functionalities/scripted_process/TestScriptedProcess.py (613 of 2577)
UNSUPPORTED: lldb-api :: functionalities/scripted_process_empty_memory_region/TestScriptedProcessEmptyMemoryRegion.py (614 of 2577)
UNSUPPORTED: lldb-api :: functionalities/set-data/TestSetData.py (615 of 2577)
PASS: lldb-api :: functionalities/show_location/TestShowLocationDwarf5.py (616 of 2577)
PASS: lldb-api :: functionalities/signal/handle-abrt/TestHandleAbort.py (617 of 2577)
PASS: lldb-api :: functionalities/signal/TestSendSignal.py (618 of 2577)
PASS: lldb-api :: functionalities/signal/handle-segv/TestHandleSegv.py (619 of 2577)
FAIL: lldb-api :: functionalities/scripted_frame_provider/TestScriptedFrameProvider.py (620 of 2577)
******************** TEST 'lldb-api :: functionalities/scripted_frame_provider/TestScriptedFrameProvider.py' FAILED ********************
Script:
--
/usr/bin/python3.10 /home/tcwg-buildbot/worker/lldb-arm-ubuntu/llvm-project/lldb/test/API/dotest.py -u CXXFLAGS -u CFLAGS --env LLVM_LIBS_DIR=/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./lib --env LLVM_INCLUDE_DIR=/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/include --env LLVM_TOOLS_DIR=/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./bin --arch armv8l --build-dir /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/lldb-test-build.noindex --lldb-module-cache-dir /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/lldb-test-build.noindex/module-cache-lldb/lldb-api --clang-module-cache-dir /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/lldb-test-build.noindex/module-cache-clang/lldb-api --executable /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./bin/lldb --compiler /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./bin/clang --dsymutil /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./bin/dsymutil --make /usr/bin/gmake --llvm-tools-dir /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./bin --lldb-obj-root /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/tools/lldb --lldb-libs-dir /home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/./lib --cmake-build-type Release /home/tcwg-buildbot/worker/lldb-arm-ubuntu/llvm-project/lldb/test/API/functionalities/scripted_frame_provider -p TestScriptedFrameProvider.py
--
Exit Code: 1

Command Output (stdout):
--
lldb version 22.0.0git (https://github.com/llvm/llvm-project.git revision 1e467e44851a9da96c16c0dcd16725f996e6abf7)
  clang revision 1e467e44851a9da96c16c0dcd16725f996e6abf7
  llvm revision 1e467e44851a9da96c16c0dcd16725f996e6abf7
Skipping the following test categories: ['libc++', 'msvcstl', 'dsym', 'pdb', 'gmodules', 'debugserver', 'objc']

--
Command Output (stderr):
--
PASS: LLDB (/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang-arm) :: test_append_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
FAIL: LLDB (/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang-arm) :: test_applies_to_thread (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
PASS: LLDB (/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang-arm) :: test_prepend_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
PASS: LLDB (/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang-arm) :: test_remove_frame_provider_by_id (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
PASS: LLDB (/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang-arm) :: test_replace_all_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
FAIL: LLDB (/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang-arm) :: test_scripted_frame_objects (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
======================================================================
FAIL: test_applies_to_thread (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
   Test that applies_to_thread filters which threads get the provider.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tcwg-buildbot/worker/lldb-arm-ubuntu/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 223, in test_applies_to_thread
    self.assertEqual(
AssertionError: 65534 != 65535 : Thread with ID 1 should have synthetic PC 0xFFFF
Config=arm-/home/tcwg-buildbot/worker/lldb-arm-ubuntu/build/bin/clang
======================================================================
FAIL: test_scripted_frame_objects (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
   Test that provider can return ScriptedFrame objects.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/tcwg-buildbot/worker/lldb-arm-ubuntu/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 152, in test_scripted_frame_objects

@Michael137
Copy link
Member

Looks like this is failing on the public macOS bots (both x86 and arm64): https://green.lab.llvm.org/job/llvm.org/view/LLDB/job/as-lldb-cmake/36221/execution/node/106/log/

09:27:59  ======================================================================
09:27:59  FAIL: test_append_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames after real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 122, in test_append_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 1)
09:27:59  AssertionError: 5 != 6
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_applies_to_thread (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that applies_to_thread filters which threads get the provider.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 218, in test_applies_to_thread
09:27:59      self.assertEqual(
09:27:59  AssertionError: 5 != 1 : Thread with ID 1 should have 1 synthetic frame
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_prepend_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames before real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 84, in test_prepend_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 2)
09:27:59  AssertionError: 5 != 7
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_remove_frame_provider_by_id (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that RemoveScriptedFrameProvider removes a specific provider by ID.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 272, in test_remove_frame_provider_by_id
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_replace_all_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can replace the entire stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 41, in test_replace_all_frames
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_scripted_frame_objects (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that provider can return ScriptedFrame objects.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 159, in test_scripted_frame_objects
09:27:59      self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
09:27:59  AssertionError: 'thread_func(int)' != 'custom_scripted_frame_0'
09:27:59  - thread_func(int)
09:27:59  + custom_scripted_frame_0
09:27:59  
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ----------------------------------------------------------------------
09:27:59  Ran 6 tests in 14.242s
09:27:59  
09:27:59  FAILED (failures=6)

Reverting for now

@DavidSpickett
Copy link
Collaborator

I'm looking at the Arm failures. First part is probably a Thumb function, second part might be a codegen difference.

Michael137 added a commit that referenced this pull request Nov 12, 2025
…7662)

The new test fails on x86 and arm64 public macOS bots:
```
09:27:59  ======================================================================
09:27:59  FAIL: test_append_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames after real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 122, in test_append_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 1)
09:27:59  AssertionError: 5 != 6
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_applies_to_thread (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that applies_to_thread filters which threads get the provider.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 218, in test_applies_to_thread
09:27:59      self.assertEqual(
09:27:59  AssertionError: 5 != 1 : Thread with ID 1 should have 1 synthetic frame
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_prepend_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames before real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 84, in test_prepend_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 2)
09:27:59  AssertionError: 5 != 7
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_remove_frame_provider_by_id (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that RemoveScriptedFrameProvider removes a specific provider by ID.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 272, in test_remove_frame_provider_by_id
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_replace_all_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can replace the entire stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 41, in test_replace_all_frames
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_scripted_frame_objects (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that provider can return ScriptedFrame objects.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 159, in test_scripted_frame_objects
09:27:59      self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
09:27:59  AssertionError: 'thread_func(int)' != 'custom_scripted_frame_0'
09:27:59  - thread_func(int)
09:27:59  + custom_scripted_frame_0
09:27:59  
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ----------------------------------------------------------------------
09:27:59  Ran 6 tests in 14.242s
09:27:59  
09:27:59  FAILED (failures=6)
```

Reverts #161870
@DavidSpickett
Copy link
Collaborator

The PC value is because we know we're in Arm mode. Something is removing the bottom bit of the fake PC value to reflect that. Which is probably fine.

The other failure is that we get no frames at all:

(Pdb) p thread.GetNumFrames()
0
(Pdb) p thread
python3: /home/david.spickett/llvm-project/lldb/source/Target/StackFrameList.cpp:687: StackFrameSP lldb_private::StackFrameList::GetFrameAtIndex(uint32_t): Assertion `!m_thread.IsValid() && "A valid thread has no frames."' failed.
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace and instructions to reproduce the bug.
#0 0xe1dc0758 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) Signals.cpp:0:0
#1 0xe1dbdc3c llvm::sys::RunSignalHandlers() Signals.cpp:0:0
#2 0xe1dc1784 SignalHandler(int, siginfo_t*, void*) Signals.cpp:0:0
#3 0xe84dfe50 __default_rt_sa_restorer ./signal/../sysdeps/unix/sysv/linux/arm/sigrestorer.S:80:0
#4 0xe84d06c6 __libc_do_syscall ./csu/../sysdeps/unix/sysv/linux/arm/libc-do-syscall.S:47:0
#5 0xe851069c __pthread_kill_implementation ./nptl/pthread_kill.c:44:76
#6 0xe84defc6 raise ./signal/../sysdeps/posix/raise.c:27:6
Fatal Python error: Aborted

Current thread 0xe8495020 (most recent call first):
  File "/home/david.spickett/build-llvm-arm/local/lib/python3.12/dist-packages/lldb/__init__.py", line 14960 in __repr__
  File "/usr/lib/python3.12/pdb.py", line 1335 in _msg_val_func
  File "/usr/lib/python3.12/pdb.py", line 1350 in do_p
  File "/usr/lib/python3.12/cmd.py", line 217 in onecmd
  File "/usr/lib/python3.12/pdb.py", line 527 in onecmd
  File "/usr/lib/python3.12/cmd.py", line 138 in cmdloop
  File "/usr/lib/python3.12/pdb.py", line 390 in _cmdloop
  File "/usr/lib/python3.12/pdb.py", line 425 in interaction
  File "/usr/lib/python3.12/pdb.py", line 329 in user_line
  File "/usr/lib/python3.12/bdb.py", line 114 in dispatch_line
  File "/usr/lib/python3.12/bdb.py", line 90 in trace_dispatch
  File "/home/david.spickett/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 42 in test_scripted_frame_objects
  File "/usr/lib/python3.12/unittest/case.py", line 589 in _callTestMethod
  File "/usr/lib/python3.12/unittest/case.py", line 634 in run
  File "/usr/lib/python3.12/unittest/case.py", line 690 in __call__
  File "/usr/lib/python3.12/unittest/suite.py", line 122 in run
  File "/usr/lib/python3.12/unittest/suite.py", line 84 in __call__
  File "/usr/lib/python3.12/unittest/suite.py", line 122 in run
  File "/usr/lib/python3.12/unittest/suite.py", line 84 in __call__
  File "/usr/lib/python3.12/unittest/runner.py", line 240 in run
  File "/home/david.spickett/llvm-project/lldb/packages/Python/lldbsuite/test/dotest.py", line 1164 in run_suite
  File "/home/david.spickett/llvm-project/lldb/test/API/dotest.py", line 8 in <module>

Extension modules: lldb._lldb (total: 1)

Though the provider was registered successfully.

ScriptedFrameObjectProvider is asked for frame 0, but nothing further.

Since you've got MacOS things to fix I won't dig into this now. Fix those and if it still fails on Arm I'll look at it again.

llvm-sync bot pushed a commit to arm/arm-toolchain that referenced this pull request Nov 12, 2025
…reads" (#167662)

The new test fails on x86 and arm64 public macOS bots:
```
09:27:59  ======================================================================
09:27:59  FAIL: test_append_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames after real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 122, in test_append_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 1)
09:27:59  AssertionError: 5 != 6
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_applies_to_thread (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that applies_to_thread filters which threads get the provider.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 218, in test_applies_to_thread
09:27:59      self.assertEqual(
09:27:59  AssertionError: 5 != 1 : Thread with ID 1 should have 1 synthetic frame
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_prepend_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames before real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 84, in test_prepend_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 2)
09:27:59  AssertionError: 5 != 7
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_remove_frame_provider_by_id (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that RemoveScriptedFrameProvider removes a specific provider by ID.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 272, in test_remove_frame_provider_by_id
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_replace_all_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can replace the entire stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 41, in test_replace_all_frames
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_scripted_frame_objects (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that provider can return ScriptedFrame objects.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 159, in test_scripted_frame_objects
09:27:59      self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
09:27:59  AssertionError: 'thread_func(int)' != 'custom_scripted_frame_0'
09:27:59  - thread_func(int)
09:27:59  + custom_scripted_frame_0
09:27:59
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ----------------------------------------------------------------------
09:27:59  Ran 6 tests in 14.242s
09:27:59
09:27:59  FAILED (failures=6)
```

Reverts llvm/llvm-project#161870
@medismailben
Copy link
Member Author

Thanks @DavidSpickett and @Michael137 for taking care of this. I'll try to reproduce the issue locally.

git-crd pushed a commit to git-crd/crd-llvm-project that referenced this pull request Nov 13, 2025
…m#167662)

The new test fails on x86 and arm64 public macOS bots:
```
09:27:59  ======================================================================
09:27:59  FAIL: test_append_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames after real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 122, in test_append_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 1)
09:27:59  AssertionError: 5 != 6
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_applies_to_thread (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that applies_to_thread filters which threads get the provider.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 218, in test_applies_to_thread
09:27:59      self.assertEqual(
09:27:59  AssertionError: 5 != 1 : Thread with ID 1 should have 1 synthetic frame
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_prepend_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can add frames before real stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 84, in test_prepend_frames
09:27:59      self.assertEqual(new_frame_count, original_frame_count + 2)
09:27:59  AssertionError: 5 != 7
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_remove_frame_provider_by_id (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that RemoveScriptedFrameProvider removes a specific provider by ID.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 272, in test_remove_frame_provider_by_id
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_replace_all_frames (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that we can replace the entire stack.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 41, in test_replace_all_frames
09:27:59      self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
09:27:59  AssertionError: 5 != 3 : Should have 3 synthetic frames
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ======================================================================
09:27:59  FAIL: test_scripted_frame_objects (TestScriptedFrameProvider.ScriptedFrameProviderTestCase)
09:27:59     Test that provider can return ScriptedFrame objects.
09:27:59  ----------------------------------------------------------------------
09:27:59  Traceback (most recent call last):
09:27:59    File "/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/llvm-project/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py", line 159, in test_scripted_frame_objects
09:27:59      self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
09:27:59  AssertionError: 'thread_func(int)' != 'custom_scripted_frame_0'
09:27:59  - thread_func(int)
09:27:59  + custom_scripted_frame_0
09:27:59  
09:27:59  Config=arm64-/Users/ec2-user/jenkins/workspace/llvm.org/as-lldb-cmake/lldb-build/bin/clang
09:27:59  ----------------------------------------------------------------------
09:27:59  Ran 6 tests in 14.242s
09:27:59  
09:27:59  FAILED (failures=6)
```

Reverts llvm#161870
medismailben added a commit to medismailben/llvm-project that referenced this pull request Nov 29, 2025
This patch extends ScriptedFrame to work with real (non-scripted)
threads,
enabling frame providers to synthesize frames for native processes.

Previously, ScriptedFrame only worked within
ScriptedProcess/ScriptedThread
contexts. This patch decouples ScriptedFrame from ScriptedThread,
allowing
users to augment or replace stack frames in real debugging sessions for
use
cases like custom calling conventions, reconstructing corrupted frames
from
core files, or adding diagnostic frames.

Key changes:

- ScriptedFrame::Create() now accepts ThreadSP instead of requiring
ScriptedThread, extracting architecture from the target triple rather
than ScriptedProcess.arch

- Added SBTarget::RegisterScriptedFrameProvider() and
ClearScriptedFrameProvider() APIs, with Target storing a
SyntheticFrameProviderDescriptor template for new threads

- Added "target frame-provider register/clear" commands for CLI access

- Thread class gains LoadScriptedFrameProvider(),
ClearScriptedFrameProvider(),
and GetFrameProvider() methods for per-thread frame provider management

- New SyntheticStackFrameList overrides FetchFramesUpTo() to lazily
provide
frames from either the frame provider or the real stack

This enables practical use of the SyntheticFrameProvider infrastructure
in
real debugging workflows.

rdar://161834688

Signed-off-by: Med Ismail Bennani <[email protected]>

Signed-off-by: Med Ismail Bennani <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants