https://github.com/rchamala updated 
https://github.com/llvm/llvm-project/pull/183678

>From 875a23256fa551b0deca166adb67b43620480acc Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <[email protected]>
Date: Thu, 26 Feb 2026 14:55:33 -0800
Subject: [PATCH 1/5] [lldb] Add LocateSourceFile callback to SymbolLocator
 plugin interface

Add a LocateSourceFile callback slot to the SymbolLocator plugin
interface and wire it into source file resolution via
LineEntry::ApplyFileMappings.

This is NFC for existing plugins: Default and DebugSymbols use the
default nullptr, and Debuginfod passes an explicit nullptr since
DebuggerInitialize follows it in the argument list.

The new callback takes (ModuleSP, FileSpec) with no TargetSP, following
the global plugin model. When a SymbolLocator plugin provides a
LocateSourceFile implementation, it is called before the existing
target.source-map resolution path in ApplyFileMappings.

The ApplyFileMappings signature gains a `const Address &address`
parameter so the implementation can extract the ModuleSP from the
address. All four call sites (Module.cpp, StackFrame.cpp,
StackFrameList.cpp, ThreadPlanStepRange.cpp) are updated accordingly.
---
 lldb/include/lldb/Core/PluginManager.h        |  4 ++++
 lldb/include/lldb/Symbol/LineEntry.h          |  2 +-
 lldb/include/lldb/lldb-private-interfaces.h   |  2 ++
 lldb/source/Core/Module.cpp                   |  2 +-
 lldb/source/Core/PluginManager.cpp            | 22 +++++++++++++++++--
 .../Debuginfod/SymbolLocatorDebuginfod.cpp    |  2 +-
 lldb/source/Symbol/LineEntry.cpp              | 17 +++++++++++++-
 lldb/source/Target/StackFrame.cpp             |  3 ++-
 lldb/source/Target/StackFrameList.cpp         |  3 ++-
 lldb/source/Target/ThreadPlanStepRange.cpp    |  3 ++-
 10 files changed, 51 insertions(+), 9 deletions(-)

diff --git a/lldb/include/lldb/Core/PluginManager.h 
b/lldb/include/lldb/Core/PluginManager.h
index 4d116f52460ff..3fd3e6177afa0 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -455,6 +455,7 @@ class PluginManager {
       SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file =
           nullptr,
       SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle = nullptr,
+      SymbolLocatorLocateSourceFile locate_source_file = nullptr,
       DebuggerInitializeCallback debugger_init_callback = nullptr);
 
   static bool UnregisterPlugin(SymbolLocatorCreateInstance create_callback);
@@ -479,6 +480,9 @@ class PluginManager {
                                          const UUID *uuid,
                                          const ArchSpec *arch);
 
+  static FileSpec LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                   const FileSpec &original_source_file);
+
   // Trace
   static bool RegisterPlugin(
       llvm::StringRef name, llvm::StringRef description,
diff --git a/lldb/include/lldb/Symbol/LineEntry.h 
b/lldb/include/lldb/Symbol/LineEntry.h
index adf2e989e3e34..f0554805e2b74 100644
--- a/lldb/include/lldb/Symbol/LineEntry.h
+++ b/lldb/include/lldb/Symbol/LineEntry.h
@@ -128,7 +128,7 @@ struct LineEntry {
   ///
   /// \param[in] target_sp
   ///     Shared pointer to the target this LineEntry belongs to.
-  void ApplyFileMappings(lldb::TargetSP target_sp);
+  void ApplyFileMappings(lldb::TargetSP target_sp, lldb::ModuleSP module_sp);
 
   /// Helper to access the file.
   const FileSpec &GetFile() const { return file_sp->GetSpecOnly(); }
diff --git a/lldb/include/lldb/lldb-private-interfaces.h 
b/lldb/include/lldb/lldb-private-interfaces.h
index a87e01769c555..6d71b8d671b71 100644
--- a/lldb/include/lldb/lldb-private-interfaces.h
+++ b/lldb/include/lldb/lldb-private-interfaces.h
@@ -110,6 +110,8 @@ typedef std::optional<FileSpec> 
(*SymbolLocatorLocateExecutableSymbolFile)(
 typedef bool (*SymbolLocatorDownloadObjectAndSymbolFile)(
     ModuleSpec &module_spec, Status &error, bool force_lookup,
     bool copy_executable);
+typedef std::optional<FileSpec> (*SymbolLocatorLocateSourceFile)(
+    const lldb::ModuleSP &module_sp, const FileSpec &original_source_file);
 using BreakpointHitCallback =
     std::function<bool(void *baton, StoppointCallbackContext *context,
                        lldb::user_id_t break_id, lldb::user_id_t 
break_loc_id)>;
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 659190833c20d..ccff98e6fcd19 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -476,7 +476,7 @@ uint32_t Module::ResolveSymbolContextForAddress(
           symfile->ResolveSymbolContext(so_addr, resolve_scope, sc);
 
       if ((resolve_scope & eSymbolContextLineEntry) && sc.line_entry.IsValid())
-        sc.line_entry.ApplyFileMappings(sc.target_sp);
+        sc.line_entry.ApplyFileMappings(sc.target_sp, so_addr.GetModule());
     }
 
     // Resolve the symbol if requested, but don't re-look it up if we've
diff --git a/lldb/source/Core/PluginManager.cpp 
b/lldb/source/Core/PluginManager.cpp
index 64130d700a006..5b8bcc7cc68ef 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -1476,18 +1476,21 @@ struct SymbolLocatorInstance
       SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file,
       SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file,
       SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle,
+      SymbolLocatorLocateSourceFile locate_source_file,
       DebuggerInitializeCallback debugger_init_callback)
       : PluginInstance<SymbolLocatorCreateInstance>(
             name, description, create_callback, debugger_init_callback),
         locate_executable_object_file(locate_executable_object_file),
         locate_executable_symbol_file(locate_executable_symbol_file),
         download_object_symbol_file(download_object_symbol_file),
-        find_symbol_file_in_bundle(find_symbol_file_in_bundle) {}
+        find_symbol_file_in_bundle(find_symbol_file_in_bundle),
+        locate_source_file(locate_source_file) {}
 
   SymbolLocatorLocateExecutableObjectFile locate_executable_object_file;
   SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file;
   SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file;
   SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle;
+  SymbolLocatorLocateSourceFile locate_source_file;
 };
 typedef PluginInstances<SymbolLocatorInstance> SymbolLocatorInstances;
 
@@ -1503,11 +1506,12 @@ bool PluginManager::RegisterPlugin(
     SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file,
     SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file,
     SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle,
+    SymbolLocatorLocateSourceFile locate_source_file,
     DebuggerInitializeCallback debugger_init_callback) {
   return GetSymbolLocatorInstances().RegisterPlugin(
       name, description, create_callback, locate_executable_object_file,
       locate_executable_symbol_file, download_object_symbol_file,
-      find_symbol_file_in_bundle, debugger_init_callback);
+      find_symbol_file_in_bundle, locate_source_file, debugger_init_callback);
 }
 
 bool PluginManager::UnregisterPlugin(
@@ -1591,6 +1595,20 @@ FileSpec PluginManager::FindSymbolFileInBundle(const 
FileSpec &symfile_bundle,
   return {};
 }
 
+FileSpec PluginManager::LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                         const FileSpec &original_source_file) 
{
+  auto instances = GetSymbolLocatorInstances().GetSnapshot();
+  for (auto &instance : instances) {
+    if (instance.locate_source_file) {
+      std::optional<FileSpec> result =
+          instance.locate_source_file(module_sp, original_source_file);
+      if (result)
+        return *result;
+    }
+  }
+  return {};
+}
+
 #pragma mark Trace
 
 struct TraceInstance
diff --git 
a/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp 
b/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
index a09bb356e3a8c..bdef57f0671e1 100644
--- a/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
+++ b/lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
@@ -111,7 +111,7 @@ void SymbolLocatorDebuginfod::Initialize() {
     PluginManager::RegisterPlugin(
         GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
         LocateExecutableObjectFile, LocateExecutableSymbolFile, nullptr,
-        nullptr, SymbolLocatorDebuginfod::DebuggerInitialize);
+        nullptr, nullptr, SymbolLocatorDebuginfod::DebuggerInitialize);
     llvm::HTTPClient::initialize();
   });
 }
diff --git a/lldb/source/Symbol/LineEntry.cpp b/lldb/source/Symbol/LineEntry.cpp
index dcfbac8789863..4e467755ff2b2 100644
--- a/lldb/source/Symbol/LineEntry.cpp
+++ b/lldb/source/Symbol/LineEntry.cpp
@@ -7,6 +7,8 @@
 
//===----------------------------------------------------------------------===//
 
 #include "lldb/Symbol/LineEntry.h"
+#include "lldb/Core/Address.h"
+#include "lldb/Core/PluginManager.h"
 #include "lldb/Symbol/CompileUnit.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/Target.h"
@@ -242,8 +244,21 @@ AddressRange LineEntry::GetSameLineContiguousAddressRange(
   return complete_line_range;
 }
 
