[Lldb-commits] [lldb] [lldb/Interpreter] Surface Python exceptions from scripted extensions (PR #198153)

via lldb-commits lldb-commits at lists.llvm.org
Mon Jul 27 00:17:35 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-lldb

Author: Med Ismail Bennani (medismailben)

<details>
<summary>Changes</summary>

When calling scripted affordance methods, Python exceptions were silently consumed by llvm::consumeError(), making it very hard to debug scripted extensions: users would see a generic "Failed to create script object." or just a null result, with no clue about what raised in their script.

This patch propagates the actual Python error through the existing error-reporting channel of each call site:

- The Dispatch and CallStaticMethod templates now extract the full Python backtrace via PythonException::ReadBacktrace and populate the Status out-parameter with a message of the form "Python exception in <script-class> method '<method>':\n<traceback>".

- ErrorWithMessage no longer overwrites a Status that already has detailed content, so the traceback survives the helper's existing logging.

- ScriptedInterface now holds the ScriptedMetadata (set by each concrete CreatePluginObject) so the error message can name the script class.

- For entry points that have no return-channel for errors (ScriptedProcess::CreateInstance, ScriptedThreadPlan::DidPush, BreakpointResolverScripted, OperatingSystemPython's constructor) the detailed error is broadcast via Debugger::ReportError so users see it instead of a silent failure.

- For entry points that already return llvm::Expected<T> or Status (ScriptedThread::Create, ScriptedFrameProvider::CreateInstance, StopHookScripted::SetScriptCallback) the existing return type now carries the detailed error, replacing the previous consumeError + generic-message pattern.

- ScriptedProcess::DoAttach / DoReadMemory / DoWriteMemory wrap their affordance Status with the operation that failed.

A new test directory at lldb/test/API/functionalities/scripted_extensions/ listens for eBroadcastBitError diagnostics and asserts that each malformed scripted extension produces a user-visible error.

---

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


17 Files Affected:

- (modified) lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h (+1-1) 
- (modified) lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h (+1-1) 
- (modified) lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h (+25-10) 
- (modified) lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h (+1-1) 
- (modified) lldb/include/lldb/Interpreter/ScriptInterpreter.h (+4) 
- (modified) lldb/source/Breakpoint/BreakpointResolverScripted.cpp (+7-1) 
- (modified) lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp (+7-1) 
- (modified) lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp (+54-6) 
- (modified) lldb/source/Plugins/Process/scripted/ScriptedProcess.h (+12-2) 
- (modified) lldb/source/Plugins/Process/scripted/ScriptedThread.cpp (+2-5) 
- (modified) lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h (+124-8) 
- (modified) lldb/source/Target/ScriptedThreadPlan.cpp (+5) 
- (added) lldb/test/API/functionalities/scripted_extensions/Makefile (+3) 
- (added) lldb/test/API/functionalities/scripted_extensions/TestScriptedExtensionsDiagnostics.py (+124) 
- (added) lldb/test/API/functionalities/scripted_extensions/main.c (+1) 
- (added) lldb/test/API/functionalities/scripted_extensions/malformed_scripted_extensions.py (+195) 
- (modified) lldb/test/API/functionalities/scripted_process/TestScriptedProcess.py (+5-1) 


``````````diff
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
index 982f231d9f0b2..7328aa26db65d 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedBreakpointInterface.h
@@ -15,7 +15,7 @@
 #include "lldb/lldb-private.h"
 
 namespace lldb_private {
-class ScriptedBreakpointInterface : public ScriptedInterface {
+class ScriptedBreakpointInterface : virtual public ScriptedInterface {
 public:
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(const ScriptedMetadata &scripted_metadata,
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
index 757a99cb2387b..8c94df430c103 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedFrameProviderInterface : public ScriptedInterface {
+class ScriptedFrameProviderInterface : virtual public ScriptedInterface {
 public:
   virtual bool AppliesToThread(llvm::StringRef class_name,
                                lldb::ThreadSP thread_sp) {
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
index 3dbc009a58311..4fc1e5860702b 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedInterface.h
@@ -20,6 +20,7 @@
 
 #include "llvm/Support/Compiler.h"
 
+#include <functional>
 #include <optional>
 #include <string>
 
@@ -37,6 +38,17 @@ class ScriptedInterface {
     return m_scripted_metadata;
   }
 
+  /// Set error callback to surface Python exceptions directly to users.
+  ///
+  /// This allows command handlers to receive Python exception details
+  /// immediately rather than relying on diagnostic broadcasts.
+  ///
+  /// \param callback Function to call with Status containing exception details.
+  virtual void SetErrorCallback(std::function<void(const Status &)> callback) {}
+
+  /// Clear the error callback.
+  virtual void ClearErrorCallback() {}
+
   struct AbstractMethodRequirement {
     llvm::StringLiteral name;
     size_t min_arg_count = 0;
@@ -62,18 +74,20 @@ class ScriptedInterface {
   static Ret ErrorWithMessage(llvm::StringRef caller_name,
                               llvm::StringRef error_msg, Status &error,
                               LLDBLog log_category = LLDBLog::Process) {
+    // Log the error for debugging (includes function signature for context).
     LLDB_LOGF(GetLog(log_category), "%s ERROR = %s", caller_name.data(),
               error_msg.data());
-    std::string full_error_message =
-        llvm::Twine(caller_name + llvm::Twine(" ERROR = ") +
-                    llvm::Twine(error_msg))
-            .str();
-    if (const char *detailed_error = error.AsCString())
-      full_error_message +=
-          llvm::Twine(llvm::Twine(" (") + llvm::Twine(detailed_error) +
-                      llvm::Twine(")"))
-              .str();
-    error = Status(std::move(full_error_message));
+
+    // For user-facing messages, just pass through the Status if it already
+    // has detailed information (like Python tracebacks); otherwise set it.
+    llvm::StringRef existing_error = error.AsCString();
+    if (!error.Fail() || existing_error.empty()) {
+      // Status is empty, populate it with the simple error message.
+      error = Status::FromErrorString(error_msg.data());
+    }
+    // If Status already has content, leave it as-is (it has the Python
+    // traceback).
+
     return {};
   }
 
@@ -105,4 +119,5 @@ class ScriptedInterface {
   std::optional<ScriptedMetadata> m_scripted_metadata;
 };
 } // namespace lldb_private
+
 #endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
index c68498cba1632..ba325cfdf38b2 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedStopHookInterface.h
@@ -14,7 +14,7 @@
 #include "ScriptedInterface.h"
 
 namespace lldb_private {
-class ScriptedStopHookInterface : public ScriptedInterface {
+class ScriptedStopHookInterface : virtual public ScriptedInterface {
 public:
   virtual llvm::Expected<StructuredData::GenericSP>
   CreatePluginObject(const ScriptedMetadata &scripted_metadata,
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 58af82fb48390..2cc3890d86916 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -663,6 +663,10 @@ class ScriptInterpreter : public PluginInterface {
 
   lldb::TargetSP GetOpaqueTypeFromSBTarget(const lldb::SBTarget &target) const;
 
+  /// Get the debugger associated with this script interpreter.
+  Debugger &GetDebugger() { return m_debugger; }
+  const Debugger &GetDebugger() const { return m_debugger; }
+
 protected:
   Debugger &m_debugger;
   lldb::ScriptLanguage m_script_lang;
diff --git a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
index 0719c8b634cd3..0fdb185537c9a 100644
--- a/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
+++ b/lldb/source/Breakpoint/BreakpointResolverScripted.cpp
@@ -20,6 +20,7 @@
 #include "lldb/Target/Target.h"
 #include "lldb/Utility/Log.h"
 #include "lldb/Utility/StreamString.h"
+#include "llvm/Support/FormatVariadic.h"
 
 using namespace lldb;
 using namespace lldb_private;
@@ -94,7 +95,12 @@ void BreakpointResolverScripted::CreateImplementationIfNeeded(
       m_interface_sp->CreatePluginObject(scripted_metadata, breakpoint_sp);
   if (!obj_or_err) {
     m_interface_sp.reset();
-    m_error = Status::FromError(obj_or_err.takeError());
+    std::string msg = llvm::toString(obj_or_err.takeError());
+    Debugger::ReportError(
+        llvm::formatv("Failed to create BreakpointResolverScripted: {0}", msg)
+            .str(),
+        target.GetDebugger().GetID());
+    m_error = Status(msg);
     return;
   }
   StructuredData::ObjectSP object_sp = *obj_or_err;
diff --git a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
index 49df19906929f..662b8117041bc 100644
--- a/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
+++ b/lldb/source/Plugins/OperatingSystem/Python/OperatingSystemPython.cpp
@@ -33,6 +33,7 @@
 #include "lldb/Utility/StreamString.h"
 #include "lldb/Utility/StructuredData.h"
 #include "lldb/ValueObject/ValueObjectVariable.h"
+#include "llvm/Support/FormatVariadic.h"
 
 #include <memory>
 
@@ -120,7 +121,12 @@ OperatingSystemPython::OperatingSystemPython(lldb_private::Process *process,
       scripted_metadata, exe_ctx, nullptr);
 
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
+    std::string msg = llvm::toString(obj_or_err.takeError());
+    if (process)
+      Debugger::ReportError(
+          llvm::formatv("Failed to create OperatingSystemPython: {0}", msg)
+              .str(),
+          process->GetTarget().GetDebugger().GetID());
     return;
   }
 
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
index 842eef9524328..b0418307cdebb 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.cpp
@@ -27,6 +27,10 @@
 
 #include "Plugins/ObjectFile/Placeholder/ObjectFilePlaceholder.h"
 
+#include "llvm/Support/Error.h"
+
+#include <string>
+
 using namespace lldb;
 using namespace lldb_private;
 
@@ -57,11 +61,27 @@ lldb::ProcessSP ScriptedProcess::CreateInstance(lldb::TargetSP target_sp,
 
   ScriptedMetadata scripted_metadata(target_sp->GetProcessLaunchInfo());
 
+  // CreateInstance is invoked for every process plugin during process
+  // creation; if the launch info doesn't request a scripted process, bail
+  // out silently rather than treating the missing class name as an error.
+  if (!scripted_metadata)
+    return nullptr;
+
   Status error;
   auto process_sp = std::shared_ptr<ScriptedProcess>(
       new ScriptedProcess(target_sp, listener_sp, scripted_metadata, error));
 
   if (error.Fail() || !process_sp || !process_sp->m_interface_up) {
+    // CreateInstance returns nullptr on failure with no Status output
+    // parameter, so we must report the error via the diagnostic system for
+    // users to see it.
+    if (error.Fail()) {
+      Debugger::ReportError(
+          llvm::formatv("Failed to create ScriptedProcess: {0}",
+                        error.AsCString())
+              .str(),
+          target_sp->GetDebugger().GetID());
+    }
     LLDB_LOGF(GetLog(LLDBLog::Process), "%s", error.AsCString());
     return nullptr;
   }
@@ -112,8 +132,10 @@ ScriptedProcess::ScriptedProcess(lldb::TargetSP target_sp,
       GetInterface().CreatePluginObject(m_scripted_metadata, exe_ctx);
 
   if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
-    error = Status::FromErrorString("Failed to create script object.");
+    // Extract the detailed error message including the Python backtrace.
+    std::string error_msg = llvm::toString(obj_or_err.takeError());
+    error = Status::FromErrorStringWithFormatv(
+        "Failed to create script object: {0}", error_msg);
     return;
   }
 
@@ -189,10 +211,13 @@ Status ScriptedProcess::DoResume(RunDirection direction) {
 
 Status ScriptedProcess::DoAttach(const ProcessAttachInfo &attach_info) {
   Status error = GetInterface().Attach(attach_info);
+  if (error.Fail()) {
+    error = Status::FromErrorStringWithFormatv(
+        "Failed to attach to scripted process: {0}", error.AsCString());
+    return error;
+  }
   SetPrivateState(eStateRunning);
   SetPrivateState(eStateStopped);
-  if (error.Fail())
-    return error;
   // NOTE: We need to set the PID before finishing to attach otherwise we will
   // hit an assert when calling the attach completion handler.
   DidLaunch();
@@ -224,8 +249,14 @@ size_t ScriptedProcess::DoReadMemory(lldb::addr_t addr, void *buf, size_t size,
   lldb::DataExtractorSP data_extractor_sp =
       GetInterface().ReadMemoryAtAddress(addr, size, error);
 
-  if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail())
+  if (!data_extractor_sp || !data_extractor_sp->HasData() || error.Fail()) {
+    if (error.Fail()) {
+      error = Status::FromErrorStringWithFormatv(
+          "Failed to read memory from scripted process at 0x{0:x-}: {1}", addr,
+          error.AsCString());
+    }
     return 0;
+  }
 
   offset_t bytes_copied = data_extractor_sp->CopyByteOrderedData(
       0, data_extractor_sp->GetByteSize(), buf, size, GetByteOrder());
@@ -251,9 +282,15 @@ size_t ScriptedProcess::DoWriteMemory(lldb::addr_t vm_addr, const void *buf,
   lldb::offset_t bytes_written =
       GetInterface().WriteMemoryAtAddress(vm_addr, data_extractor_sp, error);
 
-  if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET)
+  if (!bytes_written || bytes_written == LLDB_INVALID_OFFSET) {
+    if (error.Fail()) {
+      error = Status::FromErrorStringWithFormatv(
+          "Failed to write memory to scripted process at 0x{0:x-}: {1}", vm_addr,
+          error.AsCString());
+    }
     return ScriptedInterface::ErrorWithMessage<size_t>(
         LLVM_PRETTY_FUNCTION, "Failed to copy write buffer to memory.", error);
+  }
 
   // FIXME: We should use the diagnostic system to report a warning if the
   // `bytes_written` is different from `size`.
@@ -566,3 +603,14 @@ void *ScriptedProcess::GetImplementation() {
     return object_instance_sp->GetAsGeneric()->GetValue();
   return nullptr;
 }
+
+void ScriptedProcess::SetScriptedInterfaceErrorCallback(
+    std::function<void(const Status &)> callback) {
+  if (m_interface_up)
+    m_interface_up->SetErrorCallback(std::move(callback));
+}
+
+void ScriptedProcess::ClearScriptedInterfaceErrorCallback() {
+  if (m_interface_up)
+    m_interface_up->ClearErrorCallback();
+}
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
index 8371180734217..9ce4a3f8d3cc9 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedProcess.h
@@ -17,8 +17,6 @@
 
 #include "ScriptedThread.h"
 
-#include <mutex>
-
 namespace lldb_private {
 class ScriptedProcess : public Process {
 public:
@@ -93,6 +91,18 @@ class ScriptedProcess : public Process {
 
   void *GetImplementation() override;
 
+  /// Set error callback to surface Python exceptions directly to users.
+  ///
+  /// This allows command handlers to receive Python exception details
+  /// immediately rather than relying on diagnostic broadcasts.
+  ///
+  /// \param callback Function to call with Status containing exception details.
+  void SetScriptedInterfaceErrorCallback(
+      std::function<void(const Status &)> callback);
+
+  /// Clear the error callback.
+  void ClearScriptedInterfaceErrorCallback();
+
   void ForceScriptedState(lldb::StateType state) override {
     // If we're about to stop, we should fetch the loaded dynamic libraries
     // dictionary before emitting the private stop event to avoid having the
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
index f87b37ca67e09..ac5dd00483f3b 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedThread.cpp
@@ -69,11 +69,8 @@ ScriptedThread::Create(ScriptedProcess &process,
   auto obj_or_err = scripted_thread_interface->CreatePluginObject(
       thread_metadata, exe_ctx, script_object);
 
-  if (!obj_or_err) {
-    llvm::consumeError(obj_or_err.takeError());
-    return llvm::createStringError(llvm::inconvertibleErrorCode(),
-                                   "Failed to create script object.");
-  }
+  if (!obj_or_err)
+    return obj_or_err.takeError();
 
   StructuredData::GenericSP owned_script_object_sp = *obj_or_err;
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 7d0d4cdd3c6d1..5608d5d35d654 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -9,12 +9,14 @@
 #ifndef LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
 #define LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
 
+#include <functional>
 #include <optional>
 #include <sstream>
 #include <tuple>
 #include <type_traits>
 #include <utility>
 
+#include "lldb/Core/Debugger.h"
 #include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
 #include "lldb/Utility/DataBufferHeap.h"
 
@@ -29,6 +31,22 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
   ScriptedPythonInterface(ScriptInterpreterPythonImpl &interpreter);
   ~ScriptedPythonInterface() override = default;
 
+  /// Set callback to surface Python exceptions to CommandReturnObject.
+  ///
+  /// When set, this callback will be invoked whenever a Python exception occurs
+  /// in scripting affordance methods, allowing errors to be surfaced directly
+  /// to the user via CommandReturnObject::AppendError().
+  ///
+  /// If no callback is registered, errors will be reported via
+  /// Debugger::ReportError() instead.
+  ///
+  /// \param callback Function to call with Status containing exception details.
+  using ErrorCallback = std::function<void(const Status &)>;
+  void SetErrorCallback(ErrorCallback callback) override {
+    m_error_callback = std::move(callback);
+  }
+  void ClearErrorCallback() override { m_error_callback = nullptr; }
+
   enum class AbstractMethodCheckerCases {
     eNotImplemented,
     eNotAllocated,
@@ -130,7 +148,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
       llvm::Expected<PythonObject> callable_or_err =
           class_dict.GetItem(method_name);
       if (!callable_or_err) {
-        llvm::consumeError(callable_or_err.takeError());
+        Log *log = GetLog(LLDBLog::Script);
+        if (log) {
+          std::string error_msg =
+              ExtractPythonError(callable_or_err.takeError());
+          LLDB_LOGF(log, "Failed to get method '%s': %s", method_name.data(),
+                    error_msg.c_str());
+        } else {
+          llvm::consumeError(callable_or_err.takeError());
+        }
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eNotAllocated)
       }
@@ -145,7 +171,15 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
       auto arg_info_or_err = callable.GetArgInfo();
       if (!arg_info_or_err) {
-        llvm::consumeError(arg_info_or_err.takeError());
+        Log *log = GetLog(LLDBLog::Script);
+        if (log) {
+          std::string error_msg =
+              ExtractPythonError(arg_info_or_err.takeError());
+          LLDB_LOGF(log, "Failed to get arg info for method '%s': %s",
+                    method_name.data(), error_msg.c_str());
+        } else {
+          llvm::consumeError(arg_info_or_err.takeError());
+        }
         SET_CASE_AND_CONTINUE(method_name,
                               AbstractMethodCheckerCases::eUnknownArgumentCount)
       }
@@ -258,14 +292,18 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
 
         std::apply(
             [&init, &expected_return_object](auto &&...args) {
-              llvm::consumeError(expected_return_object.takeError());
+              // Consume placeholder error (expected initial state).
+              if (!expected_return_object)
+                llvm::consumeError(expected_return_object.takeError());
               expected_return_object = init(args...);
             },
             std::tuple_cat(transformed_args, std::make_tuple(dict)));
       } else {
         std::apply(
             [&init, &expected_return_object](auto &&...args) {
-              llvm::consumeError(expected_return_object.takeError());
+              // Consume placeholder error (expected initial state).
+              if (!expected_return_object)
+                llvm::consumeError(expected_return_object.takeError());
               expected_return_object = init(args...);
             },
             transformed_args);
@@ -467,13 +505,41 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
         llvm::createStringError("not initialized");
     std::apply(
         [&method, &expected_return_object](auto &&...args) {
-          llvm::consumeError(expected_return_object.takeError());
+          // Consume placeholder error (expected initial state).
+          if (!expected_return_object)
+            llvm::consumeError(expected_return_object.takeError());
           expected_return_object = method(args...);
         },
         transformed_args);
 
     if (llvm::Error e = expected_return_object.takeError()) {
-      error = Status::FromError(std::move(e));
+      // Extract Python backtrace and log it.
+      std::string detailed_error = ExtractPythonError(std::move(e));
+
+      Log *log = GetLog(LLDBLog::Script);
+      if (log) {
+        LLDB_LOGF(log, "%s: Python exception in static method %s:\n%s",
+                  caller_signature.c_str(), method_name.data(),
+                  detailed_error.c_str());
+      }
+
+      // Create Status with full context including the interface type.
+      // TODO: Stringify `args` and include them in the message so users
+      // can see what was passed to the failing call (e.g.
+      // `read_memory_at_address(0x500000000, 4)`). Requires a SFINAE
+      // helper that falls back to a placeholder for types without a
+      // format_provider / o...
[truncated]

``````````

</details>


https://github.com/llvm/llvm-project/pull/198153


More information about the lldb-commits mailing list