[Lldb-commits] [lldb] [lldb] Add ScriptedSymbolLocator plugin for source file resolution (PR #181334)

via lldb-commits lldb-commits at lists.llvm.org
Fri Feb 13 11:09:42 PST 2026


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

>From 788e499c416eb56bcb8e1016e7e141bef5028629 Mon Sep 17 00:00:00 2001
From: Rahul Reddy Chamala <rachamal at meta.com>
Date: Fri, 13 Feb 2026 11:03:29 -0800
Subject: [PATCH] Add ScriptedSymbolLocator plugin with   per-target
 registration

---
 lldb/bindings/python/CMakeLists.txt           |   1 +
 lldb/bindings/python/python-wrapper.swig      |  13 +
 .../use/tutorials/scripted-symbol-locator.md  | 163 +++++++++
 .../templates/scripted_symbol_locator.py      | 220 ++++++++++++
 lldb/include/lldb/API/SBFileSpec.h            |   5 +
 lldb/include/lldb/API/SBTarget.h              |  41 ++-
 lldb/include/lldb/Core/PluginManager.h        |   4 +
 .../ScriptedSymbolLocatorInterface.h          |  56 +++
 .../lldb/Interpreter/ScriptInterpreter.h      |  33 +-
 lldb/include/lldb/Symbol/LineEntry.h          |   2 +-
 lldb/include/lldb/Target/Target.h             |  24 ++
 lldb/include/lldb/lldb-forward.h              |   3 +
 lldb/include/lldb/lldb-private-interfaces.h   |   2 +
 lldb/source/API/SBTarget.cpp                  |  50 ++-
 lldb/source/Commands/CommandObjectTarget.cpp  | 326 +++++++++++++-----
 lldb/source/Core/Module.cpp                   |   2 +-
 lldb/source/Core/PluginManager.cpp            |  24 +-
 lldb/source/Interpreter/ScriptInterpreter.cpp |   7 +
 .../Python/Interfaces/CMakeLists.txt          |   3 +-
 .../ScriptInterpreterPythonInterfaces.cpp     |   2 +
 .../ScriptInterpreterPythonInterfaces.h       |   1 +
 .../Interfaces/ScriptedPythonInterface.cpp    |  13 +
 .../Interfaces/ScriptedPythonInterface.h      |  83 +++++
 .../ScriptedSymbolLocatorPythonInterface.cpp  | 136 ++++++++
 .../ScriptedSymbolLocatorPythonInterface.h    |  71 ++++
 .../Python/SWIGPythonBridge.h                 |   1 +
 .../Python/ScriptInterpreterPython.cpp        |   5 +
 .../Python/ScriptInterpreterPythonImpl.h      |   3 +
 .../Plugins/SymbolLocator/CMakeLists.txt      |   1 +
 .../Debuginfod/SymbolLocatorDebuginfod.cpp    |   2 +-
 .../SymbolLocator/Scripted/CMakeLists.txt     |  13 +
 .../Scripted/SymbolLocatorScripted.cpp        | 202 +++++++++++
 .../Scripted/SymbolLocatorScripted.h          |  55 +++
 lldb/source/Symbol/LineEntry.cpp              |  22 +-
 lldb/source/Target/StackFrame.cpp             |   2 +-
 lldb/source/Target/StackFrameList.cpp         |   2 +-
 lldb/source/Target/Target.cpp                 | 195 +++++++----
 lldb/source/Target/ThreadPlanStepRange.cpp    |   2 +-
 .../scripted_symbol_locator/Makefile          |   9 +
 .../TestScriptedSymbolLocator.py              | 195 +++++++++++
 .../scripted_symbol_locator/main.c            |   7 +
 .../scripted_symbol_locator/source_locator.py |  74 ++++
 .../Python/PythonTestSuite.cpp                |   5 +
 43 files changed, 1871 insertions(+), 209 deletions(-)
 create mode 100644 lldb/docs/use/tutorials/scripted-symbol-locator.md
 create mode 100644 lldb/examples/python/templates/scripted_symbol_locator.py
 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
 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
 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..058b3ceb9b038 100644
--- a/lldb/bindings/python/CMakeLists.txt
+++ b/lldb/bindings/python/CMakeLists.txt
@@ -116,6 +116,7 @@ function(finish_swig_python swig_target lldb_python_bindings_dir lldb_python_tar
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_platform.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/operating_system.py"
     "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_thread_plan.py"
+    "${LLDB_SOURCE_DIR}/examples/python/templates/scripted_symbol_locator.py"
     )
 
   if(APPLE)
diff --git a/lldb/bindings/python/python-wrapper.swig b/lldb/bindings/python/python-wrapper.swig
index bf59569920470..3cf3407be910b 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -583,6 +583,19 @@ void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyOb
   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_CastPyObjectToSBFrameList(PyObject *data) {
   lldb::SBFrameList *sb_ptr = NULL;
 
diff --git a/lldb/docs/use/tutorials/scripted-symbol-locator.md b/lldb/docs/use/tutorials/scripted-symbol-locator.md
new file mode 100644
index 0000000000000..33147edbd896e
--- /dev/null
+++ b/lldb/docs/use/tutorials/scripted-symbol-locator.md
@@ -0,0 +1,163 @@
+# Scripted Symbol Locator Tutorial
+
+The **Scripted Symbol Locator** lets you write a Python class that tells LLDB
+where to find executables, symbol files, and 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
+
+   class MyLocator:
+       def __init__(self, exe_ctx, args):
+           self.cache_dir = None
+           if args.IsValid():
+               d = 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 path, 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 candidate
+           return None
+   ```
+
+2. **Import the script and register the locator on a target:**
+
+   ```
+   (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 that target,
+   your `locate_source_file` method will be called automatically.
+
+## Available Methods
+
+Your locator class can implement any combination of these methods. All are
+optional except `__init__` and `locate_source_file` (which is the abstract
+method that must be present).
+
+| Method | Called When |
+|--------|------------|
+| `locate_source_file(module, path)` | LLDB resolves a source file path in debug info |
+| `locate_executable_object_file(module_spec)` | LLDB needs the binary for a module |
+| `locate_executable_symbol_file(module_spec, search_paths)` | LLDB needs separate debug symbols |
+| `download_object_and_symbol_file(module_spec, force, copy)` | Last-resort download from a remote source |
+
+### 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[str]:
+    ...
+
+def locate_executable_object_file(
+        self, module_spec: lldb.SBModuleSpec) -> Optional[str]:
+    ...
+
+def locate_executable_symbol_file(
+        self, module_spec: lldb.SBModuleSpec,
+        default_search_paths: list) -> Optional[str]:
+    ...
+
+def download_object_and_symbol_file(
+        self, module_spec: lldb.SBModuleSpec,
+        force_lookup: bool, copy_executable: bool) -> bool:
+    ...
+```
+
+## Per-Target Registration
+
+The scripted symbol locator is registered **per target**. Different targets
+can use different locator classes or different arguments.
+
+```
+(lldb) target select 0
+(lldb) target symbols scripted register -C my_locator.MyLocator \
+           -k cache_dir -v /cache/project-a
+
+(lldb) target select 1
+(lldb) target symbols scripted register -C my_locator.MyLocator \
+           -k cache_dir -v /cache/project-b
+```
+
+### Commands
+
+| Command | Description |
+|---------|-------------|
+| `target symbols scripted register -C <class> [-k <key> -v <value> ...]` | Register a locator |
+| `target symbols scripted clear` | Remove the locator from the current target |
+| `target symbols scripted info` | Show the current locator class |
+
+### SB API
+
+You can also register locators programmatically:
+
+```python
+import lldb
+
+error = target.RegisterScriptedSymbolLocator(
+    "my_locator.MyLocator", args)
+# args is an SBStructuredData dictionary
+
+target.ClearScriptedSymbolLocator()
+```
+
+## Caching
+
+Source file resolutions are cached per `(module UUID, source file path)` pair
+within each target. The cache is cleared when:
+
+- A new locator is registered (via `register`)
+- The locator is cleared (via `clear`)
+
+This means your `locate_source_file` method is called at most once per
+unique `(UUID, path)` combination.
+
+## 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 extracting the target and args from the execution
+context. See `lldb/examples/python/templates/scripted_symbol_locator.py`
+for the full template with docstrings.
+
+## Listing Scripting Extensions
+
+To see all registered scripting extensions (including symbol locators):
+
+```
+(lldb) scripting extension list
+```
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..d9b956ff6a3d1
--- /dev/null
+++ b/lldb/examples/python/templates/scripted_symbol_locator.py
@@ -0,0 +1,220 @@
+from abc import ABCMeta, abstractmethod
+import os
+
+import lldb
+
+
+class ScriptedSymbolLocator(metaclass=ABCMeta):
+    """
+    The base class for a scripted symbol locator.
+
+    Most of the base class methods are optional and return ``None`` to fall
+    through to LLDB's default resolution. Override only the methods you need.
+
+    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.
+            args (lldb.SBStructuredData): A Dictionary holding arbitrary
+                key/value pairs used by the scripted symbol locator.
+        """
+        target = None
+        self.target = None
+        self.args = None
+        if isinstance(exe_ctx, lldb.SBExecutionContext):
+            target = exe_ctx.target
+        if isinstance(target, lldb.SBTarget) and target.IsValid():
+            self.target = target
+            self.dbg = target.GetDebugger()
+        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, 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:
+            str: The resolved file path, or None to fall through to
+                LLDB's default source resolution.
+        """
+        return None
+
+    def locate_executable_object_file(self, module_spec):
+        """Locate the executable (object) file for a given module.
+
+        Called when LLDB needs to find the binary for a module during
+        target creation or module loading. For example, when loading a
+        minidump, LLDB calls this for each shared library referenced
+        in the dump.
+
+        Args:
+            module_spec (lldb.SBModuleSpec): The module specification
+                containing the UUID, file path, architecture, and other
+                search criteria.
+
+        Returns:
+            str: The path to the located executable, or None to fall
+                through to LLDB's default search.
+        """
+        return None
+
+    def locate_executable_symbol_file(self, module_spec, default_search_paths):
+        """Locate the symbol file for a given module.
+
+        Called when LLDB needs to find separate debug symbols (e.g.,
+        ``.dSYM`` bundles on macOS, ``.debug`` files on Linux, ``.dwp``
+        files for split DWARF) for a module.
+
+        Args:
+            module_spec (lldb.SBModuleSpec): The module specification
+                containing the UUID and file path to search for.
+            default_search_paths (list): A list of default search paths
+                to check.
+
+        Returns:
+            str: The path to the located symbol file, or None to fall
+                through to LLDB's default search.
+        """
+        return None
+
+    def download_object_and_symbol_file(
+        self, module_spec, force_lookup, copy_executable
+    ):
+        """Download both the object file and symbol file for a module.
+
+        Called when LLDB needs to download a binary and its debug symbols
+        from a remote source (e.g., a symbol server, build artifact
+        store, or cloud storage). This is the last method called in the
+        resolution chain, typically as a fallback when local lookups
+        fail.
+
+        Args:
+            module_spec (lldb.SBModuleSpec): The module specification
+                containing the UUID and file path to download.
+            force_lookup (bool): If True, skip any cached results and
+                force a fresh lookup.
+            copy_executable (bool): If True, copy the executable to a
+                local path.
+
+        Returns:
+            bool: True if the download succeeded, False otherwise.
+        """
+        return False
+
+
+class LocalCacheSymbolLocator(ScriptedSymbolLocator):
+    """Example locator that resolves files from a local cache directory.
+
+    Demonstrates how to subclass ``ScriptedSymbolLocator`` to implement
+    custom symbol and source file resolution. This locator looks up files
+    in a local directory structure organized by build ID (UUID)::
+
+        <cache_dir>/
+            <uuid>/
+                <binary_name>
+                <binary_name>.debug
+                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:
+      - Executables:   ``<cache_dir>/<uuid>/<filename>``
+      - Symbol files:  ``<cache_dir>/<uuid>/<filename>.debug``
+      - 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 path
+        return None
+
+    def locate_executable_object_file(self, module_spec):
+        """Look up executables under ``<cache_dir>/<uuid>/``."""
+        uuid_str = module_spec.GetUUIDString()
+        filename = os.path.basename(module_spec.GetFileSpec().GetFilename() or "")
+        if not filename:
+            return None
+        path = self._get_cache_path(uuid_str, filename)
+        if path and os.path.exists(path):
+            return path
+        return None
+
+    def locate_executable_symbol_file(self, module_spec, default_search_paths):
+        """Look up debug symbol files under ``<cache_dir>/<uuid>/``."""
+        uuid_str = module_spec.GetUUIDString()
+        filename = os.path.basename(module_spec.GetFileSpec().GetFilename() or "")
+        if not filename:
+            return None
+        debug_path = self._get_cache_path(uuid_str, filename + ".debug")
+        if debug_path and os.path.exists(debug_path):
+            return debug_path
+        return None
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/SBTarget.h b/lldb/include/lldb/API/SBTarget.h
index dd2cf59b831da..ab10fc30ed352 100644
--- a/lldb/include/lldb/API/SBTarget.h
+++ b/lldb/include/lldb/API/SBTarget.h
@@ -684,15 +684,14 @@ class LLDB_API SBTarget {
       const char *symbol_name,
       uint32_t
           name_type_mask, // Logical OR one or more FunctionNameType enum bits
-      const SBFileSpecList &module_list,
-      const SBFileSpecList &comp_unit_list);
+      const SBFileSpecList &module_list, const SBFileSpecList &comp_unit_list);
 
   lldb::SBBreakpoint BreakpointCreateByName(
       const char *symbol_name,
       uint32_t
           name_type_mask, // Logical OR one or more FunctionNameType enum bits
-      lldb::LanguageType symbol_language,
-      const SBFileSpecList &module_list, const SBFileSpecList &comp_unit_list);
+      lldb::LanguageType symbol_language, const SBFileSpecList &module_list,
+      const SBFileSpecList &comp_unit_list);
 
   lldb::SBBreakpoint BreakpointCreateByName(
       const char *symbol_name,
@@ -729,23 +728,21 @@ class LLDB_API SBTarget {
       const char *symbol_name[], uint32_t num_names,
       uint32_t
           name_type_mask, // Logical OR one or more FunctionNameType enum bits
-      const SBFileSpecList &module_list,
-      const SBFileSpecList &comp_unit_list);
+      const SBFileSpecList &module_list, const SBFileSpecList &comp_unit_list);
 
   lldb::SBBreakpoint BreakpointCreateByNames(
       const char *symbol_name[], uint32_t num_names,
       uint32_t
           name_type_mask, // Logical OR one or more FunctionNameType enum bits
-      lldb::LanguageType symbol_language,
-      const SBFileSpecList &module_list, const SBFileSpecList &comp_unit_list);
+      lldb::LanguageType symbol_language, const SBFileSpecList &module_list,
+      const SBFileSpecList &comp_unit_list);
 
   lldb::SBBreakpoint BreakpointCreateByNames(
       const char *symbol_name[], uint32_t num_names,
       uint32_t
           name_type_mask, // Logical OR one or more FunctionNameType enum bits
-      lldb::LanguageType symbol_language,
-      lldb::addr_t offset, const SBFileSpecList &module_list,
-      const SBFileSpecList &comp_unit_list);
+      lldb::LanguageType symbol_language, lldb::addr_t offset,
+      const SBFileSpecList &module_list, const SBFileSpecList &comp_unit_list);
 #endif
 
   lldb::SBBreakpoint BreakpointCreateByRegex(const char *symbol_name_regex,
@@ -804,10 +801,8 @@ class LLDB_API SBTarget {
   ///     An SBBreakpoint that will set locations based on the logic in the
   ///     resolver's search callback.
   lldb::SBBreakpoint BreakpointCreateFromScript(
-      const char *class_name,
-      SBStructuredData &extra_args,
-      const SBFileSpecList &module_list,
-      const SBFileSpecList &file_list,
+      const char *class_name, SBStructuredData &extra_args,
+      const SBFileSpecList &module_list, const SBFileSpecList &file_list,
       bool request_hardware = false);
 
   /// Read breakpoints from source_file and return the newly created
@@ -1002,6 +997,22 @@ class LLDB_API SBTarget {
   ///     An error if a Trace already exists or the trace couldn't be created.
   lldb::SBTrace CreateTrace(SBError &error);
 
+  /// Register a scripted symbol locator for this target.
+  ///
+  /// \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 the scripted symbol locator for this target.
+  void ClearScriptedSymbolLocator();
+
   lldb::SBMutex GetAPIMutex() const;
 
   /// Register a scripted frame provider for this target.
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/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
new file mode 100644
index 0000000000000..52e1c9855df56
--- /dev/null
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
@@ -0,0 +1,56 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Core/StructuredDataImpl.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<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec, Status &error) {
+    return {};
+  }
+
+  virtual std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths,
+                             Status &error) {
+    return {};
+  }
+
+  virtual bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                           Status &error, bool force_lookup,
+                                           bool copy_executable) {
+    return false;
+  }
+
+  virtual std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file, Status &error) {
+    return {};
+  }
+};
+} // 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..af1f5b4ec0d53 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -16,6 +16,7 @@
 #include "lldb/API/SBError.h"
 #include "lldb/API/SBEvent.h"
 #include "lldb/API/SBExecutionContext.h"
+#include "lldb/API/SBFileSpec.h"
 #include "lldb/API/SBFrameList.h"
 #include "lldb/API/SBLaunchInfo.h"
 #include "lldb/API/SBMemoryRegionInfo.h"
@@ -33,6 +34,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"
@@ -253,9 +255,9 @@ class ScriptInterpreter : public PluginInterface {
     return StructuredData::GenericSP();
   }
 
-  virtual lldb::ValueObjectListSP GetRecognizedArguments(
-      const StructuredData::ObjectSP &implementor,
-      lldb::StackFrameSP frame_sp) {
+  virtual lldb::ValueObjectListSP
+  GetRecognizedArguments(const StructuredData::ObjectSP &implementor,
+                         lldb::StackFrameSP frame_sp) {
     return lldb::ValueObjectListSP();
   }
 
@@ -276,8 +278,7 @@ class ScriptInterpreter : public PluginInterface {
   }
 
   virtual Status GenerateFunction(const char *signature,
-                                  const StringList &input,
-                                  bool is_callback) {
+                                  const StringList &input, bool is_callback) {
     return Status::FromErrorString("not implemented");
   }
 
@@ -398,11 +399,12 @@ class ScriptInterpreter : public PluginInterface {
     return false;
   }
 
-  virtual bool RunScriptBasedParsedCommand(
-      StructuredData::GenericSP impl_obj_sp, Args& args,
-      ScriptedCommandSynchronicity synchronicity,
-      lldb_private::CommandReturnObject &cmd_retobj, Status &error,
-      const lldb_private::ExecutionContext &exe_ctx) {
+  virtual bool
+  RunScriptBasedParsedCommand(StructuredData::GenericSP impl_obj_sp, Args &args,
+                              ScriptedCommandSynchronicity synchronicity,
+                              lldb_private::CommandReturnObject &cmd_retobj,
+                              Status &error,
+                              const lldb_private::ExecutionContext &exe_ctx) {
     return false;
   }
 
@@ -518,8 +520,8 @@ class ScriptInterpreter : public PluginInterface {
 
   virtual llvm::Expected<unsigned>
   GetMaxPositionalArgumentsForCallable(const llvm::StringRef &callable_name) {
-    return llvm::createStringError(
-    llvm::inconvertibleErrorCode(), "Unimplemented function");
+    return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                   "Unimplemented function");
   }
 
   static std::string LanguageToString(lldb::ScriptLanguage language);
@@ -545,6 +547,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() {
     return {};
@@ -606,6 +613,8 @@ class ScriptInterpreter : public PluginInterface {
   lldb::ExecutionContextRefSP GetOpaqueTypeFromSBExecutionContext(
       const lldb::SBExecutionContext &exe_ctx) const;
 
+  FileSpec GetOpaqueTypeFromSBFileSpec(const lldb::SBFileSpec &file_spec) const;
+
   lldb::StackFrameListSP
   GetOpaqueTypeFromSBFrameList(const lldb::SBFrameList &exe_ctx) const;
 
diff --git a/lldb/include/lldb/Symbol/LineEntry.h b/lldb/include/lldb/Symbol/LineEntry.h
index adf2e989e3e34..e023eda6d89a4 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, const Address &address);
 
   /// Helper to access the file.
   const FileSpec &GetFile() const { return file_sp->GetSpecOnly(); }
diff --git a/lldb/include/lldb/Target/Target.h b/lldb/include/lldb/Target/Target.h
index 4f5b022765f9e..d591f1cbc4d9c 100644
--- a/lldb/include/lldb/Target/Target.h
+++ b/lldb/include/lldb/Target/Target.h
@@ -13,6 +13,7 @@
 #include <map>
 #include <memory>
 #include <string>
+#include <unordered_map>
 #include <vector>
 
 #include "lldb/Breakpoint/BreakpointList.h"
@@ -1705,6 +1706,22 @@ class Target : public std::enable_shared_from_this<Target>,
 
   void SaveScriptedLaunchInfo(lldb_private::ProcessInfo &process_info);
 
+  // Scripted symbol locator per-target registration.
+  Status RegisterScriptedSymbolLocator(llvm::StringRef class_name,
+                                       StructuredData::DictionarySP args_sp);
+  void ClearScriptedSymbolLocator();
+  lldb::ScriptedSymbolLocatorInterfaceSP GetScriptedSymbolLocatorInterface();
+  llvm::StringRef GetScriptedSymbolLocatorClassName() const {
+    return m_scripted_symbol_locator_class_name;
+  }
+
+  /// Look up a previously cached source file resolution result.
+  /// Returns true if a cached entry exists (even if the result is nullopt).
+  bool LookupScriptedSourceFileCache(const std::string &key,
+                                     std::optional<FileSpec> &result) const;
+  void InsertScriptedSourceFileCache(const std::string &key,
+                                     const std::optional<FileSpec> &result);
+
   /// Add a signal for the target.  This will get copied over to the process
   /// if the signal exists on that target.  Only the values with Yes and No are
   /// set, Calculate values will be ignored.
@@ -1843,6 +1860,13 @@ class Target : public std::enable_shared_from_this<Target>,
   /// signals you will have.
   llvm::StringMap<DummySignalValues> m_dummy_signals;
 
+  // Per-target scripted symbol locator.
+  std::string m_scripted_symbol_locator_class_name;
+  StructuredData::DictionarySP m_scripted_symbol_locator_args_sp;
+  lldb::ScriptedSymbolLocatorInterfaceSP m_scripted_symbol_locator_interface_sp;
+  std::unordered_map<std::string, std::optional<FileSpec>>
+      m_scripted_source_file_cache;
+
   static void ImageSearchPathsChanged(const PathMappingList &path_list,
                                       void *baton);
 
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/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/API/SBTarget.cpp b/lldb/source/API/SBTarget.cpp
index 99dfbb3fd9bce..81ae4fdfaec3c 100644
--- a/lldb/source/API/SBTarget.cpp
+++ b/lldb/source/API/SBTarget.cpp
@@ -541,9 +541,8 @@ lldb::SBProcess SBTarget::ConnectRemote(SBListener &listener, const char *url,
   if (TargetSP target_sp = GetSP()) {
     std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
     if (listener.IsValid())
-      process_sp =
-          target_sp->CreateProcess(listener.m_opaque_sp, plugin_name, nullptr,
-                                   true);
+      process_sp = target_sp->CreateProcess(listener.m_opaque_sp, plugin_name,
+                                            nullptr, true);
     else
       process_sp = target_sp->CreateProcess(
           target_sp->GetDebugger().GetListener(), plugin_name, nullptr, true);
@@ -1042,7 +1041,7 @@ SBTarget::BreakpointCreateForException(lldb::LanguageType language,
     std::lock_guard<std::recursive_mutex> guard(target_sp->GetAPIMutex());
     const bool hardware = false;
     sb_bp = target_sp->CreateExceptionBreakpoint(language, catch_bp, throw_bp,
-                                                  hardware);
+                                                 hardware);
   }
 
   return sb_bp;
@@ -1061,14 +1060,9 @@ lldb::SBBreakpoint SBTarget::BreakpointCreateFromScript(
     Status error;
 
     StructuredData::ObjectSP obj_sp = extra_args.m_impl_up->GetObjectSP();
-    sb_bp =
-        target_sp->CreateScriptedBreakpoint(class_name,
-                                            module_list.get(),
-                                            file_list.get(),
-                                            false, /* internal */
-                                            request_hardware,
-                                            obj_sp,
-                                            &error);
+    sb_bp = target_sp->CreateScriptedBreakpoint(
+        class_name, module_list.get(), file_list.get(), false, /* internal */
+        request_hardware, obj_sp, &error);
   }
 
   return sb_bp;
@@ -2200,7 +2194,7 @@ SBError SBTarget::SetModuleLoadAddress(lldb::SBModule module,
 }
 
 SBError SBTarget::SetModuleLoadAddress(lldb::SBModule module,
-                                               uint64_t slide_offset) {
+                                       uint64_t slide_offset) {
 
   SBError sb_error;
 
@@ -2432,6 +2426,36 @@ lldb::SBTrace SBTarget::CreateTrace(lldb::SBError &error) {
   return SBTrace();
 }
 
+lldb::SBError
+SBTarget::RegisterScriptedSymbolLocator(const char *class_name,
+                                        lldb::SBStructuredData &args) {
+  LLDB_INSTRUMENT_VA(this, class_name, args);
+
+  lldb::SBError sb_error;
+  TargetSP target_sp = GetSP();
+  if (!target_sp) {
+    sb_error.SetErrorString("invalid target");
+    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 = target_sp->RegisterScriptedSymbolLocator(class_name, args_sp);
+  if (error.Fail())
+    sb_error.SetErrorString(error.AsCString());
+  return sb_error;
+}
+
+void SBTarget::ClearScriptedSymbolLocator() {
+  LLDB_INSTRUMENT_VA(this);
+
+  if (TargetSP target_sp = GetSP())
+    target_sp->ClearScriptedSymbolLocator();
+}
+
 lldb::SBMutex SBTarget::GetAPIMutex() const {
   LLDB_INSTRUMENT_VA(this);
 
diff --git a/lldb/source/Commands/CommandObjectTarget.cpp b/lldb/source/Commands/CommandObjectTarget.cpp
index 59ccf390dea31..c901ecee29289 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -71,7 +71,6 @@
 #include "llvm/Support/FileSystem.h"
 #include "llvm/Support/FormatAdapters.h"
 
-
 using namespace lldb;
 using namespace lldb_private;
 
@@ -370,9 +369,10 @@ class CommandObjectTargetCreate : public CommandObjectParsed {
                                    "when debugging on the host.");
                 return;
               }
-              if (platform_sp->IsConnected() && !platform_sp->GetFileExists(remote_file)) {
+              if (platform_sp->IsConnected() &&
+                  !platform_sp->GetFileExists(remote_file)) {
                 result.AppendError("remote --> local transfer without local "
-                                 "path is not implemented yet");
+                                   "path is not implemented yet");
                 return;
               }
               // Since there's only a remote file, we need to set the executable
@@ -2038,7 +2038,8 @@ class CommandObjectTargetModulesDumpSymtab
           }
           if (INTERRUPT_REQUESTED(GetDebugger(),
                                   "Interrupted in dump all symtabs with {0} "
-                                  "of {1} dumped.", num_dumped, num_modules))
+                                  "of {1} dumped.",
+                                  num_dumped, num_modules))
             break;
 
           num_dumped++;
@@ -2066,9 +2067,10 @@ class CommandObjectTargetModulesDumpSymtab
                 result.GetOutputStream().EOL();
                 result.GetOutputStream().EOL();
               }
-              if (INTERRUPT_REQUESTED(GetDebugger(),
-                    "Interrupted in dump symtab list with {0} of {1} dumped.",
-                    num_dumped, num_matches))
+              if (INTERRUPT_REQUESTED(
+                      GetDebugger(),
+                      "Interrupted in dump symtab list with {0} of {1} dumped.",
+                      num_dumped, num_matches))
                 break;
 
               num_dumped++;
@@ -2129,9 +2131,10 @@ class CommandObjectTargetModulesDumpSections
       result.GetOutputStream().Format("Dumping sections for {0} modules.\n",
                                       num_modules);
       for (size_t image_idx = 0; image_idx < num_modules; ++image_idx) {
-        if (INTERRUPT_REQUESTED(GetDebugger(),
-              "Interrupted in dump all sections with {0} of {1} dumped",
-              image_idx, num_modules))
+        if (INTERRUPT_REQUESTED(
+                GetDebugger(),
+                "Interrupted in dump all sections with {0} of {1} dumped",
+                image_idx, num_modules))
           break;
 
         num_dumped++;
@@ -2150,9 +2153,10 @@ class CommandObjectTargetModulesDumpSections
             FindModulesByName(&target, arg_cstr, module_list, true);
         if (num_matches > 0) {
           for (size_t i = 0; i < num_matches; ++i) {
-            if (INTERRUPT_REQUESTED(GetDebugger(),
-                  "Interrupted in dump section list with {0} of {1} dumped.",
-                  i, num_matches))
+            if (INTERRUPT_REQUESTED(
+                    GetDebugger(),
+                    "Interrupted in dump section list with {0} of {1} dumped.",
+                    i, num_matches))
               break;
 
             Module *module = module_list.GetModulePointerAtIndex(i);
@@ -2306,9 +2310,10 @@ class CommandObjectTargetModulesDumpClangAST
       }
 
       for (size_t i = 0; i < num_matches; ++i) {
-        if (INTERRUPT_REQUESTED(GetDebugger(),
-              "Interrupted in dump clang ast list with {0} of {1} dumped.",
-              i, num_matches))
+        if (INTERRUPT_REQUESTED(
+                GetDebugger(),
+                "Interrupted in dump clang ast list with {0} of {1} dumped.", i,
+                num_matches))
           break;
 
         Module *m = module_list.GetModulePointerAtIndex(i);
@@ -2358,9 +2363,10 @@ class CommandObjectTargetModulesDumpSymfile
       result.GetOutputStream().Format(
           "Dumping debug symbols for {0} modules.\n", num_modules);
       for (ModuleSP module_sp : target_modules.ModulesNoLocking()) {
-        if (INTERRUPT_REQUESTED(GetDebugger(), "Interrupted in dumping all "
+        if (INTERRUPT_REQUESTED(GetDebugger(),
+                                "Interrupted in dumping all "
                                 "debug symbols with {0} of {1} modules dumped",
-                                 num_dumped, num_modules))
+                                num_dumped, num_modules))
           break;
 
         if (DumpModuleSymbolFile(result.GetOutputStream(), module_sp.get()))
@@ -2377,9 +2383,10 @@ class CommandObjectTargetModulesDumpSymfile
             FindModulesByName(&target, arg_cstr, module_list, true);
         if (num_matches > 0) {
           for (size_t i = 0; i < num_matches; ++i) {
-            if (INTERRUPT_REQUESTED(GetDebugger(), "Interrupted dumping {0} "
-                                                   "of {1} requested modules",
-                                                   i, num_matches))
+            if (INTERRUPT_REQUESTED(GetDebugger(),
+                                    "Interrupted dumping {0} "
+                                    "of {1} requested modules",
+                                    i, num_matches))
               break;
             Module *module = module_list.GetModulePointerAtIndex(i);
             if (module) {
@@ -2448,8 +2455,8 @@ class CommandObjectTargetModulesDumpLineTable
           for (ModuleSP module_sp : target_modules.ModulesNoLocking()) {
             if (INTERRUPT_REQUESTED(GetDebugger(),
                                     "Interrupted in dump all line tables with "
-                                    "{0} of {1} dumped", num_dumped,
-                                    num_modules))
+                                    "{0} of {1} dumped",
+                                    num_dumped, num_modules))
               break;
 
             if (DumpCompileUnitLineTable(
@@ -3236,81 +3243,79 @@ class CommandObjectTargetModulesList : public CommandObjectParsed {
       return;
     }
 
-      size_t num_modules = 0;
+    size_t num_modules = 0;
 
-      // This locker will be locked on the mutex in module_list_ptr if it is
-      // non-nullptr. Otherwise it will lock the
-      // AllocationModuleCollectionMutex when accessing the global module list
-      // directly.
-      std::unique_lock<std::recursive_mutex> guard(
-          Module::GetAllocationModuleCollectionMutex(), std::defer_lock);
+    // This locker will be locked on the mutex in module_list_ptr if it is
+    // non-nullptr. Otherwise it will lock the
+    // AllocationModuleCollectionMutex when accessing the global module list
+    // directly.
+    std::unique_lock<std::recursive_mutex> guard(
+        Module::GetAllocationModuleCollectionMutex(), std::defer_lock);
 
-      const ModuleList *module_list_ptr = nullptr;
-      const size_t argc = command.GetArgumentCount();
-      if (argc == 0) {
-        if (use_global_module_list) {
-          guard.lock();
-          num_modules = Module::GetNumberAllocatedModules();
-        } else {
-          module_list_ptr = &target.GetImages();
-        }
+    const ModuleList *module_list_ptr = nullptr;
+    const size_t argc = command.GetArgumentCount();
+    if (argc == 0) {
+      if (use_global_module_list) {
+        guard.lock();
+        num_modules = Module::GetNumberAllocatedModules();
       } else {
-        for (const Args::ArgEntry &arg : command) {
-          // Dump specified images (by basename or fullpath)
-          const size_t num_matches = FindModulesByName(
-              &target, arg.c_str(), module_list, use_global_module_list);
-          if (num_matches == 0) {
-            if (argc == 1) {
-              result.AppendErrorWithFormat("no modules found that match '%s'",
-                                           arg.c_str());
-              return;
-            }
+        module_list_ptr = &target.GetImages();
+      }
+    } else {
+      for (const Args::ArgEntry &arg : command) {
+        // Dump specified images (by basename or fullpath)
+        const size_t num_matches = FindModulesByName(
+            &target, arg.c_str(), module_list, use_global_module_list);
+        if (num_matches == 0) {
+          if (argc == 1) {
+            result.AppendErrorWithFormat("no modules found that match '%s'",
+                                         arg.c_str());
+            return;
           }
         }
-
-        module_list_ptr = &module_list;
       }
 
-      std::unique_lock<std::recursive_mutex> lock;
-      if (module_list_ptr != nullptr) {
-        lock =
-            std::unique_lock<std::recursive_mutex>(module_list_ptr->GetMutex());
+      module_list_ptr = &module_list;
+    }
 
-        num_modules = module_list_ptr->GetSize();
-      }
+    std::unique_lock<std::recursive_mutex> lock;
+    if (module_list_ptr != nullptr) {
+      lock =
+          std::unique_lock<std::recursive_mutex>(module_list_ptr->GetMutex());
 
-      if (num_modules > 0) {
-        for (uint32_t image_idx = 0; image_idx < num_modules; ++image_idx) {
-          ModuleSP module_sp;
-          Module *module;
-          if (module_list_ptr) {
-            module_sp = module_list_ptr->GetModuleAtIndexUnlocked(image_idx);
-            module = module_sp.get();
-          } else {
-            module = Module::GetAllocatedModuleAtIndex(image_idx);
-            module_sp = module->shared_from_this();
-          }
+      num_modules = module_list_ptr->GetSize();
+    }
 
-          const size_t indent = strm.Printf("[%3u] ", image_idx);
-          PrintModule(target, module, indent, strm);
-        }
-        result.SetStatus(eReturnStatusSuccessFinishResult);
-      } else {
-        if (argc) {
-          if (use_global_module_list)
-            result.AppendError(
-                "the global module list has no matching modules");
-          else
-            result.AppendError("the target has no matching modules");
+    if (num_modules > 0) {
+      for (uint32_t image_idx = 0; image_idx < num_modules; ++image_idx) {
+        ModuleSP module_sp;
+        Module *module;
+        if (module_list_ptr) {
+          module_sp = module_list_ptr->GetModuleAtIndexUnlocked(image_idx);
+          module = module_sp.get();
         } else {
-          if (use_global_module_list)
-            result.AppendError("the global module list is empty");
-          else
-            result.AppendError(
-                "the target has no associated executable images");
+          module = Module::GetAllocatedModuleAtIndex(image_idx);
+          module_sp = module->shared_from_this();
         }
-        return;
+
+        const size_t indent = strm.Printf("[%3u] ", image_idx);
+        PrintModule(target, module, indent, strm);
+      }
+      result.SetStatus(eReturnStatusSuccessFinishResult);
+    } else {
+      if (argc) {
+        if (use_global_module_list)
+          result.AppendError("the global module list has no matching modules");
+        else
+          result.AppendError("the target has no matching modules");
+      } else {
+        if (use_global_module_list)
+          result.AppendError("the global module list is empty");
+        else
+          result.AppendError("the target has no associated executable images");
       }
+      return;
+    }
   }
 
   void PrintModule(Target &target, Module *module, int indent, Stream &strm) {
@@ -3410,7 +3415,8 @@ class CommandObjectTargetModulesList : public CommandObjectParsed {
           ref_count = module_sp.use_count() - 1;
         }
         if (width)
-          strm.Printf("{%c %*" PRIu64 "}", in_shared_cache, width, (uint64_t)ref_count);
+          strm.Printf("{%c %*" PRIu64 "}", in_shared_cache, width,
+                      (uint64_t)ref_count);
         else
           strm.Printf("{%c %" PRIu64 "}", in_shared_cache, (uint64_t)ref_count);
       } break;
@@ -3543,7 +3549,7 @@ class CommandObjectTargetModulesShowUnwind : public CommandObjectParsed {
 
     int m_type = eLookupTypeInvalid; // Should be a eLookupTypeXXX enum after
                                      // parsing options
-    std::string m_str; // Holds name lookup
+    std::string m_str;               // Holds name lookup
     lldb::addr_t m_addr = LLDB_INVALID_ADDRESS; // Holds the address to lookup
     bool m_cached = true;
   };
@@ -4307,8 +4313,8 @@ class CommandObjectTargetSymbolsAdd : public CommandObjectParsed {
 
     // First extract all module specs from the symbol file
     lldb_private::ModuleSpecList symfile_module_specs;
-    if (ObjectFile::GetModuleSpecifications(module_spec.GetSymbolFileSpec(),
-                                            0, 0, symfile_module_specs)) {
+    if (ObjectFile::GetModuleSpecifications(module_spec.GetSymbolFileSpec(), 0,
+                                            0, symfile_module_specs)) {
       // Now extract the module spec that matches the target architecture
       ModuleSpec target_arch_module_spec;
       ModuleSpec symfile_module_spec;
@@ -4330,8 +4336,8 @@ class CommandObjectTargetSymbolsAdd : public CommandObjectParsed {
         const size_t num_symfile_module_specs = symfile_module_specs.GetSize();
         for (size_t i = 0;
              i < num_symfile_module_specs && matching_modules.IsEmpty(); ++i) {
-          if (symfile_module_specs.GetModuleSpecAtIndex(
-                  i, symfile_module_spec)) {
+          if (symfile_module_specs.GetModuleSpecAtIndex(i,
+                                                        symfile_module_spec)) {
             if (symfile_module_spec.GetUUID().IsValid()) {
               // It has a UUID.  Look for this UUID in the target modules.
               ModuleSpec symfile_uuid_module_spec;
@@ -4691,6 +4697,133 @@ 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 for the current target.",
+            "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 {
+    Target &target = GetTarget();
+
+    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);
+
+    Status error = target.RegisterScriptedSymbolLocator(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' for target.\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 the scripted symbol locator for the current target.",
+            "target symbols scripted clear") {}
+
+  ~CommandObjectTargetSymbolsScriptedClear() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    target.ClearScriptedSymbolLocator();
+    result.AppendMessageWithFormat(
+        "Cleared scripted symbol locator for target.\n");
+    result.SetStatus(eReturnStatusSuccessFinishResult);
+  }
+};
+
+#pragma mark CommandObjectTargetSymbolsScriptedInfo
+
+class CommandObjectTargetSymbolsScriptedInfo : public CommandObjectParsed {
+public:
+  CommandObjectTargetSymbolsScriptedInfo(CommandInterpreter &interpreter)
+      : CommandObjectParsed(
+            interpreter, "target symbols scripted info",
+            "Show the current scripted symbol locator for the target.",
+            "target symbols scripted info") {}
+
+  ~CommandObjectTargetSymbolsScriptedInfo() override = default;
+
+protected:
+  void DoExecute(Args &command, CommandReturnObject &result) override {
+    Target &target = GetTarget();
+    llvm::StringRef class_name = target.GetScriptedSymbolLocatorClassName();
+    if (class_name.empty()) {
+      result.AppendMessageWithFormat(
+          "No scripted symbol locator registered for this target.\n");
+    } else {
+      result.AppendMessageWithFormat("Scripted symbol locator: %s\n",
+                                     class_name.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 +4838,9 @@ class CommandObjectTargetSymbols : public CommandObjectMultiword {
             "target symbols <sub-command> ...") {
     LoadSubCommand(
         "add", CommandObjectSP(new CommandObjectTargetSymbolsAdd(interpreter)));
+    LoadSubCommand(
+        "scripted",
+        CommandObjectSP(new CommandObjectTargetSymbolsScripted(interpreter)));
   }
 
   ~CommandObjectTargetSymbols() override = default;
@@ -4999,10 +5135,10 @@ Filter Options:
     m_stop_hook_sp.reset();
 
     Target &target = GetTarget();
-    Target::StopHookSP new_hook_sp =
-        target.CreateStopHook(m_python_class_options.GetName().empty() ?
-                               Target::StopHook::StopHookKind::CommandBased
-                               : Target::StopHook::StopHookKind::ScriptBased);
+    Target::StopHookSP new_hook_sp = target.CreateStopHook(
+        m_python_class_options.GetName().empty()
+            ? Target::StopHook::StopHookKind::CommandBased
+            : Target::StopHook::StopHookKind::ScriptBased);
 
     //  First step, make the specifier.
     std::unique_ptr<SymbolContextSpecifier> specifier_up;
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 659190833c20d..fc17daf86a901 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);
     }
 
     // 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..5e6bfe343b6a1 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,13 @@ 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 +1596,21 @@ 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/Interpreter/ScriptInterpreter.cpp b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 5e8478c2670bb..5066d744b150f 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -158,6 +158,13 @@ ScriptInterpreter::GetOpaqueTypeFromSBExecutionContext(
   return exe_ctx.m_exe_ctx_sp;
 }
 
+FileSpec ScriptInterpreter::GetOpaqueTypeFromSBFileSpec(
+    const lldb::SBFileSpec &file_spec) const {
+  if (file_spec.m_opaque_up)
+    return *file_spec.m_opaque_up;
+  return {};
+}
+
 lldb::StackFrameListSP ScriptInterpreter::GetOpaqueTypeFromSBFrameList(
     const lldb::SBFrameList &frame_list) const {
   return frame_list.m_opaque_sp;
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
index 50569cdefaafa..ddffff08095fb 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
@@ -26,6 +26,7 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces PLUGIN
   ScriptedFrameProviderPythonInterface.cpp
   ScriptedPlatformPythonInterface.cpp
   ScriptedProcessPythonInterface.cpp
+  ScriptedSymbolLocatorPythonInterface.cpp
   ScriptedPythonInterface.cpp
   ScriptedStopHookPythonInterface.cpp
   ScriptedBreakpointPythonInterface.cpp
@@ -42,5 +43,3 @@ add_lldb_library(lldbPluginScriptInterpreterPythonInterfaces PLUGIN
     ${Python3_LIBRARIES}
     ${LLDB_LIBEDIT_LIBS}
   )
-
-
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
index f6c707b2bd168..a6225c5da4747 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -31,6 +31,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedStopHookPythonInterface::Initialize();
   ScriptedBreakpointPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
+  ScriptedSymbolLocatorPythonInterface::Initialize();
   ScriptedFrameProviderPythonInterface::Initialize();
 }
 
@@ -41,6 +42,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedStopHookPythonInterface::Terminate();
   ScriptedBreakpointPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
+  ScriptedSymbolLocatorPythonInterface::Terminate();
   ScriptedFrameProviderPythonInterface::Terminate();
 }
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
index 721902ec1e253..52827d01b2495 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
@@ -22,6 +22,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 f5fd8b2d2d802..9fcbc024d764f 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -18,6 +18,7 @@
 #include "../ScriptInterpreterPythonImpl.h"
 #include "ScriptedPythonInterface.h"
 #include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Utility/FileSpec.h"
 #include "lldb/ValueObject/ValueObjectList.h"
 #include <optional>
 
@@ -311,4 +312,16 @@ ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
   return out;
 }
 
+template <>
+FileSpec ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpec>(
+    python::PythonObject &p, Status &error) {
+  if (lldb::SBFileSpec *sb_file_spec = reinterpret_cast<lldb::SBFileSpec *>(
+          python::LLDBSWIGPython_CastPyObjectToSBFileSpec(p.get())))
+    return m_interpreter.GetOpaqueTypeFromSBFileSpec(*sb_file_spec);
+  error = Status::FromErrorString(
+      "Couldn't cast lldb::SBFileSpec to lldb_private::FileSpec.");
+
+  return {};
+}
+
 #endif
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 4aadee584b2e2..2874d22d247ac 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -17,9 +17,15 @@
 #include <type_traits>
 #include <utility>
 
+#include "lldb/Core/ModuleSpec.h"
 #include "lldb/Host/Config.h"
 #include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
 #include "lldb/Utility/DataBufferHeap.h"
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/FileSpecList.h"
+
+#include "lldb/API/SBFileSpec.h"
+#include "lldb/API/SBModuleSpec.h"
 
 #include "../PythonDataObjects.h"
 #include "../SWIGPythonBridge.h"
@@ -632,6 +638,10 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(lldb::ModuleSP arg) {
+    return python::SWIGBridge::ToSWIGWrapper(arg);
+  }
+
   python::PythonObject Transform(Event *arg) {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
@@ -660,6 +670,61 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(const ModuleSpec &arg) {
+    // Build an SBModuleSpec using public API setters since the constructor
+    // from ModuleSpec is private.
+    lldb::SBModuleSpec sb_module_spec;
+
+    const UUID &uuid = arg.GetUUID();
+    if (uuid.IsValid())
+      sb_module_spec.SetUUIDBytes(uuid.GetBytes().data(),
+                                  uuid.GetBytes().size());
+
+    const FileSpec &file = arg.GetFileSpec();
+    if (file)
+      sb_module_spec.SetFileSpec(
+          lldb::SBFileSpec(file.GetPath().c_str(), false));
+
+    const FileSpec &platform_file = arg.GetPlatformFileSpec();
+    if (platform_file)
+      sb_module_spec.SetPlatformFileSpec(
+          lldb::SBFileSpec(platform_file.GetPath().c_str(), false));
+
+    const FileSpec &symbol_file = arg.GetSymbolFileSpec();
+    if (symbol_file)
+      sb_module_spec.SetSymbolFileSpec(
+          lldb::SBFileSpec(symbol_file.GetPath().c_str(), false));
+
+    const ArchSpec &arch = arg.GetArchitecture();
+    if (arch.IsValid())
+      sb_module_spec.SetTriple(arch.GetTriple().getTriple().c_str());
+
+    ConstString object_name = arg.GetObjectName();
+    if (object_name)
+      sb_module_spec.SetObjectName(object_name.GetCString());
+
+    sb_module_spec.SetObjectOffset(arg.GetObjectOffset());
+    sb_module_spec.SetObjectSize(arg.GetObjectSize());
+
+    return python::SWIGBridge::ToSWIGWrapper(
+        std::make_unique<lldb::SBModuleSpec>(sb_module_spec));
+  }
+
+  python::PythonObject Transform(const FileSpecList &arg) {
+    python::PythonList py_list(arg.GetSize());
+    for (size_t i = 0; i < arg.GetSize(); i++) {
+      const FileSpec &fs = arg.GetFileSpecAtIndex(i);
+      py_list.SetItemAtIndex(i, python::SWIGBridge::ToSWIGWrapper(
+                                    std::make_unique<lldb::SBFileSpec>(
+                                        fs.GetPath().c_str(), false)));
+    }
+    return py_list;
+  }
+
+  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!
@@ -671,6 +736,20 @@ class ScriptedPythonInterface : virtual public ScriptedInterface {
     original_arg = ExtractValueFromPythonObject<T>(transformed_arg, error);
   }
 
+  // Read-only types: Python doesn't modify these, so reverse transform is a
+  // no-op.
+  void ReverseTransform(ModuleSpec &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {}
+
+  void ReverseTransform(FileSpecList &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {}
+
+  void ReverseTransform(std::string &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {}
+
+  void ReverseTransform(lldb::ModuleSP &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {}
+
   void ReverseTransform(bool &original_arg,
                         python::PythonObject transformed_arg, Status &error) {
     python::PythonBoolean boolean_arg = python::PythonBoolean(
@@ -828,6 +907,10 @@ lldb::ValueObjectListSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
     python::PythonObject &p, Status &error);
 
+template <>
+FileSpec ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpec>(
+    python::PythonObject &p, Status &error);
+
 } // namespace lldb_private
 
 #endif // LLDB_ENABLE_PYTHON
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..d913615c7085e
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
@@ -0,0 +1,136 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/PluginManager.h"
+#include "lldb/Host/Config.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Utility/Log.h"
+#include "lldb/lldb-enumerations.h"
+
+#if LLDB_ENABLE_PYTHON
+
+// 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<ModuleSpec>
+ScriptedSymbolLocatorPythonInterface::LocateExecutableObjectFile(
+    const ModuleSpec &module_spec, Status &error) {
+  // Make a copy so Dispatch's ReverseTransform can operate on a mutable value.
+  ModuleSpec ms_copy(module_spec);
+  StructuredData::ObjectSP obj =
+      Dispatch("locate_executable_object_file", error, ms_copy);
+
+  if (!obj || error.Fail())
+    return {};
+
+  llvm::StringRef value = obj->GetStringValue();
+  if (value.empty())
+    return {};
+
+  ModuleSpec result_spec(module_spec);
+  result_spec.GetFileSpec().SetPath(value);
+  return result_spec;
+}
+
+std::optional<FileSpec>
+ScriptedSymbolLocatorPythonInterface::LocateExecutableSymbolFile(
+    const ModuleSpec &module_spec, const FileSpecList &default_search_paths,
+    Status &error) {
+  ModuleSpec ms_copy(module_spec);
+  FileSpecList fsl_copy(default_search_paths);
+  StructuredData::ObjectSP obj =
+      Dispatch("locate_executable_symbol_file", error, ms_copy, fsl_copy);
+
+  if (!obj || error.Fail())
+    return {};
+
+  llvm::StringRef value = obj->GetStringValue();
+  if (value.empty())
+    return {};
+
+  FileSpec file_spec;
+  file_spec.SetPath(value);
+  return file_spec;
+}
+
+bool ScriptedSymbolLocatorPythonInterface::DownloadObjectAndSymbolFile(
+    ModuleSpec &module_spec, Status &error, bool force_lookup,
+    bool copy_executable) {
+  StructuredData::ObjectSP obj =
+      Dispatch("download_object_and_symbol_file", error, module_spec,
+               force_lookup, copy_executable);
+
+  if (!obj || error.Fail())
+    return false;
+
+  return obj->GetBooleanValue();
+}
+
+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);
+
+  StructuredData::ObjectSP obj =
+      Dispatch("locate_source_file", error, module_copy, source_path);
+
+  if (!obj || error.Fail())
+    return {};
+
+  llvm::StringRef value = obj->GetStringValue();
+  if (value.empty())
+    return {};
+
+  FileSpec file_spec;
+  file_spec.SetPath(value);
+  return file_spec;
+}
+
+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 = {
+      "SBTarget.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);
+}
+
+#endif // LLDB_ENABLE_PYTHON
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..24d22f354b158
--- /dev/null
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
@@ -0,0 +1,71 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Host/Config.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
+
+#if LLDB_ENABLE_PYTHON
+
+#include "ScriptedPythonInterface.h"
+
+#include <optional>
+
+namespace lldb_private {
+class ScriptedSymbolLocatorPythonInterface
+    : public ScriptedSymbolLocatorInterface,
+      public ScriptedPythonInterface,
+      public PluginInterface {
+public:
+  ScriptedSymbolLocatorPythonInterface(
+      ScriptInterpreterPythonImpl &interpreter);
+
+  llvm::Expected<StructuredData::GenericSP>
+  CreatePluginObject(const 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<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec,
+                             Status &error) override;
+
+  std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths,
+                             Status &error) override;
+
+  bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec, Status &error,
+                                   bool force_lookup,
+                                   bool copy_executable) override;
+
+  std::optional<FileSpec> LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                           const FileSpec &original_source_file,
+                                           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_ENABLE_PYTHON
+#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 7a64d8e91e62c..d62b25d50195b 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -272,6 +272,7 @@ void *LLDBSWIGPython_CastPyObjectToSBValue(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBValueList(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data);
 } // namespace python
 
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 35a772c1454df..1346f496b0e07 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
@@ -1532,6 +1532,11 @@ ScriptInterpreterPythonImpl::CreateScriptedFrameProviderInterface() {
   return std::make_shared<ScriptedFrameProviderPythonInterface>(*this);
 }
 
+ScriptedSymbolLocatorInterfaceSP
+ScriptInterpreterPythonImpl::CreateScriptedSymbolLocatorInterface() {
+  return std::make_shared<ScriptedSymbolLocatorPythonInterface>(*this);
+}
+
 ScriptedThreadPlanInterfaceSP
 ScriptInterpreterPythonImpl::CreateScriptedThreadPlanInterface() {
   return std::make_shared<ScriptedThreadPlanPythonInterface>(*this);
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
index 1eac78e6360f2..60b2fc6106c87 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
@@ -104,6 +104,9 @@ class ScriptInterpreterPythonImpl : public ScriptInterpreterPython {
   lldb::ScriptedFrameProviderInterfaceSP
   CreateScriptedFrameProviderInterface() override;
 
+  lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() override;
+
   lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() override;
 
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/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/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..c3784ccbf739a
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
@@ -0,0 +1,202 @@
+//===----------------------------------------------------------------------===//
+//
+// 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/Debugger.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/PluginManager.h"
+#include "lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h"
+#include "lldb/Target/Target.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);
+}
+
+void SymbolLocatorScripted::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
+
+llvm::StringRef SymbolLocatorScripted::GetPluginDescriptionStatic() {
+  return "Scripted symbol locator plug-in.";
+}
+
+SymbolLocator *SymbolLocatorScripted::CreateInstance() {
+  return new SymbolLocatorScripted();
+}
+
+/// Iterate all debuggers and their targets, calling \p callback for each
+/// target that has a scripted symbol locator registered. The callback
+/// receives the target and its interface. If the callback returns true,
+/// iteration stops early.
+template <typename Callback>
+static void ForEachScriptedTarget(Callback &&callback) {
+  for (size_t di = 0; di < Debugger::GetNumDebuggers(); di++) {
+    DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(di);
+    if (!debugger_sp)
+      continue;
+    TargetList &target_list = debugger_sp->GetTargetList();
+    for (size_t ti = 0; ti < target_list.GetNumTargets(); ti++) {
+      TargetSP target_sp = target_list.GetTargetAtIndex(ti);
+      if (!target_sp)
+        continue;
+      auto interface_sp = target_sp->GetScriptedSymbolLocatorInterface();
+      if (!interface_sp)
+        continue;
+      if (callback(*target_sp, interface_sp))
+        return;
+    }
+  }
+}
+
+std::optional<ModuleSpec> SymbolLocatorScripted::LocateExecutableObjectFile(
+    const ModuleSpec &module_spec) {
+  std::optional<ModuleSpec> result;
+  ForEachScriptedTarget(
+      [&](Target &target,
+          ScriptedSymbolLocatorInterfaceSP &interface_sp) -> bool {
+        Status error;
+        auto located =
+            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) {
+          result = located;
+          return true; // Stop iterating.
+        }
+        return false;
+      });
+  return result;
+}
+
+std::optional<FileSpec> SymbolLocatorScripted::LocateExecutableSymbolFile(
+    const ModuleSpec &module_spec, const FileSpecList &default_search_paths) {
+  std::optional<FileSpec> result;
+  ForEachScriptedTarget(
+      [&](Target &target,
+          ScriptedSymbolLocatorInterfaceSP &interface_sp) -> bool {
+        Status error;
+        auto located = interface_sp->LocateExecutableSymbolFile(
+            module_spec, default_search_paths, error);
+        if (!error.Success()) {
+          Log *log = GetLog(LLDBLog::Symbols);
+          LLDB_LOG(log,
+                   "SymbolLocatorScripted: locate_executable_symbol_file "
+                   "failed: {0}",
+                   error);
+        }
+        if (located) {
+          result = located;
+          return true;
+        }
+        return false;
+      });
+  return result;
+}
+
+bool SymbolLocatorScripted::DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                                        Status &error,
+                                                        bool force_lookup,
+                                                        bool copy_executable) {
+  bool result = false;
+  ForEachScriptedTarget(
+      [&](Target &target,
+          ScriptedSymbolLocatorInterfaceSP &interface_sp) -> bool {
+        bool success = interface_sp->DownloadObjectAndSymbolFile(
+            module_spec, error, force_lookup, copy_executable);
+        if (success) {
+          result = true;
+          return true;
+        }
+        return false;
+      });
+  return result;
+}
+
+std::optional<FileSpec>
+SymbolLocatorScripted::LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                        const FileSpec &original_source_file) {
+  if (!module_sp)
+    return {};
+
+  // Find the target that owns this module.
+  Target *owning_target = nullptr;
+  for (size_t di = 0; di < Debugger::GetNumDebuggers(); di++) {
+    DebuggerSP debugger_sp = Debugger::GetDebuggerAtIndex(di);
+    if (!debugger_sp)
+      continue;
+    TargetList &target_list = debugger_sp->GetTargetList();
+    for (size_t ti = 0; ti < target_list.GetNumTargets(); ti++) {
+      TargetSP target_sp = target_list.GetTargetAtIndex(ti);
+      if (!target_sp)
+        continue;
+      ModuleSP found_module =
+          target_sp->GetImages().FindModule(module_sp.get());
+      if (found_module) {
+        owning_target = target_sp.get();
+        break;
+      }
+    }
+    if (owning_target)
+      break;
+  }
+
+  if (!owning_target)
+    return {};
+
+  auto interface_sp = owning_target->GetScriptedSymbolLocatorInterface();
+  if (!interface_sp)
+    return {};
+
+  // Cache resolved source files to avoid repeated Python calls for the same
+  // (module, source_file) pair.
+  std::string cache_key =
+      module_sp->GetUUID().GetAsString() + ":" + original_source_file.GetPath();
+
+  std::optional<FileSpec> cached;
+  if (owning_target->LookupScriptedSourceFileCache(cache_key, cached))
+    return cached;
+
+  Status error;
+  auto located =
+      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);
+  }
+
+  owning_target->InsertScriptedSourceFileCache(cache_key, located);
+
+  if (located) {
+    Log *log = GetLog(LLDBLog::Symbols);
+    LLDB_LOGF(log,
+              "SymbolLocatorScripted::%s: resolved source file '%s' to '%s'",
+              __FUNCTION__, original_source_file.GetPath().c_str(),
+              located->GetPath().c_str());
+  }
+  return located;
+}
diff --git a/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
new file mode 100644
index 0000000000000..b16bc972c8907
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
@@ -0,0 +1,55 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 llvm::StringRef GetPluginNameStatic() { return "scripted"; }
+  static llvm::StringRef GetPluginDescriptionStatic();
+
+  static lldb_private::SymbolLocator *CreateInstance();
+
+  /// PluginInterface protocol.
+  /// \{
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+  /// \}
+
+  // Locate the executable file given a module specification.
+  static std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec);
+
+  // Locate the symbol file given a module specification.
+  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);
+
+  // Locate the source file given a module and original source file path.
+  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
diff --git a/lldb/source/Symbol/LineEntry.cpp b/lldb/source/Symbol/LineEntry.cpp
index dcfbac8789863..d246b4f82efc8 100644
--- a/lldb/source/Symbol/LineEntry.cpp
+++ b/lldb/source/Symbol/LineEntry.cpp
@@ -7,6 +7,9 @@
 //===----------------------------------------------------------------------===//
 
 #include "lldb/Symbol/LineEntry.h"
+#include "lldb/Core/Address.h"
+#include "lldb/Core/Module.h"
+#include "lldb/Core/PluginManager.h"
 #include "lldb/Symbol/CompileUnit.h"
 #include "lldb/Target/Process.h"
 #include "lldb/Target/Target.h"
@@ -242,8 +245,25 @@ AddressRange LineEntry::GetSameLineContiguousAddressRange(
   return complete_line_range;
 }
 
-void LineEntry::ApplyFileMappings(lldb::TargetSP target_sp) {
+void LineEntry::ApplyFileMappings(lldb::TargetSP target_sp,
+                                  const Address &address) {
   if (target_sp) {
+    // Try to resolve the source file via SymbolLocator plugins (e.g.,
+    // ScriptedSymbolLocator). This allows users to fetch source files
+    // by build ID from remote servers.
+    // Use Address::GetModule() directly to avoid re-entering
+    // ResolveSymbolContextForAddress which would cause infinite recursion.
+    lldb::ModuleSP module_sp = address.GetModule();
+    if (module_sp) {
+      FileSpec resolved = PluginManager::LocateSourceFile(
+          module_sp, original_file_sp->GetSpecOnly());
+      if (resolved) {
+        original_file_sp = std::make_shared<SupportFile>(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 340607e14abed..656e68a9dd511 100644
--- a/lldb/source/Target/StackFrame.cpp
+++ b/lldb/source/Target/StackFrame.cpp
@@ -412,7 +412,7 @@ 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);
         }
       }
     } else {
diff --git a/lldb/source/Target/StackFrameList.cpp b/lldb/source/Target/StackFrameList.cpp
index 4f4b06f30460b..a8649a0eb3ecd 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -562,7 +562,7 @@ 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);
         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/Target.cpp b/lldb/source/Target/Target.cpp
index 07c3653782c6b..97b9c282fe47b 100644
--- a/lldb/source/Target/Target.cpp
+++ b/lldb/source/Target/Target.cpp
@@ -42,6 +42,7 @@
 #include "lldb/Interpreter/OptionGroupWatchpoint.h"
 #include "lldb/Interpreter/OptionValues.h"
 #include "lldb/Interpreter/Property.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/Function.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Symbol/Symbol.h"
@@ -401,7 +402,7 @@ llvm::StringRef Target::GetABIName() const {
   if (!abi_sp)
     abi_sp = ABI::FindPlugin(ProcessSP(), GetArchitecture());
   if (abi_sp)
-      return abi_sp->GetPluginName();
+    return abi_sp->GetPluginName();
   return {};
 }
 
@@ -922,7 +923,7 @@ void Target::ApplyNameToBreakpoints(BreakpointName &bp_name) {
 
 void Target::GetBreakpointNames(std::vector<std::string> &names) {
   names.clear();
-  for (const auto& bp_name_entry : m_breakpoint_names) {
+  for (const auto &bp_name_entry : m_breakpoint_names) {
     names.push_back(bp_name_entry.first.AsCString());
   }
   llvm::sort(names);
@@ -2266,9 +2267,9 @@ size_t Target::ReadStringFromMemory(const Address &addr, char *dst,
   return total_bytes_read;
 }
 
-size_t Target::ReadScalarIntegerFromMemory(const Address &addr, uint32_t byte_size,
-                                           bool is_signed, Scalar &scalar,
-                                           Status &error,
+size_t Target::ReadScalarIntegerFromMemory(const Address &addr,
+                                           uint32_t byte_size, bool is_signed,
+                                           Scalar &scalar, Status &error,
                                            bool force_live_memory) {
   uint64_t uval;
 
@@ -2310,7 +2311,8 @@ int64_t Target::ReadSignedIntegerFromMemory(const Address &addr,
 
 uint64_t Target::ReadUnsignedIntegerFromMemory(const Address &addr,
                                                size_t integer_byte_size,
-                                               uint64_t fail_value, Status &error,
+                                               uint64_t fail_value,
+                                               Status &error,
                                                bool force_live_memory) {
   Scalar scalar;
   if (ReadScalarIntegerFromMemory(addr, integer_byte_size, false, scalar, error,
@@ -2415,7 +2417,7 @@ ModuleSP Target::GetOrCreateModule(const ModuleSpec &orig_module_spec,
                 module_spec.GetFileSpec().GetDirectory(), transformed_dir)) {
           transformed_spec.GetFileSpec().SetDirectory(transformed_dir);
           transformed_spec.GetFileSpec().SetFilename(
-                module_spec.GetFileSpec().GetFilename());
+              module_spec.GetFileSpec().GetFilename());
           transformed_spec.SetTarget(shared_from_this());
           error = ModuleList::GetSharedModule(transformed_spec, module_sp,
                                               &old_modules, &did_create_module);
@@ -2669,9 +2671,8 @@ Target::GetScratchTypeSystems(bool create_on_demand) {
           "Language '{1}' has expression support but no scratch type "
           "system available: {0}",
           Language::GetNameForLanguageType(language));
-    else
-      if (auto ts = *type_system_or_err)
-        scratch_type_systems.push_back(ts);
+    else if (auto ts = *type_system_or_err)
+      scratch_type_systems.push_back(ts);
   }
 
   std::sort(scratch_type_systems.begin(), scratch_type_systems.end());
@@ -2822,11 +2823,10 @@ llvm::Error Target::SetLabel(llvm::StringRef label) {
   for (size_t i = 0; i < targets.GetNumTargets(); i++) {
     TargetSP target_sp = targets.GetTargetAtIndex(i);
     if (target_sp && target_sp->GetLabel() == label) {
-        return llvm::make_error<llvm::StringError>(
-            llvm::formatv(
-                "Cannot use label '{0}' since it's set in target #{1}.", label,
-                i),
-            llvm::inconvertibleErrorCode());
+      return llvm::make_error<llvm::StringError>(
+          llvm::formatv("Cannot use label '{0}' since it's set in target #{1}.",
+                        label, i),
+          llvm::inconvertibleErrorCode());
     }
   }
 
@@ -2884,8 +2884,7 @@ ExpressionResults Target::EvaluateExpression(
   // Only check for persistent variables the expression starts with a '$'
   lldb::ExpressionVariableSP persistent_var_sp;
   if (expr[0] == '$') {
-    auto type_system_or_err =
-            GetScratchTypeSystemForLanguage(eLanguageTypeC);
+    auto type_system_or_err = GetScratchTypeSystemForLanguage(eLanguageTypeC);
     if (auto err = type_system_or_err.takeError()) {
       LLDB_LOG_ERROR(GetLog(LLDBLog::Target), std::move(err),
                      "Unable to get scratch type system");
@@ -3163,7 +3162,8 @@ bool Target::RunStopHooks(bool at_initial_stop) {
   // because breakpoint commands get run before stop hooks, and one of them
   // might have run an expression. You have to ensure you run the stop hooks
   // once per natural stop.
-  uint32_t last_natural_stop = m_process_sp->GetModIDRef().GetLastNaturalStopID();
+  uint32_t last_natural_stop =
+      m_process_sp->GetModIDRef().GetLastNaturalStopID();
   if (last_natural_stop != 0 && m_latest_stop_hook_id == last_natural_stop)
     return false;
 
@@ -3288,8 +3288,7 @@ bool Target::RunStopHooks(bool at_initial_stop) {
 TargetProperties &Target::GetGlobalProperties() {
   // NOTE: intentional leak so we don't crash if global destructor chain gets
   // called as other threads still use the result of this function
-  static TargetProperties *g_settings_ptr =
-      new TargetProperties(nullptr);
+  static TargetProperties *g_settings_ptr = new TargetProperties(nullptr);
   return *g_settings_ptr;
 }
 
@@ -3426,6 +3425,78 @@ void Target::SaveScriptedLaunchInfo(lldb_private::ProcessInfo &process_info) {
   }
 }
 
+Status
+Target::RegisterScriptedSymbolLocator(llvm::StringRef class_name,
+                                      StructuredData::DictionarySP args_sp) {
+  if (class_name.empty())
+    return Status::FromErrorString(
+        "class name must not be empty; use ClearScriptedSymbolLocator() to "
+        "unregister");
+
+  ScriptInterpreter *interpreter = GetDebugger().GetScriptInterpreter();
+  if (!interpreter)
+    return Status::FromErrorString("no script interpreter available");
+
+  auto interface_sp = interpreter->CreateScriptedSymbolLocatorInterface();
+  if (!interface_sp)
+    return Status::FromErrorString(
+        "failed to create scripted symbol locator interface");
+
+  ExecutionContext exe_ctx;
+  TargetSP target_sp(shared_from_this());
+  exe_ctx.SetTargetSP(target_sp);
+
+  auto obj_or_err =
+      interface_sp->CreatePluginObject(class_name, exe_ctx, args_sp);
+  if (!obj_or_err)
+    return Status::FromError(obj_or_err.takeError());
+
+  m_scripted_symbol_locator_class_name = class_name.str();
+  m_scripted_symbol_locator_args_sp = args_sp;
+  m_scripted_symbol_locator_interface_sp = interface_sp;
+  m_scripted_source_file_cache.clear();
+
+  // Invalidate cached stack frames so the next backtrace re-resolves line
+  // entries through ApplyFileMappings, which will call our locator.
+  ProcessSP process_sp = GetProcessSP();
+  if (process_sp) {
+    ThreadList &thread_list = process_sp->GetThreadList();
+    for (uint32_t i = 0; i < thread_list.GetSize(false); i++) {
+      ThreadSP thread_sp = thread_list.GetThreadAtIndex(i, false);
+      if (thread_sp)
+        thread_sp->ClearStackFrames();
+    }
+  }
+
+  return Status();
+}
+
+void Target::ClearScriptedSymbolLocator() {
+  m_scripted_symbol_locator_class_name.clear();
+  m_scripted_symbol_locator_args_sp.reset();
+  m_scripted_symbol_locator_interface_sp.reset();
+  m_scripted_source_file_cache.clear();
+}
+
+ScriptedSymbolLocatorInterfaceSP Target::GetScriptedSymbolLocatorInterface() {
+  return m_scripted_symbol_locator_interface_sp;
+}
+
+bool Target::LookupScriptedSourceFileCache(
+    const std::string &key, std::optional<FileSpec> &result) const {
+  auto it = m_scripted_source_file_cache.find(key);
+  if (it != m_scripted_source_file_cache.end()) {
+    result = it->second;
+    return true;
+  }
+  return false;
+}
+
+void Target::InsertScriptedSourceFileCache(
+    const std::string &key, const std::optional<FileSpec> &result) {
+  m_scripted_source_file_cache[key] = result;
+}
+
 Status Target::Launch(ProcessLaunchInfo &launch_info, Stream *stream) {
   m_stats.SetLaunchOrAttachTime();
   Status error;
@@ -3650,7 +3721,7 @@ Status Target::Attach(ProcessAttachInfo &attach_info, Stream *stream) {
   if (!attach_info.ProcessInfoSpecified()) {
     if (old_exec_module_sp)
       attach_info.GetExecutableFile().SetFilename(
-            old_exec_module_sp->GetPlatformFileSpec().GetFilename());
+          old_exec_module_sp->GetPlatformFileSpec().GetFilename());
 
     if (!attach_info.ProcessInfoSpecified()) {
       return Status::FromErrorString(
@@ -3868,28 +3939,28 @@ void Target::FinalizeFileActions(ProcessLaunchInfo &info) {
   }
 }
 
-void Target::AddDummySignal(llvm::StringRef name, LazyBool pass, LazyBool notify,
-                            LazyBool stop) {
-    if (name.empty())
-      return;
-    // Don't add a signal if all the actions are trivial:
-    if (pass == eLazyBoolCalculate && notify == eLazyBoolCalculate
-        && stop == eLazyBoolCalculate)
-      return;
+void Target::AddDummySignal(llvm::StringRef name, LazyBool pass,
+                            LazyBool notify, LazyBool stop) {
+  if (name.empty())
+    return;
+  // Don't add a signal if all the actions are trivial:
+  if (pass == eLazyBoolCalculate && notify == eLazyBoolCalculate &&
+      stop == eLazyBoolCalculate)
+    return;
 
-    auto& elem = m_dummy_signals[name];
-    elem.pass = pass;
-    elem.notify = notify;
-    elem.stop = stop;
+  auto &elem = m_dummy_signals[name];
+  elem.pass = pass;
+  elem.notify = notify;
+  elem.stop = stop;
 }
 
 bool Target::UpdateSignalFromDummy(UnixSignalsSP signals_sp,
-                                          const DummySignalElement &elem) {
+                                   const DummySignalElement &elem) {
   if (!signals_sp)
     return false;
 
-  int32_t signo
-      = signals_sp->GetSignalNumberFromName(elem.first().str().c_str());
+  int32_t signo =
+      signals_sp->GetSignalNumberFromName(elem.first().str().c_str());
   if (signo == LLDB_INVALID_SIGNAL_NUMBER)
     return false;
 
@@ -3911,11 +3982,11 @@ bool Target::UpdateSignalFromDummy(UnixSignalsSP signals_sp,
 }
 
 bool Target::ResetSignalFromDummy(UnixSignalsSP signals_sp,
-                                          const DummySignalElement &elem) {
+                                  const DummySignalElement &elem) {
   if (!signals_sp)
     return false;
-  int32_t signo
-      = signals_sp->GetSignalNumberFromName(elem.first().str().c_str());
+  int32_t signo =
+      signals_sp->GetSignalNumberFromName(elem.first().str().c_str());
   if (signo == LLDB_INVALID_SIGNAL_NUMBER)
     return false;
   bool do_pass = elem.second.pass != eLazyBoolCalculate;
@@ -3933,7 +4004,7 @@ void Target::UpdateSignalsFromDummy(UnixSignalsSP signals_sp,
   for (const auto &elem : m_dummy_signals) {
     if (!UpdateSignalFromDummy(signals_sp, elem))
       warning_stream_sp->Printf("Target signal '%s' not found in process\n",
-          elem.first().str().c_str());
+                                elem.first().str().c_str());
   }
 }
 
@@ -3966,11 +4037,14 @@ void Target::PrintDummySignals(Stream &strm, Args &signal_args) {
   strm.Printf("NAME         PASS     STOP     NOTIFY\n");
   strm.Printf("===========  =======  =======  =======\n");
 
-  auto str_for_lazy = [] (LazyBool lazy) -> const char * {
+  auto str_for_lazy = [](LazyBool lazy) -> const char * {
     switch (lazy) {
-      case eLazyBoolCalculate: return "not set";
-      case eLazyBoolYes: return "true   ";
-      case eLazyBoolNo: return "false  ";
+    case eLazyBoolCalculate:
+      return "not set";
+    case eLazyBoolYes:
+      return "true   ";
+    case eLazyBoolNo:
+      return "false  ";
     }
     llvm_unreachable("Fully covered switch above!");
   };
@@ -4085,7 +4159,8 @@ void Target::StopHookCommandLine::GetSubclassDescription(
 }
 
 // Target::StopHookCommandLine
-void Target::StopHookCommandLine::SetActionFromString(const std::string &string) {
+void Target::StopHookCommandLine::SetActionFromString(
+    const std::string &string) {
   GetCommands().SplitIntoLines(string);
 }
 
@@ -4304,20 +4379,14 @@ static constexpr OptionEnumValueElement g_import_std_module_value_types[] = {
         "false",
         "Never import the 'std' C++ module in the expression parser.",
     },
-    {
-        eImportStdModuleFallback,
-        "fallback",
-        "Retry evaluating expressions with an imported 'std' C++ module if they"
-        " failed to parse without the module. This allows evaluating more "
-        "complex expressions involving C++ standard library types."
-    },
-    {
-        eImportStdModuleTrue,
-        "true",
-        "Always import the 'std' C++ module. This allows evaluating more "
-        "complex expressions involving C++ standard library types. This feature"
-        " is experimental."
-    },
+    {eImportStdModuleFallback, "fallback",
+     "Retry evaluating expressions with an imported 'std' C++ module if they"
+     " failed to parse without the module. This allows evaluating more "
+     "complex expressions involving C++ standard library types."},
+    {eImportStdModuleTrue, "true",
+     "Always import the 'std' C++ module. This allows evaluating more "
+     "complex expressions involving C++ standard library types. This feature"
+     " is experimental."},
 };
 
 static constexpr OptionEnumValueElement
@@ -4498,8 +4567,8 @@ TargetProperties::TargetProperties(Target *target)
     m_collection_sp->SetValueChangedCallback(
         ePropertyDisableSTDIO, [this] { DisableSTDIOValueChangedCallback(); });
 
-    m_collection_sp->SetValueChangedCallback(
-        ePropertySaveObjectsDir, [this] { CheckJITObjectsDir(); });
+    m_collection_sp->SetValueChangedCallback(ePropertySaveObjectsDir,
+                                             [this] { CheckJITObjectsDir(); });
     m_experimental_properties_up =
         std::make_unique<TargetExperimentalProperties>();
     m_collection_sp->AppendProperty(
@@ -4520,8 +4589,8 @@ TargetProperties::TargetProperties(Target *target)
     m_collection_sp->AppendProperty(
         "process", "Settings specific to processes.", true,
         Process::GetGlobalProperties().GetValueProperties());
-    m_collection_sp->SetValueChangedCallback(
-        ePropertySaveObjectsDir, [this] { CheckJITObjectsDir(); });
+    m_collection_sp->SetValueChangedCallback(ePropertySaveObjectsDir,
+                                             [this] { CheckJITObjectsDir(); });
   }
 }
 
diff --git a/lldb/source/Target/ThreadPlanStepRange.cpp b/lldb/source/Target/ThreadPlanStepRange.cpp
index 3a9deb6f5c6fd..0675977d0964c 100644
--- a/lldb/source/Target/ThreadPlanStepRange.cpp
+++ b/lldb/source/Target/ThreadPlanStepRange.cpp
@@ -433,7 +433,7 @@ 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());
             if (!top_most_line_entry.file_sp->GetSpecOnly())
               top_most_line_entry.file_sp =
                   top_most_line_entry.original_file_sp;
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..e1604d88b9dbb
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/Makefile
@@ -0,0 +1,9 @@
+C_SOURCES := main.c
+USE_SYSTEM_STDLIB := 1
+
+# Linux/FreeBSD need --build-id for a UUID; Darwin gets one automatically.
+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..8fdad934de502
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
@@ -0,0 +1,195 @@
+"""
+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_locator(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)
+
+        # Now set up the scripted locator with per-target registration.
+        self.import_locator()
+        self.register_locator(
+            "source_locator.SourceLocator",
+            "-k resolved_dir -v '%s'" % tmp_dir,
+        )
+        self.addTearDownHook(lambda: self.clear_locator())
+
+        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.
+        process = target.LaunchSimple(None, None, os.getcwd())
+        self.assertIsNotNone(process)
+        self.assertState(process.GetState(), lldb.eStateStopped)
+
+        thread = process.GetSelectedThread()
+        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
+        # instance calls via the scripted symbol locator.
+        # Since calls are now instance-level, we access them through
+        # the scripted interface's Python object.
+        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 script class set 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_locator())
+
+        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)
+
+        # Second: test with no script class set -- 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 the class name."""
+        self.build()
+
+        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
+        self.assertTrue(target and target.IsValid(), VALID_TARGET)
+
+        # Before registration, should report no locator.
+        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_locator())
+
+        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)
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..beef030966265
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
@@ -0,0 +1,7 @@
+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..420a5ecdd24e8
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py
@@ -0,0 +1,74 @@
+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[str]:
+        uuid = module.GetUUIDString()
+        self.calls.append((uuid, original_source_file))
+        if self.resolved_dir:
+            basename = os.path.basename(original_source_file)
+            return os.path.join(self.resolved_dir, basename)
+        return None
+
+    def locate_executable_object_file(
+        self, module_spec: lldb.SBModuleSpec
+    ) -> Optional[str]:
+        return None
+
+    def locate_executable_symbol_file(
+        self, module_spec: lldb.SBModuleSpec, default_search_paths: list
+    ) -> Optional[str]:
+        return None
+
+    def download_object_and_symbol_file(
+        self, module_spec: lldb.SBModuleSpec, force_lookup: bool, copy_executable: bool
+    ) -> bool:
+        return False
+
+
+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[str]:
+        return None
+
+    def locate_executable_object_file(
+        self, module_spec: lldb.SBModuleSpec
+    ) -> Optional[str]:
+        return None
+
+    def locate_executable_symbol_file(
+        self, module_spec: lldb.SBModuleSpec, default_search_paths: list
+    ) -> Optional[str]:
+        return None
+
+    def download_object_and_symbol_file(
+        self, module_spec: lldb.SBModuleSpec, force_lookup: bool, copy_executable: bool
+    ) -> bool:
+        return False
diff --git a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
index 5694aeeff3e5b..a339378b967f1 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
@@ -171,6 +171,11 @@ lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data) {
   return nullptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpec(
+    PyObject *data) {
+  return nullptr;
+}
+
 lldb::ValueObjectSP
 lldb_private::python::SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
     void *data) {



More information about the lldb-commits mailing list