-void LineEntry::ApplyFileMappings(lldb::TargetSP target_sp) {
+void LineEntry::ApplyFileMappings(lldb::TargetSP target_sp,
+                                  lldb::ModuleSP module_sp) {
   if (target_sp) {
+    // Try SymbolLocator plugins for source file resolution. The plugin
+    // handles the fast path internally (returns immediately when no
+    // scripted locators are registered).
+    if (module_sp) {
+      FileSpec resolved = PluginManager::LocateSourceFile(
+          module_sp, original_file_sp->GetSpecOnly());
+      if (resolved) {
+        file_sp = std::make_shared<SupportFile>(resolved);
+        return;
+      }
+    }
+
     // Apply any file remappings to our file.
     if (auto new_file_spec = target_sp->GetSourcePathMap().FindFile(
             original_file_sp->GetSpecOnly())) {
diff --git a/lldb/source/Target/StackFrame.cpp 
b/lldb/source/Target/StackFrame.cpp
index 9fb26176e43c0..2b97443a6a0fd 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -412,7 +412,8 @@ StackFrame::GetSymbolContext(SymbolContextItem 
resolve_scope) {
         if ((resolved & eSymbolContextLineEntry) &&
             !m_sc.line_entry.IsValid()) {
           m_sc.line_entry = sc.line_entry;
-          m_sc.line_entry.ApplyFileMappings(m_sc.target_sp);
+          m_sc.line_entry.ApplyFileMappings(m_sc.target_sp,
+                                            lookup_addr.GetModule());
         }
       }
     } else {
diff --git a/lldb/source/Target/StackFrameList.cpp 
b/lldb/source/Target/StackFrameList.cpp
index 825d538e3815b..183a0b3e474ac 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -563,7 +563,8 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
 
       while (unwind_sc.GetParentOfInlinedScope(
           curr_frame_address, next_frame_sc, next_frame_address)) {
-        next_frame_sc.line_entry.ApplyFileMappings(target_sp);
+        next_frame_sc.line_entry.ApplyFileMappings(
+            target_sp, curr_frame_address.GetModule());
         behaves_like_zeroth_frame = false;
         StackFrameSP frame_sp(new StackFrame(
             m_thread.shared_from_this(), m_frames.size(), idx,
diff --git a/lldb/source/Target/ThreadPlanStepRange.cpp 
b/lldb/source/Target/ThreadPlanStepRange.cpp
index 3a9deb6f5c6fd..c1d1dbe5762c5 100644
--- a/lldb/source/Target/ThreadPlanStepRange.cpp
+++ b/lldb/source/Target/ThreadPlanStepRange.cpp
@@ -433,7 +433,8 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
             top_most_line_entry.range = range;
             top_most_line_entry.file_sp = std::make_shared<SupportFile>();
             top_most_line_entry.ApplyFileMappings(
-                GetThread().CalculateTarget());
+                GetThread().CalculateTarget(),
+                range.GetBaseAddress().GetModule());
             if (!top_most_line_entry.file_sp->GetSpecOnly())
               top_most_line_entry.file_sp =
                   top_most_line_entry.original_file_sp;

>From 9131dbe5b99389f763e763bbb42cd430cbc958c3 Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <[email protected]>
Date: Thu, 26 Feb 2026 14:59:08 -0800
Subject: [PATCH 2/5] [lldb] Add ScriptedSymbolLocator Python interface bridge

Add the abstract interface, Python bridge, SWIG bindings, and all
plumbing needed for Python classes to implement symbol locator callbacks.

New files:
- ScriptedSymbolLocatorInterface.h: abstract base with all 4 callback
  virtual methods (LocateSourceFile, LocateExecutableObjectFile,
  LocateExecutableSymbolFile, DownloadObjectAndSymbolFile)
- ScriptedSymbolLocatorPythonInterface.h/.cpp: Python interface class
  using Dispatch<FileSpec> for locate_source_file; the remaining 3
  callbacks are no-op stubs pending SBModuleSpec SWIG bridge

Modified files wire up the new interface:
- lldb-forward.h: forward decl + ScriptedSymbolLocatorInterfaceSP typedef
- SBFileSpec.h, SBModule.h: friend class ScriptInterpreter
- ScriptInterpreter.h/.cpp: CreateScriptedSymbolLocatorInterface() virtual,
  GetOpaqueTypeFromSBFileSpec/SBModule helpers
- ScriptedPythonInterface.h/.cpp: Transform(ModuleSP), Transform(string),
  ExtractValueFromPythonObject<FileSpec/ModuleSP> specializations
- SWIGPythonBridge.h + python-wrapper.swig: CastPyObjectToSBFileSpec/SBModule
- CMakeLists, Interfaces registration, ScriptInterpreterPython overrides
- PythonTestSuite.cpp: test stubs for new cast functions
---
 lldb/bindings/python/python-swigsafecast.swig |   5 +
 lldb/bindings/python/python-wrapper.swig      |  36 ++++++
 lldb/include/lldb/API/SBFileSpec.h            |   5 +
 lldb/include/lldb/API/SBModule.h              |   1 +
 lldb/include/lldb/API/SBModuleSpec.h          |   9 ++
 .../ScriptedSymbolLocatorInterface.h          |  52 +++++++++
 .../lldb/Interpreter/ScriptInterpreter.h      |  13 +++
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/source/Interpreter/ScriptInterpreter.cpp |  21 ++++
 .../Python/Interfaces/CMakeLists.txt          |   1 +
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../Interfaces/ScriptedPythonInterface.cpp    |  54 +++++++++
 .../Interfaces/ScriptedPythonInterface.h      |  30 +++++
 .../ScriptedSymbolLocatorPythonInterface.cpp  | 109 ++++++++++++++++++
 .../ScriptedSymbolLocatorPythonInterface.h    |  62 ++++++++++
 .../Python/SWIGPythonBridge.h                 |   5 +
 .../Python/ScriptInterpreterPython.cpp        |   5 +
 .../Python/ScriptInterpreterPythonImpl.h      |   3 +
 .../Python/PythonTestSuite.cpp                |  20 ++++
 20 files changed, 437 insertions(+)
 create mode 100644 
lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
 create mode 100644 
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
 create mode 100644 
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h

diff --git a/lldb/bindings/python/python-swigsafecast.swig 
b/lldb/bindings/python/python-swigsafecast.swig
index a86dc44ce4106..fdf8de3d28aa2 100644
--- a/lldb/bindings/python/python-swigsafecast.swig
+++ b/lldb/bindings/python/python-swigsafecast.swig
@@ -147,6 +147,11 @@ PythonObject SWIGBridge::ToSWIGWrapper(
   return ToSWIGHelper(module_spec_sb.release(), SWIGTYPE_p_lldb__SBModuleSpec);
 }
 
+PythonObject SWIGBridge::ToSWIGWrapper(const ModuleSpec &module_spec) {
+  return ToSWIGHelper(new lldb::SBModuleSpec(module_spec),
+                      SWIGTYPE_p_lldb__SBModuleSpec);
+}
+
 PythonObject SWIGBridge::ToSWIGWrapper(lldb::DescriptionLevel level) {
   return PythonInteger((int64_t) level);
 }
diff --git a/lldb/bindings/python/python-wrapper.swig 
b/lldb/bindings/python/python-wrapper.swig
index bf59569920470..c881cf9a5286e 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -595,6 +595,42 @@ void 
*lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *d
   return sb_ptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject 
*data) {
+  lldb::SBFileSpec *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBFileSpec, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModule(PyObject 
*data) {
+  lldb::SBModule *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBModule, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModuleSpec(PyObject 
*data) {
+  lldb::SBModuleSpec *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBModuleSpec, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
 bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
     const char *python_function_name, const char *session_dictionary_name,
     lldb::DebuggerSP debugger, const char *args,
diff --git a/lldb/include/lldb/API/SBFileSpec.h 
b/lldb/include/lldb/API/SBFileSpec.h
index 36641843aabeb..4b0b640dd4dbc 100644
--- a/lldb/include/lldb/API/SBFileSpec.h
+++ b/lldb/include/lldb/API/SBFileSpec.h
@@ -11,6 +11,10 @@
 
 #include "lldb/API/SBDefines.h"
 
+namespace lldb_private {
+class ScriptInterpreter;
+}
+
 namespace lldb {
 
 class LLDB_API SBFileSpec {
@@ -79,6 +83,7 @@ class LLDB_API SBFileSpec {
   friend class SBThread;
   friend class SBTrace;
   friend class SBSaveCoreOptions;
+  friend class lldb_private::ScriptInterpreter;
 
   SBFileSpec(const lldb_private::FileSpec &fspec);
 
diff --git a/lldb/include/lldb/API/SBModule.h b/lldb/include/lldb/API/SBModule.h
index 4009ca1461e51..3e8e5b99f6404 100644
--- a/lldb/include/lldb/API/SBModule.h
+++ b/lldb/include/lldb/API/SBModule.h
@@ -311,6 +311,7 @@ class LLDB_API SBModule {
   friend class SBType;
 
   friend class lldb_private::python::SWIGBridge;
+  friend class lldb_private::ScriptInterpreter;
 
   explicit SBModule(const lldb::ModuleSP &module_sp);
 
diff --git a/lldb/include/lldb/API/SBModuleSpec.h 
b/lldb/include/lldb/API/SBModuleSpec.h
index 0e7f0f3489596..d1252cb8e9c8e 100644
--- a/lldb/include/lldb/API/SBModuleSpec.h
+++ b/lldb/include/lldb/API/SBModuleSpec.h
@@ -12,6 +12,13 @@
 #include "lldb/API/SBDefines.h"
 #include "lldb/API/SBFileSpec.h"
 
+namespace lldb_private {
+class ScriptInterpreter;
+namespace python {
+class SWIGBridge;
+} // namespace python
+} // namespace lldb_private
+
 namespace lldb {
 
 class LLDB_API SBModuleSpec {
@@ -102,6 +109,8 @@ class LLDB_API SBModuleSpec {
   friend class SBModule;
   friend class SBPlatform;
   friend class SBTarget;
+  friend class lldb_private::ScriptInterpreter;
+  friend class lldb_private::python::SWIGBridge;
 
   SBModuleSpec(const lldb_private::ModuleSpec &module_spec);
 
diff --git 
a/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h 
b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
new file mode 100644
index 0000000000000..b81c73698d918
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
@@ -0,0 +1,52 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_SCRIPTEDSYMBOLLOCATORINTERFACE_H
+#define LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYMBOLLOCATORINTERFACE_H
+
+#include "lldb/Core/ModuleSpec.h"
+#include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
+#include "lldb/Utility/Status.h"
+
+#include "lldb/lldb-private.h"
+
+#include <optional>
+#include <string>
+
+namespace lldb_private {
+class ScriptedSymbolLocatorInterface : virtual public ScriptedInterface {
+public:
+  virtual llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, ExecutionContext &exe_ctx,
+                     StructuredData::DictionarySP args_sp,
+                     StructuredData::Generic *script_obj = nullptr) = 0;
+
+  virtual std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file, Status &error) {
+    return {};
+  }
+
+  virtual std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec, Status &error) {
+    return {};
+  }
+
+  virtual std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec, Status &error) {
+    return {};
+  }
+
+  virtual bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                           Status &error) {
+    return false;
+  }
+};
+} // namespace lldb_private
+
+#endif // LLDB_INTERPRETER_INTERFACES_SCRIPTEDSYMBOLLOCATORINTERFACE_H
diff --git a/lldb/include/lldb/Interpreter/ScriptInterpreter.h 
b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
index 557d73a415452..581b6dcdfab1d 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -33,6 +33,7 @@
 #include "lldb/Interpreter/Interfaces/ScriptedFrameProviderInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedPlatformInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedProcessInterface.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
 #include "lldb/Interpreter/ScriptObject.h"
 #include "lldb/Symbol/SymbolContext.h"
@@ -567,6 +568,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() {
+    return {};
+  }
+
   virtual StructuredData::ObjectSP
   CreateStructuredDataFromScriptObject(ScriptObject obj) {
     return {};
@@ -612,6 +618,13 @@ class ScriptInterpreter : public PluginInterface {
   lldb::ValueObjectSP
   GetOpaqueTypeFromSBValue(const lldb::SBValue &value) const;
 
+  FileSpec GetOpaqueTypeFromSBFileSpec(const lldb::SBFileSpec &file_spec) 
const;
+
+  lldb::ModuleSP GetOpaqueTypeFromSBModule(const lldb::SBModule &module) const;
+
+  ModuleSpec
+  GetOpaqueTypeFromSBModuleSpec(const lldb::SBModuleSpec &module_spec) const;
+
 protected:
   Debugger &m_debugger;
   lldb::ScriptLanguage m_script_lang;
diff --git a/lldb/include/lldb/lldb-forward.h b/lldb/include/lldb/lldb-forward.h
index ccfe5efa19e1d..c0f65b09616a3 100644
--- a/lldb/include/lldb/lldb-forward.h
+++ b/lldb/include/lldb/lldb-forward.h
@@ -196,6 +196,7 @@ class ScriptedProcessInterface;
 class ScriptedStopHookInterface;
 class ScriptedThreadInterface;
 class ScriptedThreadPlanInterface;
+class ScriptedSymbolLocatorInterface;
 class ScriptedSyntheticChildren;
 class SearchFilter;
 class Section;
@@ -431,6 +432,8 @@ typedef 
std::shared_ptr<lldb_private::ScriptedThreadPlanInterface>
     ScriptedThreadPlanInterfaceSP;
 typedef std::shared_ptr<lldb_private::ScriptedBreakpointInterface>
     ScriptedBreakpointInterfaceSP;
+typedef std::shared_ptr<lldb_private::ScriptedSymbolLocatorInterface>
+    ScriptedSymbolLocatorInterfaceSP;
 typedef std::shared_ptr<lldb_private::Section> SectionSP;
 typedef std::unique_ptr<lldb_private::SectionList> SectionListUP;
 typedef std::weak_ptr<lldb_private::Section> SectionWP;
diff --git a/lldb/source/Interpreter/ScriptInterpreter.cpp 
b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 5e8478c2670bb..4984e1eaf7995 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -7,7 +7,11 @@
 
//===----------------------------------------------------------------------===//
 
 #include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/API/SBFileSpec.h"
+#include "lldb/API/SBModule.h"
+#include "lldb/API/SBModuleSpec.h"
 #include "lldb/Core/Debugger.h"
+#include "lldb/Core/ModuleSpec.h"
 #include "lldb/Host/ConnectionFileDescriptor.h"
 #include "lldb/Host/Pipe.h"
 #include "lldb/Host/PseudoTerminal.h"
@@ -172,6 +176,23 @@ ScriptInterpreter::GetOpaqueTypeFromSBValue(const 
lldb::SBValue &value) const {
   return locker.GetLockedSP(*value.m_opaque_sp);
 }
 
+FileSpec ScriptInterpreter::GetOpaqueTypeFromSBFileSpec(
+    const lldb::SBFileSpec &file_spec) const {
+  return file_spec.ref();
+}
+
+lldb::ModuleSP ScriptInterpreter::GetOpaqueTypeFromSBModule(
+    const lldb::SBModule &module) const {
+  return module.GetSP();
+}
+
+ModuleSpec ScriptInterpreter::GetOpaqueTypeFromSBModuleSpec(
+    const lldb::SBModuleSpec &module_spec) const {
+  if (module_spec.m_opaque_up)
+    return *module_spec.m_opaque_up;
+  return {};
+}
+
 lldb::ScriptLanguage
 ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) {
   if (language.equals_insensitive(LanguageToString(eScriptLanguageNone)))
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
index 50569cdefaafa..026111d92354e 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
@@ -29,6 +29,7 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces 
PLUGIN
   ScriptedPythonInterface.cpp
   ScriptedStopHookPythonInterface.cpp
   ScriptedBreakpointPythonInterface.cpp
+  ScriptedSymbolLocatorPythonInterface.cpp
   ScriptedThreadPlanPythonInterface.cpp
   ScriptedThreadPythonInterface.cpp
 
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index 5fbccb67fe173..51c2566513c98 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -27,6 +27,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedProcessPythonInterface::Initialize();
   ScriptedStopHookPythonInterface::Initialize();
   ScriptedBreakpointPythonInterface::Initialize();
+  ScriptedSymbolLocatorPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
   ScriptedFrameProviderPythonInterface::Initialize();
 }
@@ -37,6 +38,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedProcessPythonInterface::Terminate();
   ScriptedStopHookPythonInterface::Terminate();
   ScriptedBreakpointPythonInterface::Terminate();
+  ScriptedSymbolLocatorPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
   ScriptedFrameProviderPythonInterface::Terminate();
 }
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 58b19760cb8e1..0eec005b87b21 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -19,6 +19,7 @@
 #include "ScriptedPlatformPythonInterface.h"
 #include "ScriptedProcessPythonInterface.h"
 #include "ScriptedStopHookPythonInterface.h"
+#include "ScriptedSymbolLocatorPythonInterface.h"
 #include "ScriptedThreadPlanPythonInterface.h"
 
 namespace lldb_private {
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
index 68aea7d223f7b..a30c197e80586 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -15,6 +15,7 @@
 
 #include "../ScriptInterpreterPythonImpl.h"
 #include "ScriptedPythonInterface.h"
+#include "lldb/Core/ModuleSpec.h"
 #include "lldb/Symbol/SymbolContext.h"
 #include "lldb/ValueObject/ValueObjectList.h"
 #include <optional>
@@ -308,3 +309,56 @@ 
ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
 
   return out;
 }
+
+template <>
+FileSpec ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpec>(
+    python::PythonObject &p, Status &error) {
+  if (!p.IsAllocated())
+    return {};
+
+  void *sb_file_spec_ptr =
+      python::LLDBSWIGPython_CastPyObjectToSBFileSpec(p.get());
+  if (!sb_file_spec_ptr) {
+    error = Status::FromErrorString("Couldn't cast to SBFileSpec");
+    return {};
+  }
+
+  auto *sb_file_spec = reinterpret_cast<lldb::SBFileSpec *>(sb_file_spec_ptr);
+  FileSpec result = m_interpreter.GetOpaqueTypeFromSBFileSpec(*sb_file_spec);
+  return result;
+}
+
+template <>
+lldb::ModuleSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ModuleSP>(
+    python::PythonObject &p, Status &error) {
+  if (!p.IsAllocated())
+    return {};
+
+  void *sb_module_ptr = python::LLDBSWIGPython_CastPyObjectToSBModule(p.get());
+  if (!sb_module_ptr) {
+    error = Status::FromErrorString("Couldn't cast to SBModule");
+    return {};
+  }
+
+  auto *sb_module = reinterpret_cast<lldb::SBModule *>(sb_module_ptr);
+  return m_interpreter.GetOpaqueTypeFromSBModule(*sb_module);
+}
+
+template <>
+ModuleSpec ScriptedPythonInterface::ExtractValueFromPythonObject<ModuleSpec>(
+    python::PythonObject &p, Status &error) {
+  if (!p.IsAllocated())
+    return {};
+
+  void *sb_module_spec_ptr =
+      python::LLDBSWIGPython_CastPyObjectToSBModuleSpec(p.get());
+  if (!sb_module_spec_ptr) {
+    error = Status::FromErrorString("Couldn't cast to SBModuleSpec");
+    return {};
+  }
+
+  auto *sb_module_spec =
+      reinterpret_cast<lldb::SBModuleSpec *>(sb_module_spec_ptr);
+  return m_interpreter.GetOpaqueTypeFromSBModuleSpec(*sb_module_spec);
+}
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 055d4dc8707d6..fcaae49d459de 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -657,6 +657,18 @@ class ScriptedPythonInterface : virtual public 
ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(lldb::ModuleSP arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
+  python::PythonObject Transform(const ModuleSpec &arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
+  python::PythonObject Transform(const std::string &arg) {
+    return python::PythonString(arg);
+  }
+
   template <typename T, typename U>
   void ReverseTransform(T &original_arg, U transformed_arg, Status &error) {
     // If U is not a PythonObject, don't touch it!
@@ -668,6 +680,11 @@ class ScriptedPythonInterface : virtual public 
ScriptedInterface {
     original_arg = ExtractValueFromPythonObject<T>(transformed_arg, error);
   }
 
+  void ReverseTransform(std::string &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {
+    // No-op: string arguments are input-only, no reverse transformation 
needed.
+  }
+
   void ReverseTransform(bool &original_arg,
                         python::PythonObject transformed_arg, Status &error) {
     python::PythonBoolean boolean_arg = python::PythonBoolean(
@@ -825,6 +842,19 @@ lldb::ValueObjectListSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
     python::PythonObject &p, Status &error);
 
+template <>
+FileSpec ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpec>(
+    python::PythonObject &p, Status &error);
+
+template <>
+lldb::ModuleSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ModuleSP>(
+    python::PythonObject &p, Status &error);
+
+template <>
+ModuleSpec ScriptedPythonInterface::ExtractValueFromPythonObject<ModuleSpec>(
+    python::PythonObject &p, Status &error);
+
 } // namespace lldb_private
 
 #endif // 
LLDB_SOURCE_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDPYTHONINTERFACE_H
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
new file mode 100644
index 0000000000000..87bc3add76278
--- /dev/null
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
@@ -0,0 +1,109 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Core/ModuleSpec.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+// clang-format off
+// LLDB Python header must be included first
+#include "../lldb-python.h"
+//clang-format on
+
+#include "../SWIGPythonBridge.h"
+#include "../ScriptInterpreterPythonImpl.h"
+#include "ScriptedSymbolLocatorPythonInterface.h"
+
+using namespace lldb;
+using namespace lldb_private;
+using namespace lldb_private::python;
+
+ScriptedSymbolLocatorPythonInterface::ScriptedSymbolLocatorPythonInterface(
+    ScriptInterpreterPythonImpl &interpreter)
+    : ScriptedSymbolLocatorInterface(), ScriptedPythonInterface(interpreter) {}
+
+llvm::Expected<StructuredData::GenericSP>
+ScriptedSymbolLocatorPythonInterface::CreatePluginObject(
+    const llvm::StringRef class_name, ExecutionContext &exe_ctx,
+    StructuredData::DictionarySP args_sp, StructuredData::Generic *script_obj) 
{
+  ExecutionContextRefSP exe_ctx_ref_sp =
+      std::make_shared<ExecutionContextRef>(exe_ctx);
+  StructuredDataImpl sd_impl(args_sp);
+  return ScriptedPythonInterface::CreatePluginObject(class_name, script_obj,
+                                                     exe_ctx_ref_sp, sd_impl);
+}
+
+std::optional<FileSpec> ScriptedSymbolLocatorPythonInterface::LocateSourceFile(
+    const lldb::ModuleSP &module_sp, const FileSpec &original_source_file,
+    Status &error) {
+  std::string source_path = original_source_file.GetPath();
+  lldb::ModuleSP module_copy(module_sp);
+
+  FileSpec file_spec =
+      Dispatch<FileSpec>("locate_source_file", error, module_copy, 
source_path);
+
+  if (error.Fail() || !file_spec)
+    return {};
+
+  return file_spec;
+}
+
+std::optional<ModuleSpec>
+ScriptedSymbolLocatorPythonInterface::LocateExecutableObjectFile(
+    const ModuleSpec &module_spec, Status &error) {
+  ModuleSpec module_spec_copy(module_spec);
+  ModuleSpec result = Dispatch<ModuleSpec>(
+      "locate_executable_object_file", error, module_spec_copy);
+
+  if (error.Fail() || !result.GetFileSpec())
+    return {};
+
+  return result;
+}
+
+std::optional<FileSpec>
+ScriptedSymbolLocatorPythonInterface::LocateExecutableSymbolFile(
+    const ModuleSpec &module_spec, Status &error) {
+  ModuleSpec module_spec_copy(module_spec);
+  FileSpec file_spec = Dispatch<FileSpec>("locate_executable_symbol_file",
+                                          error, module_spec_copy);
+
+  if (error.Fail() || !file_spec)
+    return {};
+
+  return file_spec;
+}
+
+bool ScriptedSymbolLocatorPythonInterface::DownloadObjectAndSymbolFile(
+    ModuleSpec &module_spec, Status &error) {
+  StructuredData::ObjectSP obj =
+      Dispatch("download_object_and_symbol_file", error, module_spec);
+
+  if (error.Fail() || !obj || !obj->IsValid())
+    return false;
+
+  return obj->GetBooleanValue();
+}
+
+void ScriptedSymbolLocatorPythonInterface::Initialize() {
+  const std::vector<llvm::StringRef> ci_usages = {
+      "target symbols scripted register -C "
+      "<script-class> [-k <key> -v <value> ...]"};
+  const std::vector<llvm::StringRef> api_usages = {
+      "SBDebugger.RegisterScriptedSymbolLocator(class_name, args_dict)"};
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(),
+      llvm::StringRef("Scripted symbol locator Python interface"),
+      CreateInstance, eScriptLanguagePython, {ci_usages, api_usages});
+}
+
+void ScriptedSymbolLocatorPythonInterface::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
new file mode 100644
index 0000000000000..f806dddf1d5a7
--- /dev/null
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
@@ -0,0 +1,62 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYMBOLLOCATORPYTHONINTERFACE_H
+#define 
LLDB_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYMBOLLOCATORPYTHONINTERFACE_H
+
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
+
+#include "ScriptedPythonInterface.h"
+
+namespace lldb_private {
+class ScriptedSymbolLocatorPythonInterface
+    : public ScriptedSymbolLocatorInterface,
+      public ScriptedPythonInterface,
+      public PluginInterface {
+public:
+  ScriptedSymbolLocatorPythonInterface(
+      ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(llvm::StringRef class_name, ExecutionContext &exe_ctx,
+                     StructuredData::DictionarySP args_sp,
+                     StructuredData::Generic *script_obj = nullptr) override;
+
+  llvm::SmallVector<AbstractMethodRequirement>
+  GetAbstractMethodRequirements() const override {
+    return llvm::SmallVector<AbstractMethodRequirement>(
+        {{"locate_source_file", 2}});
+  }
+
+  std::optional<FileSpec> LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                           const FileSpec 
&original_source_file,
+                                           Status &error) override;
+
+  std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec,
+                             Status &error) override;
+
+  std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             Status &error) override;
+
+  bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                   Status &error) override;
+
+  static void Initialize();
+  static void Terminate();
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "ScriptedSymbolLocatorPythonInterface";
+  }
+
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+} // namespace lldb_private
+
+#endif // 
LLDB_PLUGINS_SCRIPTINTERPRETER_PYTHON_INTERFACES_SCRIPTEDSYMBOLLOCATORPYTHONINTERFACE_H
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h 
b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
index 16ad895ee9f26..684b7e3cade60 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -112,6 +112,8 @@ class SWIGBridge {
   ToSWIGWrapper(std::unique_ptr<lldb::SBFileSpec> file_spec_sb);
   static PythonObject
   ToSWIGWrapper(std::unique_ptr<lldb::SBModuleSpec> module_spec_sb);
+  static PythonObject
+  ToSWIGWrapper(const lldb_private::ModuleSpec &module_spec);
 
   static python::ScopedPythonObject<lldb::SBCommandReturnObject>
   ToSWIGWrapper(CommandReturnObject &cmd_retobj);
@@ -269,6 +271,9 @@ void *LLDBSWIGPython_CastPyObjectToSBValueList(PyObject 
*data);
 void *LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBModule(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBModuleSpec(PyObject *data);
 } // namespace python
 
 } // namespace lldb_private
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp 
b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 3daa24472864e..f11df7c97fc75 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1513,6 +1513,11 @@ 
ScriptInterpreterPythonImpl::CreateScriptedBreakpointInterface() {
   return std::make_shared<ScriptedBreakpointPythonInterface>(*this);
 }
 
+ScriptedSymbolLocatorInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedSymbolLocatorInterface() {
+  return std::make_shared<ScriptedSymbolLocatorPythonInterface>(*this);
+}
+
 ScriptedThreadInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedThreadInterface() {
   return std::make_shared<ScriptedThreadPythonInterface>(*this);
diff --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h 
b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index f08b2334f9ccb..874f1eca0e1c1 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -93,6 +93,9 @@ class ScriptInterpreterPythonImpl : public 
ScriptInterpreterPython {
   lldb::ScriptedBreakpointInterfaceSP
   CreateScriptedBreakpointInterface() override;
 
+  lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() override;
+
   lldb::ScriptedThreadInterfaceSP CreateScriptedThreadInterface() override;
 
   lldb::ScriptedFrameInterfaceSP CreateScriptedFrameInterface() override;
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp 
b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
index 5694aeeff3e5b..b11131458ab56 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
@@ -171,6 +171,21 @@ 
lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data) {
   return nullptr;
 }
 
+void *
+lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject *data) {
+  return nullptr;
+}
+
+void *
+lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModule(PyObject *data) {
+  return nullptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModuleSpec(
+    PyObject *data) {
+  return nullptr;
+}
+
 lldb::ValueObjectSP
 lldb_private::python::SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
     void *data) {
@@ -363,3 +378,8 @@ python::PythonObject 
lldb_private::python::SWIGBridge::ToSWIGWrapper(
     std::shared_ptr<lldb::SBStream> stream_sb) {
   return python::PythonObject();
 }
+
+python::PythonObject lldb_private::python::SWIGBridge::ToSWIGWrapper(
+    const lldb_private::ModuleSpec &module_spec) {
+  return python::PythonObject();
+}

>From 5942f42b668ea50287ac891ffc3a5cffa4b2bc82 Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <[email protected]>
Date: Thu, 26 Feb 2026 15:00:26 -0800
Subject: [PATCH 3/5] [lldb] Add SymbolLocatorScripted plugin with global
 instance management

Create the SymbolLocatorScripted plugin that registers all 4 callbacks
(LocateSourceFile, LocateExecutableObjectFile, LocateExecutableSymbolFile,
DownloadObjectAndSymbolFile) with PluginManager and manages globally-
registered scripted locator instances.

New files:
- SymbolLocator/Scripted/SymbolLocatorScripted.h/.cpp: plugin class that
  iterates global scripted instances, with per-instance source file cache
  and fast path when no instances are registered
- SymbolLocator/Scripted/CMakeLists.txt

The plugin registers all 4 callbacks as static functions. LocateSourceFile
uses a per-instance cache keyed by (UUID, path). The remaining 3 callbacks
iterate instances and call through to the scripted interface (currently
no-op stubs pending SBModuleSpec SWIG bridge).

PluginManager additions:
- ScriptedSymbolLocatorInstance struct with interface_sp, cache, class_name
- RegisterScriptedSymbolLocator(): creates interface, calls CreatePluginObject
- ClearScriptedSymbolLocators(), GetNumScriptedSymbolLocators(),
  GetScriptedSymbolLocatorClassName(), GetScriptedSymbolLocatorInstances()
---
 lldb/include/lldb/Core/PluginManager.h        |  21 +++
 lldb/source/Core/PluginManager.cpp            |  54 ++++++
 .../Plugins/SymbolLocator/CMakeLists.txt      |   1 +
 .../SymbolLocator/Scripted/CMakeLists.txt     |  13 ++
 .../Scripted/SymbolLocatorScripted.cpp        | 166 ++++++++++++++++++
 .../Scripted/SymbolLocatorScripted.h          |  51 ++++++
 6 files changed, 306 insertions(+)
 create mode 100644 lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
 create mode 100644 
lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
 create mode 100644 
lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h

diff --git a/lldb/include/lldb/Core/PluginManager.h 
b/lldb/include/lldb/Core/PluginManager.h
index 3fd3e6177afa0..2e3aeed8718d6 100644
--- a/lldb/include/lldb/Core/PluginManager.h
+++ b/lldb/include/lldb/Core/PluginManager.h
@@ -483,6 +483,27 @@ class PluginManager {
   static FileSpec LocateSourceFile(const lldb::ModuleSP &module_sp,
                                    const FileSpec &original_source_file);
 
+  /// Scripted symbol locator instance management.
+  /// These manage globally-registered scripted locator instances that are
+  /// consulted by the SymbolLocatorScripted plugin for all symbol/source
+  /// resolution requests.
+  /// @{
+  static Status
+  RegisterScriptedSymbolLocator(Debugger &debugger, llvm::StringRef class_name,
+                                StructuredData::DictionarySP args_sp);
+  static void ClearScriptedSymbolLocators();
+  static size_t GetNumScriptedSymbolLocators();
+  static llvm::StringRef GetScriptedSymbolLocatorClassName(size_t index);
+
+  struct ScriptedSymbolLocatorInstance {
+    lldb::ScriptedSymbolLocatorInterfaceSP interface_sp;
+    llvm::StringMap<std::optional<FileSpec>> source_file_cache;
+    std::string class_name;
+  };
+  static std::vector<ScriptedSymbolLocatorInstance> &
+  GetScriptedSymbolLocatorInstances();
+  /// @}
+
   // Trace
   static bool RegisterPlugin(
       llvm::StringRef name, llvm::StringRef description,
diff --git a/lldb/source/Core/PluginManager.cpp 
b/lldb/source/Core/PluginManager.cpp
index 5b8bcc7cc68ef..dab258193855a 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -12,6 +12,7 @@
 #include "lldb/Host/FileSystem.h"
 #include "lldb/Host/HostInfo.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/SaveCoreOptions.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Utility/FileSpec.h"
@@ -1609,6 +1610,59 @@ FileSpec PluginManager::LocateSourceFile(const 
lldb::ModuleSP &module_sp,
   return {};
 }
 
+static std::vector<PluginManager::ScriptedSymbolLocatorInstance> &
+GetScriptedSymbolLocatorInstancesImpl() {
+  static std::vector<PluginManager::ScriptedSymbolLocatorInstance> g_instances;
+  return g_instances;
+}
+
+std::vector<PluginManager::ScriptedSymbolLocatorInstance> &
+PluginManager::GetScriptedSymbolLocatorInstances() {
+  return GetScriptedSymbolLocatorInstancesImpl();
+}
+
+Status PluginManager::RegisterScriptedSymbolLocator(
+    Debugger &debugger, llvm::StringRef class_name,
+    StructuredData::DictionarySP args_sp) {
+  ScriptInterpreter *interp =
+      debugger.GetScriptInterpreter(/*can_create=*/true);
+  if (!interp)
+    return Status::FromErrorString("no script interpreter");
+
+  auto interface_sp = interp->CreateScriptedSymbolLocatorInterface();
+  if (!interface_sp)
+    return Status::FromErrorString(
+        "failed to create scripted symbol locator interface");
+
+  ExecutionContext exe_ctx;
+  auto obj_or_err =
+      interface_sp->CreatePluginObject(class_name, exe_ctx, args_sp);
+  if (!obj_or_err)
+    return Status::FromError(obj_or_err.takeError());
+
+  ScriptedSymbolLocatorInstance inst;
+  inst.interface_sp = std::move(interface_sp);
+  inst.class_name = class_name.str();
+
+  GetScriptedSymbolLocatorInstancesImpl().push_back(std::move(inst));
+  return Status();
+}
+
+void PluginManager::ClearScriptedSymbolLocators() {
+  GetScriptedSymbolLocatorInstancesImpl().clear();
+}
+
+size_t PluginManager::GetNumScriptedSymbolLocators() {
+  return GetScriptedSymbolLocatorInstancesImpl().size();
+}
+
+llvm::StringRef PluginManager::GetScriptedSymbolLocatorClassName(size_t index) 
{
+  auto &instances = GetScriptedSymbolLocatorInstancesImpl();
+  if (index >= instances.size())
+    return {};
+  return instances[index].class_name;
+}
+
 #pragma mark Trace
 
 struct TraceInstance
diff --git a/lldb/source/Plugins/SymbolLocator/CMakeLists.txt 
b/lldb/source/Plugins/SymbolLocator/CMakeLists.txt
index 3b466f71dca58..bf7f6046eed9d 100644
--- a/lldb/source/Plugins/SymbolLocator/CMakeLists.txt
+++ b/lldb/source/Plugins/SymbolLocator/CMakeLists.txt
@@ -7,6 +7,7 @@ set_property(DIRECTORY PROPERTY LLDB_PLUGIN_KIND SymbolLocator)
 # provider.
 add_subdirectory(Debuginfod)
 add_subdirectory(Default)
+add_subdirectory(Scripted)
 if (CMAKE_SYSTEM_NAME MATCHES "Darwin")
   add_subdirectory(DebugSymbols)
 endif()
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt 
b/lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
new file mode 100644
index 0000000000000..89612d5e1625b
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
@@ -0,0 +1,13 @@
+set_property(DIRECTORY PROPERTY LLDB_PLUGIN_KIND SymbolLocator)
+
+add_lldb_library(lldbPluginSymbolLocatorScripted PLUGIN
+  SymbolLocatorScripted.cpp
+
+  LINK_LIBS
+    lldbCore
+    lldbHost
+    lldbInterpreter
+    lldbSymbol
+    lldbTarget
+    lldbUtility
+  )
diff --git 
a/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp 
b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
new file mode 100644
index 0000000000000..2faf9905ff0c4
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
@@ -0,0 +1,166 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "SymbolLocatorScripted.h"
+
+#include "lldb/Core/Module.h"
+#include "lldb/Core/ModuleSpec.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+LLDB_PLUGIN_DEFINE(SymbolLocatorScripted)
+
+SymbolLocatorScripted::SymbolLocatorScripted() : SymbolLocator() {}
+
+void SymbolLocatorScripted::Initialize() {
+  PluginManager::RegisterPlugin(
+      GetPluginNameStatic(), GetPluginDescriptionStatic(), CreateInstance,
+      LocateExecutableObjectFile, LocateExecutableSymbolFile,
+      DownloadObjectAndSymbolFile, nullptr, LocateSourceFile,
+      DebuggerInitialize);
+}
+
+void SymbolLocatorScripted::Terminate() {
+  PluginManager::ClearScriptedSymbolLocators();
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
+
+llvm::StringRef SymbolLocatorScripted::GetPluginDescriptionStatic() {
+  return "Scripted symbol locator plug-in.";
+}
+
+SymbolLocator *SymbolLocatorScripted::CreateInstance() {
+  return new SymbolLocatorScripted();
+}
+
+void SymbolLocatorScripted::DebuggerInitialize(Debugger &debugger) {
+  // Nothing to initialize per-debugger for now.
+}
+
+std::optional<FileSpec>
+SymbolLocatorScripted::LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                        const FileSpec &original_source_file) {
+  auto &instances = PluginManager::GetScriptedSymbolLocatorInstances();
+  if (instances.empty() || !module_sp)
+    return {};
+
+  std::string cache_key =
+      module_sp->GetUUID().GetAsString() + ":" + 
original_source_file.GetPath();
+
+  for (auto &inst : instances) {
+    // Check the per-instance cache first.
+    auto it = inst.source_file_cache.find(cache_key);
+    if (it != inst.source_file_cache.end()) {
+      if (it->second)
+        return it->second;
+      continue; // This instance cached a miss; try the next one.
+    }
+
+    Status error;
+    auto located = inst.interface_sp->LocateSourceFile(
+        module_sp, original_source_file, error);
+
+    if (!error.Success()) {
+      Log *log = GetLog(LLDBLog::Symbols);
+      LLDB_LOG(log, "SymbolLocatorScripted: locate_source_file failed: {0}",
+               error);
+    }
+
+    inst.source_file_cache[cache_key] = located;
+
+    if (located)
+      return located;
+  }
+
+  return {};
+}
+
+std::optional<ModuleSpec> SymbolLocatorScripted::LocateExecutableObjectFile(
+    const ModuleSpec &module_spec) {
+  auto &instances = PluginManager::GetScriptedSymbolLocatorInstances();
+  if (instances.empty())
+    return {};
+
+  for (auto &inst : instances) {
+    Status error;
+    auto located =
+        inst.interface_sp->LocateExecutableObjectFile(module_spec, error);
+
+    if (!error.Success()) {
+      Log *log = GetLog(LLDBLog::Symbols);
+      LLDB_LOG(log,
+               "SymbolLocatorScripted: locate_executable_object_file failed: "
+               "{0}",
+               error);
+    }
+
+    if (located)
+      return located;
+  }
+
+  return {};
+}
+
+std::optional<FileSpec> SymbolLocatorScripted::LocateExecutableSymbolFile(
+    const ModuleSpec &module_spec, const FileSpecList &default_search_paths) {
+  auto &instances = PluginManager::GetScriptedSymbolLocatorInstances();
+  if (instances.empty())
+    return {};
+
+  for (auto &inst : instances) {
+    Status error;
+    auto located =
+        inst.interface_sp->LocateExecutableSymbolFile(module_spec, error);
+
+    if (!error.Success()) {
+      Log *log = GetLog(LLDBLog::Symbols);
+      LLDB_LOG(log,
+               "SymbolLocatorScripted: locate_executable_symbol_file failed: "
+               "{0}",
+               error);
+    }
+
+    if (located)
+      return located;
+  }
+
+  return {};
+}
+
+bool SymbolLocatorScripted::DownloadObjectAndSymbolFile(ModuleSpec 
&module_spec,
+                                                        Status &error,
+                                                        bool force_lookup,
+                                                        bool copy_executable) {
+  auto &instances = PluginManager::GetScriptedSymbolLocatorInstances();
+  if (instances.empty())
+    return false;
+
+  for (auto &inst : instances) {
+    Status inst_error;
+    bool downloaded =
+        inst.interface_sp->DownloadObjectAndSymbolFile(module_spec, 
inst_error);
+
+    if (!inst_error.Success()) {
+      Log *log = GetLog(LLDBLog::Symbols);
+      LLDB_LOG(log,
+               "SymbolLocatorScripted: download_object_and_symbol_file "
+               "failed: {0}",
+               inst_error);
+    }
+
+    if (downloaded)
+      return true;
+  }
+
+  return false;
+}
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h 
b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
new file mode 100644
index 0000000000000..5d373289e2579
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
@@ -0,0 +1,51 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H
+#define LLDB_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H
+
+#include "lldb/Symbol/SymbolLocator.h"
+#include "lldb/lldb-private.h"
+
+namespace lldb_private {
+
+class SymbolLocatorScripted : public SymbolLocator {
+public:
+  SymbolLocatorScripted();
+
+  static void Initialize();
+  static void Terminate();
+  static void DebuggerInitialize(Debugger &debugger);
+
+  static llvm::StringRef GetPluginNameStatic() { return "scripted"; }
+  static llvm::StringRef GetPluginDescriptionStatic();
+
+  static lldb_private::SymbolLocator *CreateInstance();
+
+  /// PluginInterface protocol.
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+
+  static std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec);
+
+  static std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths);
+
+  static bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                          Status &error, bool force_lookup,
+                                          bool copy_executable);
+
+  static std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file);
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H

>From c0bcae31657963a51b70dc1c89703e4834228f7c Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <[email protected]>
Date: Thu, 26 Feb 2026 15:01:22 -0800
Subject: [PATCH 4/5] [lldb] Add commands and SBDebugger API for scripted
 symbol locators

Add user-facing commands and SBDebugger API for managing globally-
registered scripted symbol locators.

Commands:
- `target symbols scripted register -C <class> [-k <key> -v <value>]`:
  Register a scripted symbol locator globally
- `target symbols scripted clear`: Clear all registered locators
- `target symbols scripted info`: List registered locators

SBDebugger API:
- RegisterScriptedSymbolLocator(class_name, args): Register a locator
  programmatically, routing through PluginManager
- ClearScriptedSymbolLocators(): Clear all registered locators
---
 lldb/include/lldb/API/SBDebugger.h           |  20 +++
 lldb/source/API/SBDebugger.cpp               |  29 ++++
 lldb/source/Commands/CommandObjectTarget.cpp | 131 +++++++++++++++++++
 3 files changed, 180 insertions(+)

diff --git a/lldb/include/lldb/API/SBDebugger.h 
b/lldb/include/lldb/API/SBDebugger.h
index 688fc9197965c..006d23ceb9490 100644
--- a/lldb/include/lldb/API/SBDebugger.h
+++ b/lldb/include/lldb/API/SBDebugger.h
@@ -667,6 +667,26 @@ class LLDB_API SBDebugger {
   SBTrace LoadTraceFromFile(SBError &error,
                             const SBFileSpec &trace_description_file);
 
+  /// Register a scripted symbol locator globally.
+  ///
+  /// Scripted symbol locators are consulted for all symbol/source
+  /// resolution requests across all targets. Multiple locators can be
+  /// registered; each is called in order until one returns a result.
+  ///
+  /// \param[in] class_name
+  ///     The Python class implementing the symbol locator.
+  ///
+  /// \param[in] args
+  ///     Optional structured data arguments passed to the locator.
+  ///
+  /// \return
+  ///     An SBError indicating success or failure.
+  lldb::SBError RegisterScriptedSymbolLocator(const char *class_name,
+                                              lldb::SBStructuredData &args);
+
+  /// Clear all registered scripted symbol locators.
+  void ClearScriptedSymbolLocators();
+
 protected:
   friend class lldb_private::CommandPluginInterfaceImplementation;
   friend class lldb_private::python::SWIGBridge;
diff --git a/lldb/source/API/SBDebugger.cpp b/lldb/source/API/SBDebugger.cpp
index e97fdd0e352f2..6b86eefd4dbf5 100644
--- a/lldb/source/API/SBDebugger.cpp
+++ b/lldb/source/API/SBDebugger.cpp
@@ -1684,6 +1684,35 @@ SBDebugger::LoadTraceFromFile(SBError &error,
   return SBTrace::LoadTraceFromFile(error, *this, trace_description_file);
 }
 
+lldb::SBError
+SBDebugger::RegisterScriptedSymbolLocator(const char *class_name,
+                                          lldb::SBStructuredData &args) {
+  LLDB_INSTRUMENT_VA(this, class_name, args);
+
+  lldb::SBError sb_error;
+  if (!m_opaque_sp) {
+    sb_error.SetErrorString("invalid debugger");
+    return sb_error;
+  }
+
+  StructuredData::DictionarySP args_sp;
+  StructuredData::ObjectSP obj_sp = args.m_impl_up->GetObjectSP();
+  if (obj_sp && obj_sp->GetType() == lldb::eStructuredDataTypeDictionary)
+    args_sp = std::static_pointer_cast<StructuredData::Dictionary>(obj_sp);
+
+  Status error = PluginManager::RegisterScriptedSymbolLocator(
+      *m_opaque_sp, class_name, args_sp);
+  if (error.Fail())
+    sb_error.SetErrorString(error.AsCString());
+  return sb_error;
+}
+
+void SBDebugger::ClearScriptedSymbolLocators() {
+  LLDB_INSTRUMENT_VA(this);
+
+  PluginManager::ClearScriptedSymbolLocators();
+}
+
 void SBDebugger::RequestInterrupt() {
   LLDB_INSTRUMENT_VA(this);
 
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp 
b/lldb/source/Commands/CommandObjectTarget.cpp
index 59ccf390dea31..8592b85e1025f 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -4691,6 +4691,134 @@ class CommandObjectTargetSymbolsAdd : public 
CommandObjectParsed {
   OptionGroupBoolean m_current_stack_option;
 };
 
+#pragma mark CommandObjectTargetSymbolsScriptedRegister
+
+class CommandObjectTargetSymbolsScriptedRegister : public CommandObjectParsed {
+public:
+  CommandObjectTargetSymbolsScriptedRegister(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target symbols scripted register",
+            "Register a scripted symbol locator globally.",
+            "target symbols scripted register -C <script-class> "
+            "[-k <key> -v <value> ...]"),
+        m_python_class_options("scripted symbol locator", true, 'C', 'k', 'v',
+                               OptionGroupPythonClassWithDict::eScriptClass) {
+    m_all_options.Append(&m_python_class_options,
+                         LLDB_OPT_SET_1 | LLDB_OPT_SET_2, LLDB_OPT_SET_1);
+    m_all_options.Finalize();
+  }
+
+  ~CommandObjectTargetSymbolsScriptedRegister() override = default;
+
+  Options *GetOptions() override { return &m_all_options; }
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    llvm::StringRef class_name = m_python_class_options.GetName();
+    if (class_name.empty()) {
+      result.AppendError("must specify a script class with -C");
+      return;
+    }
+
+    StructuredData::DictionarySP args_sp;
+    StructuredData::ObjectSP extra = 
m_python_class_options.GetStructuredData();
+    if (extra && extra->GetType() == lldb::eStructuredDataTypeDictionary)
+      args_sp = std::static_pointer_cast<StructuredData::Dictionary>(extra);
+
+    Debugger &debugger = GetDebugger();
+    Status error = PluginManager::RegisterScriptedSymbolLocator(
+        debugger, class_name, args_sp);
+    if (error.Fail()) {
+      result.AppendErrorWithFormat(
+          "failed to register scripted symbol locator: %s\n",
+          error.AsCString());
+      return;
+    }
+
+    result.AppendMessageWithFormat("Registered scripted symbol locator 
'%s'.\n",
+                                   class_name.str().c_str());
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+
+  OptionGroupPythonClassWithDict m_python_class_options;
+  OptionGroupOptions m_all_options;
+};
+
+#pragma mark CommandObjectTargetSymbolsScriptedClear
+
+class CommandObjectTargetSymbolsScriptedClear : public CommandObjectParsed {
+public:
+  CommandObjectTargetSymbolsScriptedClear(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target symbols scripted clear",
+                            "Clear all scripted symbol locators.",
+                            "target symbols scripted clear") {}
+
+  ~CommandObjectTargetSymbolsScriptedClear() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    PluginManager::ClearScriptedSymbolLocators();
+    result.AppendMessageWithFormat("Cleared all scripted symbol locators.\n");
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+#pragma mark CommandObjectTargetSymbolsScriptedInfo
+
+class CommandObjectTargetSymbolsScriptedInfo : public CommandObjectParsed {
+public:
+  CommandObjectTargetSymbolsScriptedInfo(CommandInterpreter &interpreter)
+      : CommandObjectParsed(interpreter, "target symbols scripted info",
+                            "List all registered scripted symbol locators.",
+                            "target symbols scripted info") {}
+
+  ~CommandObjectTargetSymbolsScriptedInfo() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    size_t num = PluginManager::GetNumScriptedSymbolLocators();
+    if (num == 0) {
+      result.AppendMessageWithFormat(
+          "No scripted symbol locators registered.\n");
+      result.SetStatus(eReturnStatusSuccessFinishResult);
+      return;
+    }
+
+    result.AppendMessageWithFormat(
+        "%zu scripted symbol locator(s) registered:\n", num);
+    for (size_t i = 0; i < num; i++) {
+      result.AppendMessageWithFormat(
+          "  [%zu] %s\n", i,
+          PluginManager::GetScriptedSymbolLocatorClassName(i).str().c_str());
+    }
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+#pragma mark CommandObjectTargetSymbolsScripted
+
+class CommandObjectTargetSymbolsScripted : public CommandObjectMultiword {
+public:
+  CommandObjectTargetSymbolsScripted(CommandInterpreter &interpreter)
+      : CommandObjectMultiword(
+            interpreter, "target symbols scripted",
+            "Commands for managing scripted symbol locators.",
+            "target symbols scripted <sub-command> ...") {
+    LoadSubCommand(
+        "register",
+        CommandObjectSP(
+            new CommandObjectTargetSymbolsScriptedRegister(interpreter)));
+    LoadSubCommand(
+        "clear", CommandObjectSP(
+                     new 
CommandObjectTargetSymbolsScriptedClear(interpreter)));
+    LoadSubCommand(
+        "info", CommandObjectSP(
+                    new CommandObjectTargetSymbolsScriptedInfo(interpreter)));
+  }
+
+  ~CommandObjectTargetSymbolsScripted() override = default;
+};
+
 #pragma mark CommandObjectTargetSymbols
 
 // CommandObjectTargetSymbols
@@ -4705,6 +4833,9 @@ class CommandObjectTargetSymbols : public 
CommandObjectMultiword {
             "target symbols <sub-command> ...") {
     LoadSubCommand(
         "add", CommandObjectSP(new 
CommandObjectTargetSymbolsAdd(interpreter)));
+    LoadSubCommand(
+        "scripted",
+        CommandObjectSP(new CommandObjectTargetSymbolsScripted(interpreter)));
   }
 
   ~CommandObjectTargetSymbols() override = default;

>From 4fb7095c88bd7d4e1b98a0d8fa84c494f9832843 Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <[email protected]>
Date: Thu, 26 Feb 2026 15:11:16 -0800
Subject: [PATCH 5/5] [lldb] Add tests, docs, and Python template for
 ScriptedSymbolLocator

Add API tests, documentation, and Python base class template for the
ScriptedSymbolLocator plugin.

Tests:
- TestScriptedSymbolLocator.py: 5 test cases covering locate_source_file,
  None fallthrough, invalid script class, scripted info command, and
  SBDebugger API
- source_locator.py: SourceLocator and NoneLocator test helpers
- main.c + Makefile: simple test program

Documentation:
- custom-symbol-resolution.md: tutorial with quick start, available
  methods, global registration, caching, and base class template
- python_extensions.rst: automodapi section for scripted_symbol_locator
- python-reference.rst: toctree entry for the new tutorial

Template:
- scripted_symbol_locator.py: base class template with all 4 callback
  methods documented, plus LocalCacheSymbolLocator example
- bindings/python/CMakeLists.txt: add template to plugins package
- docs/CMakeLists.txt: copy template for Sphinx doc generation
---
 lldb/bindings/python/CMakeLists.txt           |   1 +
 lldb/docs/CMakeLists.txt                      |   1 +
 lldb/docs/python_extensions.rst               |   8 +
 lldb/docs/use/python-reference.rst            |   1 +
 .../use/tutorials/custom-symbol-resolution.md | 131 ++++++++++
 .../templates/scripted_symbol_locator.py      | 185 ++++++++++++++
 .../scripted_symbol_locator/Makefile          |   8 +
 .../TestScriptedSymbolLocator.py              | 229 ++++++++++++++++++
 .../scripted_symbol_locator/main.c            |   4 +
 .../scripted_symbol_locator/source_locator.py |  44 ++++
 10 files changed, 612 insertions(+)
 create mode 100644 lldb/docs/use/tutorials/custom-symbol-resolution.md
 create mode 100644 lldb/examples/python/templates/scripted_symbol_locator.py
 create mode 100644 
lldb/test/API/functionalities/scripted_symbol_locator/Makefile
 create mode 100644 
lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
 create mode 100644 lldb/test/API/functionalities/scripted_symbol_locator/main.c
 create mode 100644 
lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py

diff --git a/lldb/bindings/python/CMakeLists.txt 
b/lldb/bindings/python/CMakeLists.txt
index 2ebcf5a8e7aca..294f32fa05ba9 100644
--- a/lldb/bindings/python/CMakeLists.txt
+++ b/lldb/bindings/python/CMakeLists.txt
@@ -114,6 +114,7 @@ function(finish_swig_python swig_target 
lldb_python_bindings_dir lldb_python_tar
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_frame_provider.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_process.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_platform.py"
+    "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_symbol_locator.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/operating_system.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_thread_plan.py"
     )
diff --git a/lldb/docs/CMakeLists.txt b/lldb/docs/CMakeLists.txt
index bbecf606f1f8f..760f7e9bd121c 100644
--- a/lldb/docs/CMakeLists.txt
+++ b/lldb/docs/CMakeLists.txt
@@ -31,6 +31,7 @@ if (LLDB_ENABLE_PYTHON AND SPHINX_FOUND)
       COMMAND "${CMAKE_COMMAND}" -E copy 
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_frame_provider.py" 
"${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy 
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_process.py" 
"${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy 
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_platform.py" 
"${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
+      COMMAND "${CMAKE_COMMAND}" -E copy 
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_symbol_locator.py" 
"${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy 
"${LLDB_SOURCE_DIR}/examples/python/templates/operating_system.py" 
"${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMAND "${CMAKE_COMMAND}" -E copy 
"${LLDB_SOURCE_DIR}/examples/python/templates/scripted_thread_plan.py" 
"${CMAKE_CURRENT_BINARY_DIR}/lldb/plugins/"
       COMMENT "Copying lldb.py to pretend its a Python package.")
diff --git a/lldb/docs/python_extensions.rst b/lldb/docs/python_extensions.rst
index 8420187efcdcc..c02675ffb309d 100644
--- a/lldb/docs/python_extensions.rst
+++ b/lldb/docs/python_extensions.rst
@@ -38,6 +38,14 @@ Scripted Platform Plugins
     :skip: ABCMeta
     :no-inheritance-diagram:
 
+Scripted Symbol Locator Plugins
+-----------------------------------
+
+.. automodapi:: lldb.plugins.scripted_symbol_locator
+    :no-heading:
+    :skip: ABCMeta, LocalCacheSymbolLocator
+    :no-inheritance-diagram:
+
 Scripted Thread Plan Plugins
 -------------------------------
 
diff --git a/lldb/docs/use/python-reference.rst 
b/lldb/docs/use/python-reference.rst
index afca07520d8ad..3e7d56f4a2096 100644
--- a/lldb/docs/use/python-reference.rst
+++ b/lldb/docs/use/python-reference.rst
@@ -28,3 +28,4 @@ The following tutorials and documentation demonstrate various 
Python capabilitie
    tutorials/implementing-standalone-scripts
    tutorials/custom-frame-recognizers
    tutorials/extending-target-stop-hooks
+   tutorials/custom-symbol-resolution
diff --git a/lldb/docs/use/tutorials/custom-symbol-resolution.md 
b/lldb/docs/use/tutorials/custom-symbol-resolution.md
new file mode 100644
index 0000000000000..9d3edd2f7e57e
--- /dev/null
+++ b/lldb/docs/use/tutorials/custom-symbol-resolution.md
@@ -0,0 +1,131 @@
+# Finding Symbols With a Scripted Symbol Locator
+
+The **Scripted Symbol Locator** lets you write a Python class that tells LLDB
+where to find source files for your debug targets. This is useful when your
+build artifacts live in a custom location, such as a symbol server or a local
+build-ID-indexed cache.
+
+## Quick Start
+
+1. **Write a locator class.** Create a Python file (e.g., `my_locator.py`)
+   with a class that implements the methods you need:
+
+   ```python
+   import os
+   import lldb
+   from lldb.plugins.scripted_symbol_locator import ScriptedSymbolLocator
+
+   class MyLocator(ScriptedSymbolLocator):
+       def __init__(self, exe_ctx, args):
+           super().__init__(exe_ctx, args)
+           self.cache_dir = None
+           if self.args and self.args.IsValid():
+               d = self.args.GetValueForKey("cache_dir")
+               if d and d.IsValid():
+                   self.cache_dir = d.GetStringValue(4096)
+
+       def locate_source_file(self, module, original_source_file):
+           """Return the resolved file spec, or None to fall through."""
+           if not self.cache_dir:
+               return None
+           uuid = module.GetUUIDString()
+           basename = os.path.basename(original_source_file)
+           candidate = os.path.join(self.cache_dir, uuid, "src", basename)
+           if os.path.exists(candidate):
+               return lldb.SBFileSpec(candidate, True)
+           return None
+   ```
+
+2. **Import the script and register the locator globally:**
+
+   ```
+   (lldb) command script import /path/to/my_locator.py
+   (lldb) target symbols scripted register \
+              -C my_locator.MyLocator \
+              -k cache_dir -v /path/to/cache
+   ```
+
+3. **Debug normally.** When LLDB resolves source files for any target,
+   your `locate_source_file` method will be called automatically.
+
+## Available Methods
+
+Your locator class must implement `__init__` and `locate_source_file`.
+
+| Method | Called When |
+|--------|------------|
+| `locate_source_file(module, path)` | LLDB resolves a source file path in 
debug info |
+
+### Method Signatures
+
+```python
+def __init__(self, exe_ctx: lldb.SBExecutionContext,
+             args: lldb.SBStructuredData) -> None:
+    ...
+
+def locate_source_file(self, module: lldb.SBModule,
+                       original_source_file: str) -> Optional[lldb.SBFileSpec]:
+    ...
+```
+
+## Global Registration
+
+Scripted symbol locators are registered **globally** (not per-target),
+following the Debuginfod model. Multiple locators can be registered;
+each is called in order until one returns a result. To filter by
+platform/architecture, inspect the module's triple or UUID in each
+callback and return `None` for non-matching modules.
+
+### Commands
+
+| Command | Description |
+|---------|-------------|
+| `target symbols scripted register -C <class> [-k <key> -v <value> ...]` | 
Register a locator globally |
+| `target symbols scripted clear` | Clear all registered locators |
+| `target symbols scripted info` | List all registered locators |
+
+### SB API
+
+You can also register locators programmatically via `SBDebugger`:
+
+```python
+import lldb
+
+args = lldb.SBStructuredData()
+args.SetFromJSON('{"cache_dir": "/path/to/cache"}')
+error = debugger.RegisterScriptedSymbolLocator(
+    "my_locator.MyLocator", args)
+
+debugger.ClearScriptedSymbolLocators()
+```
+
+## Caching
+
+Source file resolutions are cached per `(module UUID, source file path)` pair
+within each locator instance. The cache is cleared when:
+
+- All locators are cleared (via `clear` or `ClearScriptedSymbolLocators()`)
+
+This means your `locate_source_file` method is called at most once per
+unique `(UUID, path)` combination per locator instance.
+
+## Base Class Template
+
+LLDB ships a base class template at `lldb.plugins.scripted_symbol_locator`.
+You can import and subclass it:
+
+```python
+from lldb.plugins.scripted_symbol_locator import ScriptedSymbolLocator
+
+class MyLocator(ScriptedSymbolLocator):
+    def __init__(self, exe_ctx, args):
+        super().__init__(exe_ctx, args)
+
+    def locate_source_file(self, module, original_source_file):
+        # Your implementation here
+        return None
+```
+
+The base class handles argument extraction. See
+`lldb/examples/python/templates/scripted_symbol_locator.py` for the full
+template with docstrings.
diff --git a/lldb/examples/python/templates/scripted_symbol_locator.py 
b/lldb/examples/python/templates/scripted_symbol_locator.py
new file mode 100644
index 0000000000000..71e5d6bf808af
--- /dev/null
+++ b/lldb/examples/python/templates/scripted_symbol_locator.py
@@ -0,0 +1,185 @@
+from abc import ABCMeta, abstractmethod
+import os
+
+import lldb
+
+
+class ScriptedSymbolLocator(metaclass=ABCMeta):
+    """
+    The base class for a scripted symbol locator.
+
+    Scripted symbol locators are registered globally (not per-target)
+    and are consulted for all symbol/source resolution requests across
+    all targets. Multiple locators can be registered; each is called
+    in order until one returns a result.
+
+    All callback methods are optional and return ``None`` (or ``False``
+    for ``download_object_and_symbol_file``) to fall through to the
+    next locator or LLDB's default resolution.
+
+    To filter by platform/architecture, inspect the module's triple
+    or UUID in each callback and return ``None`` for non-matching
+    modules.
+
+    Configuration::
+
+        (lldb) command script import /path/to/my_locator.py
+        (lldb) target symbols scripted register -C my_locator.MyLocator \\
+                   [-k key -v value ...]
+    """
+
+    @abstractmethod
+    def __init__(self, exe_ctx, args):
+        """Construct a scripted symbol locator.
+
+        Args:
+            exe_ctx (lldb.SBExecutionContext): The execution context for
+                the scripted symbol locator. This will be empty (no target)
+                since locators are registered globally.
+            args (lldb.SBStructuredData): A Dictionary holding arbitrary
+                key/value pairs used by the scripted symbol locator.
+        """
+        self.args = None
+        if isinstance(args, lldb.SBStructuredData) and args.IsValid():
+            self.args = args
+
+    def locate_source_file(self, module, original_source_file):
+        """Locate the source file for a given module.
+
+        Called when LLDB resolves source file paths during stack frame
+        display, breakpoint resolution, or source listing. This is the
+        primary method for implementing source file remapping based on
+        build IDs.
+
+        The module is a fully loaded ``SBModule`` (not an ``SBModuleSpec``),
+        so you can access its UUID, file path, platform file path,
+        symbol file path, sections, and symbols.
+
+        Results are cached per (module UUID, source file) pair per
+        locator instance, so this method is called at most once per
+        unique combination.
+
+        Args:
+            module (lldb.SBModule): The loaded module containing debug
+                info. Use ``module.GetUUIDString()`` to get the build ID
+                for looking up the correct source revision.
+            original_source_file (str): The original source file path
+                as recorded in the debug info.
+
+        Returns:
+            lldb.SBFileSpec: The resolved file spec, or None to fall
+                through to the next locator or LLDB's default resolution.
+        """
+        return None
+
+    def locate_executable_object_file(self, module_spec):
+        """Locate the executable object file for a given module spec.
+
+        Called when LLDB needs to find the executable binary for a module
+        (e.g., when loading a core dump or attaching to a process).
+
+        Args:
+            module_spec (lldb.SBModuleSpec): The module specification
+                containing UUID, architecture, and file path hints.
+
+        Returns:
+            lldb.SBModuleSpec: A module spec with the resolved file path,
+                or None to fall through.
+        """
+        return None
+
+    def locate_executable_symbol_file(self, module_spec):
+        """Locate the symbol file for a given module spec.
+
+        Called when LLDB needs to find debug symbols (e.g., .dSYM, .debug,
+        or separate .pdb files) for a module.
+
+        Args:
+            module_spec (lldb.SBModuleSpec): The module specification
+                containing UUID, architecture, and file path hints.
+
+        Returns:
+            lldb.SBFileSpec: The resolved symbol file path, or None
+                to fall through.
+        """
+        return None
+
+    def download_object_and_symbol_file(self, module_spec):
+        """Download the object file and/or symbol file for a module.
+
+        Called when LLDB needs to download both the executable and its
+        symbols (e.g., from a symbol server).
+
+        Args:
+            module_spec (lldb.SBModuleSpec): The module specification.
+                This may be modified in place to set the resolved paths.
+
+        Returns:
+            bool: True if files were successfully downloaded, False
+                to fall through.
+        """
+        return False
+
+
+class LocalCacheSymbolLocator(ScriptedSymbolLocator):
+    """Example locator that resolves source files from a local cache directory.
+
+    Demonstrates how to subclass ``ScriptedSymbolLocator`` to implement
+    custom source file resolution. This locator looks up source files
+    in a local directory structure organized by build ID (UUID)::
+
+        <cache_dir>/
+            <uuid>/
+                src/
+                    main.cpp
+                    ...
+
+    Usage::
+
+        (lldb) command script import scripted_symbol_locator
+        (lldb) target symbols scripted register \\
+                   -C scripted_symbol_locator.LocalCacheSymbolLocator \\
+                   -k cache_dir -v "/path/to/cache"
+        (lldb) target create --core /path/to/minidump.dmp
+        (lldb) bt
+
+    The locator searches for:
+      - Source files:   ``<cache_dir>/<uuid>/src/<basename>``
+    """
+
+    cache_dir = None
+
+    def __init__(self, exe_ctx, args):
+        super().__init__(exe_ctx, args)
+
+        # Allow cache_dir to be set via structured data args.
+        if self.args:
+            cache_dir_val = self.args.GetValueForKey("cache_dir")
+            if cache_dir_val and cache_dir_val.IsValid():
+                val = cache_dir_val.GetStringValue(256)
+                if val:
+                    LocalCacheSymbolLocator.cache_dir = val
+
+    def _get_cache_path(self, uuid_str, *components):
+        """Build a path under the cache directory for a given UUID.
+
+        Args:
+            uuid_str (str): The module's UUID string.
+            *components: Additional path components (e.g., filename).
+
+        Returns:
+            str: The full path, or None if cache_dir is not set or the
+                UUID is empty.
+        """
+        if not self.cache_dir or not uuid_str:
+            return None
+        return os.path.join(self.cache_dir, uuid_str, *components)
+
+    def locate_source_file(self, module, original_source_file):
+        """Look up source files under ``<cache_dir>/<uuid>/src/``."""
+        uuid_str = module.GetUUIDString()
+        basename = os.path.basename(original_source_file)
+        path = self._get_cache_path(uuid_str, "src", basename)
+        if path and os.path.exists(path):
+            return lldb.SBFileSpec(path, True)
+        return None
diff --git a/lldb/test/API/functionalities/scripted_symbol_locator/Makefile 
b/lldb/test/API/functionalities/scripted_symbol_locator/Makefile
new file mode 100644
index 0000000000000..a33c7b0e192f4
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/Makefile
@@ -0,0 +1,8 @@
+C_SOURCES := main.c
+USE_SYSTEM_STDLIB := 1
+
+ifneq "$(OS)" "Darwin"
+LD_EXTRAS := -Wl,--build-id
+endif
+
+include Makefile.rules
diff --git 
a/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
 
b/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
new file mode 100644
index 0000000000000..c43a2eba99c2f
--- /dev/null
+++ 
b/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
@@ -0,0 +1,229 @@
+"""
+Test the ScriptedSymbolLocator plugin for source file resolution.
+"""
+
+import os
+import shutil
+import tempfile
+
+import lldb
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+from lldbsuite.test import lldbutil
+
+
+class ScriptedSymbolLocatorTestCase(TestBase):
+    NO_DEBUG_INFO_TESTCASE = True
+
+    def setUp(self):
+        TestBase.setUp(self)
+        self.main_source_file = lldb.SBFileSpec("main.c")
+
+    def import_locator(self):
+        self.runCmd(
+            "command script import "
+            + os.path.join(self.getSourceDir(), "source_locator.py")
+        )
+
+    def register_locator(self, class_name, extra_args=""):
+        cmd = "target symbols scripted register -C " + class_name
+        if extra_args:
+            cmd += " " + extra_args
+        self.runCmd(cmd)
+
+    def clear_locators(self):
+        self.runCmd("target symbols scripted clear")
+
+    def script(self, expr):
+        """Execute a Python expression in LLDB's script interpreter and return
+        the result as a string."""
+        ret = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand("script " + expr, ret)
+        return ret.GetOutput().strip() if ret.Succeeded() else ""
+
+    def test_locate_source_file(self):
+        """Test that the scripted locator resolves source files and receives
+        an SBModule with a valid UUID."""
+        self.build()
+
+        # Copy main.c to a temp directory so the locator can "resolve" to it.
+        tmp_dir = tempfile.mkdtemp()
+        self.addTearDownHook(lambda: shutil.rmtree(tmp_dir))
+        shutil.copy(os.path.join(self.getSourceDir(), "main.c"), tmp_dir)
+
+        # Create the target BEFORE setting the script class, so module loading
+        # (which may run on worker threads) does not trigger the Python 
locator.
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        # Register the locator globally (via command).
+        self.import_locator()
+        self.register_locator(
+            "source_locator.SourceLocator",
+            "-k resolved_dir -v '%s'" % tmp_dir,
+        )
+        self.addTearDownHook(lambda: self.clear_locators())
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        # Launch and stop at the breakpoint so ApplyFileMappings runs on
+        # the main thread via StackFrame::GetSymbolContext.
+        (target, process, thread, bkpt) = lldbutil.run_to_breakpoint_do_run(
+            self, target, bp
+        )
+        frame = thread.GetSelectedFrame()
+        line_entry = frame.GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is 
valid")
+        self.assertEqual(line_entry.GetFileSpec().GetFilename(), "main.c")
+
+        # Verify the resolved path points to our temp directory.
+        resolved_dir = line_entry.GetFileSpec().GetDirectory()
+        self.assertEqual(resolved_dir, tmp_dir)
+
+        # Verify the locator was called with a valid UUID by reading
+        # module UUID (locator was called since path was resolved).
+        calls_str = self.script(
+            "[c for c in __import__('lldb').debugger.GetSelectedTarget()"
+            ".GetModuleAtIndex(0).GetUUIDString()]"
+        )
+        # Just verify the UUID is a non-empty string (the locator was called)
+        self.assertTrue(len(calls_str) > 0, "Module should have a UUID")
+
+        self.dbg.DeleteTarget(target)
+
+    def test_locate_source_file_none_fallthrough(self):
+        """Test that returning None falls through to normal LLDB resolution,
+        and that having no locator registered also works normally."""
+        self.build()
+
+        # First: test with NoneLocator -- should fall through.
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        self.import_locator()
+        self.register_locator("source_locator.NoneLocator")
+        self.addTearDownHook(lambda: self.clear_locators())
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        loc = bp.GetLocationAtIndex(0)
+        line_entry = loc.GetAddress().GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is 
valid")
+        self.assertEqual(line_entry.GetFileSpec().GetFilename(), "main.c")
+
+        self.dbg.DeleteTarget(target)
+        self.clear_locators()
+
+        # Second: test with no locator registered -- should also work normally.
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        loc = bp.GetLocationAtIndex(0)
+        line_entry = loc.GetAddress().GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is 
valid")
+        self.assertEqual(line_entry.GetFileSpec().GetFilename(), "main.c")
+
+        self.dbg.DeleteTarget(target)
+
+    def test_invalid_script_class(self):
+        """Test that an invalid script class name is handled gracefully
+        without crashing, and breakpoints still resolve."""
+        self.build()
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        # Registering a nonexistent class should fail, but not crash.
+        ret = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "target symbols scripted register "
+            "-C nonexistent_module.NonexistentClass",
+            ret,
+        )
+        # The command should have failed.
+        self.assertFalse(ret.Succeeded())
+
+        # Breakpoints should still resolve via normal path.
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+        self.assertEqual(bp.GetNumLocations(), 1)
+
+        loc = bp.GetLocationAtIndex(0)
+        line_entry = loc.GetAddress().GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is 
valid")
+
+        self.dbg.DeleteTarget(target)
+
+    def test_scripted_info_command(self):
+        """Test that 'target symbols scripted info' reports registered 
locators."""
+        self.build()
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        # Before registration, should report no locators.
+        ret = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "target symbols scripted info", ret
+        )
+        self.assertTrue(ret.Succeeded())
+        self.assertIn("No scripted symbol locator", ret.GetOutput())
+
+        # After registration, should report the class name.
+        self.import_locator()
+        self.register_locator("source_locator.NoneLocator")
+        self.addTearDownHook(lambda: self.clear_locators())
+
+        ret = lldb.SBCommandReturnObject()
+        self.dbg.GetCommandInterpreter().HandleCommand(
+            "target symbols scripted info", ret
+        )
+        self.assertTrue(ret.Succeeded())
+        self.assertIn("source_locator.NoneLocator", ret.GetOutput())
+
+        self.dbg.DeleteTarget(target)
+
+    def test_sb_debugger_api(self):
+        """Test the SBDebugger API for registering and clearing locators."""
+        self.build()
+
+        # Copy main.c to a temp directory so the locator can "resolve" to it.
+        tmp_dir = tempfile.mkdtemp()
+        self.addTearDownHook(lambda: shutil.rmtree(tmp_dir))
+        shutil.copy(os.path.join(self.getSourceDir(), "main.c"), tmp_dir)
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        self.import_locator()
+
+        # Register via SBDebugger API.
+        args = lldb.SBStructuredData()
+        args.SetFromJSON('{"resolved_dir": "%s"}' % tmp_dir)
+        error = self.dbg.RegisterScriptedSymbolLocator(
+            "source_locator.SourceLocator", args
+        )
+        self.assertSuccess(error)
+        self.addTearDownHook(lambda: self.dbg.ClearScriptedSymbolLocators())
+
+        bp = target.BreakpointCreateByName("func")
+        self.assertTrue(bp and bp.IsValid(), "Breakpoint is valid")
+
+        (target, process, thread, bkpt) = lldbutil.run_to_breakpoint_do_run(
+            self, target, bp
+        )
+        frame = thread.GetSelectedFrame()
+        line_entry = frame.GetLineEntry()
+        self.assertTrue(line_entry and line_entry.IsValid(), "Line entry is 
valid")
+        resolved_dir = line_entry.GetFileSpec().GetDirectory()
+        self.assertEqual(resolved_dir, tmp_dir)
+
+        self.dbg.DeleteTarget(target)
diff --git a/lldb/test/API/functionalities/scripted_symbol_locator/main.c 
b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
new file mode 100644
index 0000000000000..60d049cfd3fda
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
@@ -0,0 +1,4 @@
+int func(int argc) {
+  return argc + 1; // break here
+}
+int main(int argc, const char *argv[]) { return func(argc); }
diff --git 
a/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py 
b/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py
new file mode 100644
index 0000000000000..83c0d6c90f19d
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py
@@ -0,0 +1,44 @@
+import os
+from typing import Optional
+
+import lldb
+
+
+class SourceLocator:
+    """Test locator that records calls and returns a configured resolved 
path."""
+
+    def __init__(
+        self, exe_ctx: lldb.SBExecutionContext, args: lldb.SBStructuredData
+    ) -> None:
+        self.calls: list = []
+        self.resolved_dir: Optional[str] = None
+        if args.IsValid():
+            resolved_dir_val = args.GetValueForKey("resolved_dir")
+            if resolved_dir_val and resolved_dir_val.IsValid():
+                val = resolved_dir_val.GetStringValue(4096)
+                if val:
+                    self.resolved_dir = val
+
+    def locate_source_file(
+        self, module: lldb.SBModule, original_source_file: str
+    ) -> Optional[lldb.SBFileSpec]:
+        uuid = module.GetUUIDString()
+        self.calls.append((uuid, original_source_file))
+        if self.resolved_dir:
+            basename = os.path.basename(original_source_file)
+            return lldb.SBFileSpec(os.path.join(self.resolved_dir, basename), 
True)
+        return None
+
+
+class NoneLocator:
+    """Locator that always returns None."""
+
+    def __init__(
+        self, exe_ctx: lldb.SBExecutionContext, args: lldb.SBStructuredData
+    ) -> None:
+        pass
+
+    def locate_source_file(
+        self, module: lldb.SBModule, original_source_file: str
+    ) -> Optional[lldb.SBFileSpec]:
+        return None

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to