Author: rchamala
Date: 2026-02-14T07:39:00-08:00
New Revision: 1ee03d1e097f05cc9811a0e4a3e7e13e35340b6d

URL: 
https://github.com/llvm/llvm-project/commit/1ee03d1e097f05cc9811a0e4a3e7e13e35340b6d
DIFF: 
https://github.com/llvm/llvm-project/commit/1ee03d1e097f05cc9811a0e4a3e7e13e35340b6d.diff

LOG: [lldb] Add ScriptedSymbolLocator plugin for source file resolution 
(#181334)

## Summary                                                        
                                                                    
Based on discussion from
[RFC](https://discourse.llvm.org/t/rfc-python-callback-for-source-file-resolution/83545),
this PR adds a new `SymbolLocatorScripted` plugin that allows Python
scripts to implement custom symbol and source file resolution logic.
This enables downstream users to build custom symbol servers, source
file remapping, and build artifact resolution entirely in Python.
                                                                    
  ### Changes

- Adds `LocateSourceFile()` to the SymbolLocator plugin interface,
called during source path resolution with a fully loaded `ModuleSP`, so
the plugin has access to the module's UUID, file paths, and symbols.
- Adds `SymbolLocatorScripted` plugin that delegates all four
SymbolLocator methods (`LocateExecutableObjectFile`,
`LocateExecutableSymbolFile`, `DownloadObjectAndSymbolFile`,
`LocateSourceFile`) to a user-provided Python class.
- Adds `ScriptedSymbolLocatorPythonInterface` to bridge C++ calls to
Python, with proper GIL management and error handling.
- Results for `LocateSourceFile` are cached per (module UUID, source
file) pair.
- The Python class is configured via: `settings set
plugin.symbol-locator.scripted.script-class module.ClassName`

  ### Python class interface

  ```python
  class MyLocator:
      def __init__(self, exe_ctx, args): ...
      def locate_source_file(self, module, original_source_file):
  ...
      def locate_executable_object_file(self, module_spec): ...
      def locate_executable_symbol_file(self, module_spec,
  default_search_paths): ...
      def download_object_and_symbol_file(self, module_spec,
  force_lookup, copy_executable): ...
```

  ### Test plan
```
  Added TestScriptedSymbolLocator.py with 3 test cases:
  - test_locate_source_file — verifies the locator resolves source
  files, receives a valid SBModule with UUID, and remaps paths correctly
  - test_locate_source_file_none_fallthrough — verifies returning
None falls through to default LLDB resolution, and that having no script
  class set works normally
  - test_invalid_script_class — verifies graceful handling of
  invalid class names without crashing
```

Co-authored-by: Rahul Reddy Chamala <[email protected]>

Added: 
    lldb/docs/use/tutorials/scripted-symbol-locator.md
    lldb/examples/python/templates/scripted_symbol_locator.py
    lldb/include/lldb/Interpreter/Interfaces/ScriptedSymbolLocatorInterface.h
    
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
    
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.h
    lldb/source/Plugins/SymbolLocator/Scripted/CMakeLists.txt
    lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
    lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
    lldb/test/API/functionalities/scripted_symbol_locator/Makefile
    
lldb/test/API/functionalities/scripted_symbol_locator/TestScriptedSymbolLocator.py
    lldb/test/API/functionalities/scripted_symbol_locator/main.c
    lldb/test/API/functionalities/scripted_symbol_locator/source_locator.py

Modified: 
    lldb/bindings/python/CMakeLists.txt
    lldb/bindings/python/python-swigsafecast.swig
    lldb/bindings/python/python-wrapper.swig
    lldb/docs/use/python-reference.rst
    lldb/include/lldb/API/SBFileSpec.h
    lldb/include/lldb/API/SBModule.h
    lldb/include/lldb/API/SBModuleSpec.h
    lldb/include/lldb/API/SBTarget.h
    lldb/include/lldb/Core/PluginManager.h
    lldb/include/lldb/Interpreter/ScriptInterpreter.h
    lldb/include/lldb/Symbol/LineEntry.h
    lldb/include/lldb/Target/Target.h
    lldb/include/lldb/lldb-forward.h
    lldb/include/lldb/lldb-private-interfaces.h
    lldb/source/API/SBTarget.cpp
    lldb/source/Commands/CommandObjectTarget.cpp
    lldb/source/Core/Module.cpp
    lldb/source/Core/PluginManager.cpp
    lldb/source/Interpreter/ScriptInterpreter.cpp
    lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
    
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
    
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.h
    
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
    
lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
    lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
    lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
    lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPythonImpl.h
    lldb/source/Plugins/SymbolLocator/CMakeLists.txt
    lldb/source/Plugins/SymbolLocator/Debuginfod/SymbolLocatorDebuginfod.cpp
    lldb/source/Symbol/LineEntry.cpp
    lldb/source/Target/StackFrame.cpp
    lldb/source/Target/StackFrameList.cpp
    lldb/source/Target/Target.cpp
    lldb/source/Target/ThreadPlanStepRange.cpp
    lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp

Removed: 
    


################################################################################
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-swigsafecast.swig 
b/lldb/bindings/python/python-swigsafecast.swig
index a86dc44ce4106..584267d444d9c 100644
--- a/lldb/bindings/python/python-swigsafecast.swig
+++ b/lldb/bindings/python/python-swigsafecast.swig
@@ -147,6 +147,12 @@ PythonObject SWIGBridge::ToSWIGWrapper(
   return ToSWIGHelper(module_spec_sb.release(), SWIGTYPE_p_lldb__SBModuleSpec);
 }
 
+PythonObject SWIGBridge::ToSWIGWrapper(
+    std::unique_ptr<lldb::SBFileSpecList> file_spec_list_sb) {
+  return ToSWIGHelper(file_spec_list_sb.release(),
+                      SWIGTYPE_p_lldb__SBFileSpecList);
+}
+
 PythonObject SWIGBridge::ToSWIGWrapper(lldb::DescriptionLevel level) {
   return PythonInteger((int64_t) level);
 }

diff  --git a/lldb/bindings/python/python-wrapper.swig 
b/lldb/bindings/python/python-wrapper.swig
index bf59569920470..eba370af5ccf1 100644
--- a/lldb/bindings/python/python-wrapper.swig
+++ b/lldb/bindings/python/python-wrapper.swig
@@ -595,6 +595,58 @@ void 
*lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *d
   return sb_ptr;
 }
 
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject *
+                                                                    data) {
+  lldb::SBFileSpec *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBFileSpec, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModuleSpec(PyObject 
*
+                                                                      data) {
+  lldb::SBModuleSpec *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBModuleSpec, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModule(PyObject *
+                                                                   data) {
+  lldb::SBModule *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBModule, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpecList(
+    PyObject *data) {
+  lldb::SBFileSpecList *sb_ptr = NULL;
+
+  int valid_cast = SWIG_ConvertPtr(data, (void **)&sb_ptr,
+                                   SWIGTYPE_p_lldb__SBFileSpecList, 0);
+
+  if (valid_cast == -1)
+    return NULL;
+
+  return sb_ptr;
+}
+
 bool lldb_private::python::SWIGBridge::LLDBSwigPythonCallCommand(
     const char *python_function_name, const char *session_dictionary_name,
     lldb::DebuggerSP debugger, const char *args,

diff  --git a/lldb/docs/use/python-reference.rst 
b/lldb/docs/use/python-reference.rst
index afca07520d8ad..2b345a7cd3bbf 100644
--- a/lldb/docs/use/python-reference.rst
+++ b/lldb/docs/use/python-reference.rst
@@ -28,3 +28,4 @@ The following tutorials and documentation demonstrate various 
Python capabilitie
    tutorials/implementing-standalone-scripts
    tutorials/custom-frame-recognizers
    tutorials/extending-target-stop-hooks
+   tutorials/scripted-symbol-locator

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..eaba91d9b444b
--- /dev/null
+++ b/lldb/docs/use/tutorials/scripted-symbol-locator.md
@@ -0,0 +1,165 @@
+# 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
+   from lldb.plugins.scripted_symbol_locator import ScriptedSymbolLocator
+
+   class MyLocator(ScriptedSymbolLocator):
+       def __init__(self, exe_ctx, args):
+           super().__init__(exe_ctx, args)
+           self.cache_dir = None
+           if self.args and self.args.IsValid():
+               d = self.args.GetValueForKey("cache_dir")
+               if d and d.IsValid():
+                   self.cache_dir = d.GetStringValue(4096)
+
+       def locate_source_file(self, module, original_source_file):
+           """Return the resolved file spec, or None to fall through."""
+           if not self.cache_dir:
+               return None
+           uuid = module.GetUUIDString()
+           basename = os.path.basename(original_source_file)
+           candidate = os.path.join(self.cache_dir, uuid, "src", basename)
+           if os.path.exists(candidate):
+               return lldb.SBFileSpec(candidate, True)
+           return None
+   ```
+
+2. **Import the script and register the locator 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[lldb.SBFileSpec]:
+    ...
+
+def locate_executable_object_file(
+        self, module_spec: lldb.SBModuleSpec) -> Optional[lldb.SBFileSpec]:
+    ...
+
+def locate_executable_symbol_file(
+        self, module_spec: lldb.SBModuleSpec,
+        default_search_paths: list) -> Optional[lldb.SBFileSpec]:
+    ...
+
+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 
diff erent locator classes or 
diff erent 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..d3c87bd0563c3
--- /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:
+            lldb.SBFileSpec: The resolved file spec, 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:
+            lldb.SBFileSpec: 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:
+            lldb.SBFileSpec: 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 lldb.SBFileSpec(path, True)
+        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 lldb.SBFileSpec(path, True)
+        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 lldb.SBFileSpec(debug_path, True)
+        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/SBModule.h 
b/lldb/include/lldb/API/SBModule.h
index 4009ca1461e51..3e8e5b99f6404 100644
--- a/lldb/include/lldb/API/SBModule.h
+++ b/lldb/include/lldb/API/SBModule.h
@@ -311,6 +311,7 @@ class LLDB_API SBModule {
   friend class SBType;
 
   friend class lldb_private::python::SWIGBridge;
+  friend class lldb_private::ScriptInterpreter;
 
   explicit SBModule(const lldb::ModuleSP &module_sp);
 

diff  --git a/lldb/include/lldb/API/SBModuleSpec.h 
b/lldb/include/lldb/API/SBModuleSpec.h
index 0e7f0f3489596..5be9b5e8dd4a7 100644
--- a/lldb/include/lldb/API/SBModuleSpec.h
+++ b/lldb/include/lldb/API/SBModuleSpec.h
@@ -102,6 +102,7 @@ class LLDB_API SBModuleSpec {
   friend class SBModule;
   friend class SBPlatform;
   friend class SBTarget;
+  friend class lldb_private::ScriptInterpreter;
 
   SBModuleSpec(const lldb_private::ModuleSpec &module_spec);
 

diff  --git a/lldb/include/lldb/API/SBTarget.h 
b/lldb/include/lldb/API/SBTarget.h
index dd2cf59b831da..a9c28cf4e29fc 100644
--- a/lldb/include/lldb/API/SBTarget.h
+++ b/lldb/include/lldb/API/SBTarget.h
@@ -1002,6 +1002,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..8cb061059e5cd 100644
--- a/lldb/include/lldb/Interpreter/ScriptInterpreter.h
+++ b/lldb/include/lldb/Interpreter/ScriptInterpreter.h
@@ -16,9 +16,12 @@
 #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"
+#include "lldb/API/SBModule.h"
+#include "lldb/API/SBModuleSpec.h"
 #include "lldb/API/SBStream.h"
 #include "lldb/API/SBSymbolContext.h"
 #include "lldb/API/SBThread.h"
@@ -33,6 +36,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"
@@ -545,6 +549,11 @@ class ScriptInterpreter : public PluginInterface {
     return {};
   }
 
+  virtual lldb::ScriptedSymbolLocatorInterfaceSP
+  CreateScriptedSymbolLocatorInterface() {
+    return {};
+  }
+
   virtual lldb::ScriptedThreadPlanInterfaceSP
   CreateScriptedThreadPlanInterface() {
     return {};
@@ -612,6 +621,17 @@ class ScriptInterpreter : public PluginInterface {
   lldb::ValueObjectSP
   GetOpaqueTypeFromSBValue(const lldb::SBValue &value) const;
 
+  std::optional<FileSpec>
+  GetOpaqueTypeFromSBFileSpec(const lldb::SBFileSpec &file_spec) const;
+
+  std::optional<ModuleSpec>
+  GetOpaqueTypeFromSBModuleSpec(const lldb::SBModuleSpec &module_spec) const;
+
+  lldb::ModuleSP GetOpaqueTypeFromSBModule(const lldb::SBModule &module) const;
+
+  std::unique_ptr<lldb::SBModuleSpec>
+  MakeSBModuleSpec(const ModuleSpec &module_spec) const;
+
 protected:
   Debugger &m_debugger;
   lldb::ScriptLanguage m_script_lang;

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..b51d49f2f780d 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"
@@ -38,6 +39,7 @@
 #include "lldb/Utility/Broadcaster.h"
 #include "lldb/Utility/LLDBAssert.h"
 #include "lldb/Utility/RealpathPrefixes.h"
+#include "lldb/Utility/ScriptedMetadata.h"
 #include "lldb/Utility/StructuredData.h"
 #include "lldb/Utility/Timeout.h"
 #include "lldb/lldb-public.h"
@@ -1705,6 +1707,24 @@ 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_metadata_sp
+               ? m_scripted_symbol_locator_metadata_sp->GetClassName()
+               : "";
+  }
+
+  /// Look up a previously cached source file resolution result.
+  /// Returns true if a cached entry exists (even if the result is nullopt).
+  bool LookupScriptedSourceFileCache(llvm::StringRef key,
+                                     std::optional<FileSpec> &result) const;
+  void InsertScriptedSourceFileCache(llvm::StringRef 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 +1863,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.
+  /// @{
+  lldb::ScriptedMetadataSP m_scripted_symbol_locator_metadata_sp;
+  lldb::ScriptedSymbolLocatorInterfaceSP 
m_scripted_symbol_locator_interface_sp;
+  llvm::StringMap<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..8fe6ea81385ca 100644
--- a/lldb/source/API/SBTarget.cpp
+++ b/lldb/source/API/SBTarget.cpp
@@ -2432,6 +2432,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..99dab64b4291f 100644
--- a/lldb/source/Commands/CommandObjectTarget.cpp
+++ b/lldb/source/Commands/CommandObjectTarget.cpp
@@ -4691,6 +4691,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 +4832,9 @@ class CommandObjectTargetSymbols : public 
CommandObjectMultiword {
             "target symbols <sub-command> ...") {
     LoadSubCommand(
         "add", CommandObjectSP(new 
CommandObjectTargetSymbolsAdd(interpreter)));
+    LoadSubCommand(
+        "scripted",
+        CommandObjectSP(new CommandObjectTargetSymbolsScripted(interpreter)));
   }
 
   ~CommandObjectTargetSymbols() override = default;

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..5b8bcc7cc68ef 100644
--- a/lldb/source/Core/PluginManager.cpp
+++ b/lldb/source/Core/PluginManager.cpp
@@ -1476,18 +1476,21 @@ struct SymbolLocatorInstance
       SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file,
       SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file,
       SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle,
+      SymbolLocatorLocateSourceFile locate_source_file,
       DebuggerInitializeCallback debugger_init_callback)
       : PluginInstance<SymbolLocatorCreateInstance>(
             name, description, create_callback, debugger_init_callback),
         locate_executable_object_file(locate_executable_object_file),
         locate_executable_symbol_file(locate_executable_symbol_file),
         download_object_symbol_file(download_object_symbol_file),
-        find_symbol_file_in_bundle(find_symbol_file_in_bundle) {}
+        find_symbol_file_in_bundle(find_symbol_file_in_bundle),
+        locate_source_file(locate_source_file) {}
 
   SymbolLocatorLocateExecutableObjectFile locate_executable_object_file;
   SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file;
   SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file;
   SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle;
+  SymbolLocatorLocateSourceFile locate_source_file;
 };
 typedef PluginInstances<SymbolLocatorInstance> SymbolLocatorInstances;
 
@@ -1503,11 +1506,12 @@ bool PluginManager::RegisterPlugin(
     SymbolLocatorLocateExecutableSymbolFile locate_executable_symbol_file,
     SymbolLocatorDownloadObjectAndSymbolFile download_object_symbol_file,
     SymbolLocatorFindSymbolFileInBundle find_symbol_file_in_bundle,
+    SymbolLocatorLocateSourceFile locate_source_file,
     DebuggerInitializeCallback debugger_init_callback) {
   return GetSymbolLocatorInstances().RegisterPlugin(
       name, description, create_callback, locate_executable_object_file,
       locate_executable_symbol_file, download_object_symbol_file,
-      find_symbol_file_in_bundle, debugger_init_callback);
+      find_symbol_file_in_bundle, locate_source_file, debugger_init_callback);
 }
 
 bool PluginManager::UnregisterPlugin(
@@ -1591,6 +1595,20 @@ FileSpec PluginManager::FindSymbolFileInBundle(const 
FileSpec &symfile_bundle,
   return {};
 }
 
+FileSpec PluginManager::LocateSourceFile(const lldb::ModuleSP &module_sp,
+                                         const FileSpec &original_source_file) 
{
+  auto instances = GetSymbolLocatorInstances().GetSnapshot();
+  for (auto &instance : instances) {
+    if (instance.locate_source_file) {
+      std::optional<FileSpec> result =
+          instance.locate_source_file(module_sp, original_source_file);
+      if (result)
+        return *result;
+    }
+  }
+  return {};
+}
+
 #pragma mark Trace
 
 struct TraceInstance

diff  --git a/lldb/source/Interpreter/ScriptInterpreter.cpp 
b/lldb/source/Interpreter/ScriptInterpreter.cpp
index 5e8478c2670bb..9e2cad22ad6e6 100644
--- a/lldb/source/Interpreter/ScriptInterpreter.cpp
+++ b/lldb/source/Interpreter/ScriptInterpreter.cpp
@@ -172,6 +172,31 @@ ScriptInterpreter::GetOpaqueTypeFromSBValue(const 
lldb::SBValue &value) const {
   return locker.GetLockedSP(*value.m_opaque_sp);
 }
 
+std::optional<FileSpec> ScriptInterpreter::GetOpaqueTypeFromSBFileSpec(
+    const lldb::SBFileSpec &file_spec) const {
+  if (file_spec.m_opaque_up)
+    return *file_spec.m_opaque_up;
+  return {};
+}
+
+std::optional<ModuleSpec> ScriptInterpreter::GetOpaqueTypeFromSBModuleSpec(
+    const lldb::SBModuleSpec &module_spec) const {
+  if (module_spec.m_opaque_up)
+    return *module_spec.m_opaque_up;
+  return {};
+}
+
+lldb::ModuleSP ScriptInterpreter::GetOpaqueTypeFromSBModule(
+    const lldb::SBModule &module) const {
+  return module.m_opaque_sp;
+}
+
+std::unique_ptr<lldb::SBModuleSpec>
+ScriptInterpreter::MakeSBModuleSpec(const ModuleSpec &module_spec) const {
+  return std::unique_ptr<lldb::SBModuleSpec>(
+      new lldb::SBModuleSpec(module_spec));
+}
+
 lldb::ScriptLanguage
 ScriptInterpreter::StringToLanguage(const llvm::StringRef &language) {
   if (language.equals_insensitive(LanguageToString(eScriptLanguageNone)))

diff  --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/CMakeLists.txt
index 50569cdefaafa..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..96a81c9153736 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptInterpreterPythonInterfaces.cpp
@@ -32,6 +32,7 @@ void ScriptInterpreterPythonInterfaces::Initialize() {
   ScriptedBreakpointPythonInterface::Initialize();
   ScriptedThreadPlanPythonInterface::Initialize();
   ScriptedFrameProviderPythonInterface::Initialize();
+  ScriptedSymbolLocatorPythonInterface::Initialize();
 }
 
 void ScriptInterpreterPythonInterfaces::Terminate() {
@@ -42,6 +43,7 @@ void ScriptInterpreterPythonInterfaces::Terminate() {
   ScriptedBreakpointPythonInterface::Terminate();
   ScriptedThreadPlanPythonInterface::Terminate();
   ScriptedFrameProviderPythonInterface::Terminate();
+  ScriptedSymbolLocatorPythonInterface::Terminate();
 }
 
 #endif

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..92438cf51928d 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.cpp
@@ -17,7 +17,10 @@
 
 #include "../ScriptInterpreterPythonImpl.h"
 #include "ScriptedPythonInterface.h"
+#include "lldb/Core/ModuleSpec.h"
 #include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/FileSpecList.h"
 #include "lldb/ValueObject/ValueObjectList.h"
 #include <optional>
 
@@ -311,4 +314,68 @@ 
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()))) {
+    if (auto file_spec =
+            m_interpreter.GetOpaqueTypeFromSBFileSpec(*sb_file_spec))
+      return *file_spec;
+  }
+  error = Status::FromErrorString(
+      "couldn't cast lldb::SBFileSpec to lldb_private::FileSpec.");
+
+  return {};
+}
+
+template <>
+ModuleSpec ScriptedPythonInterface::ExtractValueFromPythonObject<ModuleSpec>(
+    python::PythonObject &p, Status &error) {
+  if (lldb::SBModuleSpec *sb_module_spec =
+          reinterpret_cast<lldb::SBModuleSpec *>(
+              python::LLDBSWIGPython_CastPyObjectToSBModuleSpec(p.get()))) {
+    if (auto module_spec =
+            m_interpreter.GetOpaqueTypeFromSBModuleSpec(*sb_module_spec))
+      return *module_spec;
+  }
+  error = Status::FromErrorString(
+      "couldn't cast lldb::SBModuleSpec to lldb_private::ModuleSpec.");
+
+  return {};
+}
+
+template <>
+FileSpecList
+ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpecList>(
+    python::PythonObject &p, Status &error) {
+  FileSpecList result;
+  if (lldb::SBFileSpecList *sb_list = reinterpret_cast<lldb::SBFileSpecList *>(
+          python::LLDBSWIGPython_CastPyObjectToSBFileSpecList(p.get()))) {
+    for (uint32_t i = 0; i < sb_list->GetSize(); i++) {
+      lldb::SBFileSpec sb_file_spec = sb_list->GetFileSpecAtIndex(i);
+      if (auto file_spec =
+              m_interpreter.GetOpaqueTypeFromSBFileSpec(sb_file_spec))
+        result.Append(*file_spec);
+    }
+    return result;
+  }
+  error = Status::FromErrorString(
+      "couldn't cast Python object to lldb::SBFileSpecList.");
+  return result;
+}
+
+template <>
+lldb::ModuleSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ModuleSP>(
+    python::PythonObject &p, Status &error) {
+  if (lldb::SBModule *sb_module = reinterpret_cast<lldb::SBModule *>(
+          python::LLDBSWIGPython_CastPyObjectToSBModule(p.get())))
+    return m_interpreter.GetOpaqueTypeFromSBModule(*sb_module);
+  error = Status::FromErrorString(
+      "couldn't cast lldb::SBModule to lldb::ModuleSP.");
+
+  return {};
+}
+
 #endif

diff  --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
index 4aadee584b2e2..796b36b6e933f 100644
--- 
a/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedPythonInterface.h
@@ -17,9 +17,16 @@
 #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/SBFileSpecList.h"
+#include "lldb/API/SBModuleSpec.h"
 
 #include "../PythonDataObjects.h"
 #include "../SWIGPythonBridge.h"
@@ -632,6 +639,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 +671,24 @@ class ScriptedPythonInterface : virtual public 
ScriptedInterface {
     return python::SWIGBridge::ToSWIGWrapper(arg);
   }
 
+  python::PythonObject Transform(const ModuleSpec &arg) {
+    return python::SWIGBridge::ToSWIGWrapper(
+        m_interpreter.MakeSBModuleSpec(arg));
+  }
+
+  python::PythonObject Transform(const FileSpecList &arg) {
+    auto sb_list = std::make_unique<lldb::SBFileSpecList>();
+    for (size_t i = 0; i < arg.GetSize(); i++) {
+      sb_list->Append(
+          lldb::SBFileSpec(arg.GetFileSpecAtIndex(i).GetPath().c_str(), 
false));
+    }
+    return python::SWIGBridge::ToSWIGWrapper(std::move(sb_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 +700,16 @@ 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(std::string &original_arg,
+                        python::PythonObject transformed_arg, Status &error) {
+    python::PythonString py_str(python::PyRefType::Borrowed,
+                                transformed_arg.get());
+    if (py_str.IsValid())
+      original_arg = py_str.GetString().str();
+  }
+
   void ReverseTransform(bool &original_arg,
                         python::PythonObject transformed_arg, Status &error) {
     python::PythonBoolean boolean_arg = python::PythonBoolean(
@@ -828,6 +867,24 @@ lldb::ValueObjectListSP
 ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ValueObjectListSP>(
     python::PythonObject &p, Status &error);
 
+template <>
+FileSpec ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpec>(
+    python::PythonObject &p, Status &error);
+
+template <>
+ModuleSpec ScriptedPythonInterface::ExtractValueFromPythonObject<ModuleSpec>(
+    python::PythonObject &p, Status &error);
+
+template <>
+FileSpecList
+ScriptedPythonInterface::ExtractValueFromPythonObject<FileSpecList>(
+    python::PythonObject &p, Status &error);
+
+template <>
+lldb::ModuleSP
+ScriptedPythonInterface::ExtractValueFromPythonObject<lldb::ModuleSP>(
+    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..d01c8ef575d36
--- /dev/null
+++ 
b/lldb/source/Plugins/ScriptInterpreter/Python/Interfaces/ScriptedSymbolLocatorPythonInterface.cpp
@@ -0,0 +1,120 @@
+//===----------------------------------------------------------------------===//
+//
+// 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);
+  FileSpec file_spec =
+      Dispatch<FileSpec>("locate_executable_object_file", error, ms_copy);
+
+  if (error.Fail() || !file_spec)
+    return {};
+
+  ModuleSpec result_spec(module_spec);
+  result_spec.GetFileSpec() = file_spec;
+  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);
+  FileSpec file_spec = Dispatch<FileSpec>("locate_executable_symbol_file",
+                                          error, ms_copy, fsl_copy);
+
+  if (error.Fail() || !file_spec)
+    return {};
+
+  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);
+
+  FileSpec file_spec =
+      Dispatch<FileSpec>("locate_source_file", error, module_copy, 
source_path);
+
+  if (error.Fail() || !file_spec)
+    return {};
+
+  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..1c2aa7158bbfd 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/SWIGPythonBridge.h
@@ -31,6 +31,7 @@ class SBValue;
 class SBStream;
 class SBStructuredData;
 class SBFileSpec;
+class SBFileSpecList;
 class SBModuleSpec;
 class SBStringList;
 } // namespace lldb
@@ -116,6 +117,8 @@ class SWIGBridge {
   ToSWIGWrapper(std::unique_ptr<lldb::SBFileSpec> file_spec_sb);
   static PythonObject
   ToSWIGWrapper(std::unique_ptr<lldb::SBModuleSpec> module_spec_sb);
+  static PythonObject
+  ToSWIGWrapper(std::unique_ptr<lldb::SBFileSpecList> file_spec_list_sb);
 
   static python::ScopedPythonObject<lldb::SBCommandReturnObject>
   ToSWIGWrapper(CommandReturnObject &cmd_retobj);
@@ -273,6 +276,10 @@ void *LLDBSWIGPython_CastPyObjectToSBValueList(PyObject 
*data);
 void *LLDBSWIGPython_CastPyObjectToSBMemoryRegionInfo(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBExecutionContext(PyObject *data);
 void *LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBModuleSpec(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBModule(PyObject *data);
+void *LLDBSWIGPython_CastPyObjectToSBFileSpecList(PyObject *data);
 } // namespace python
 
 } // namespace lldb_private

diff  --git 
a/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp 
b/lldb/source/Plugins/ScriptInterpreter/Python/ScriptInterpreterPython.cpp
index 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..cfbb11582a042
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.cpp
@@ -0,0 +1,201 @@
+//===----------------------------------------------------------------------===//
+//
+// 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);
+
+  Log *log = GetLog(LLDBLog::Symbols);
+  if (!error.Success()) {
+    LLDB_LOG(log, "SymbolLocatorScripted: locate_source_file failed: {0}",
+             error);
+  }
+
+  owning_target->InsertScriptedSourceFileCache(cache_key, located);
+
+  if (located) {
+    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..f1b64d2ae2f0f
--- /dev/null
+++ b/lldb/source/Plugins/SymbolLocator/Scripted/SymbolLocatorScripted.h
@@ -0,0 +1,50 @@
+//===----------------------------------------------------------------------===//
+//
+// 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(); }
+
+  static std::optional<ModuleSpec>
+  LocateExecutableObjectFile(const ModuleSpec &module_spec);
+
+  static std::optional<FileSpec>
+  LocateExecutableSymbolFile(const ModuleSpec &module_spec,
+                             const FileSpecList &default_search_paths);
+
+  static bool DownloadObjectAndSymbolFile(ModuleSpec &module_spec,
+                                          Status &error, bool force_lookup,
+                                          bool copy_executable);
+
+  static std::optional<FileSpec>
+  LocateSourceFile(const lldb::ModuleSP &module_sp,
+                   const FileSpec &original_source_file);
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_PLUGINS_SYMBOLLOCATOR_SCRIPTED_SYMBOLLOCATORSCRIPTED_H

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 c7cccbe1218b2..a4bdc20410007 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..b46916a9af35e 100644
--- a/lldb/source/Target/StackFrameList.cpp
+++ b/lldb/source/Target/StackFrameList.cpp
@@ -562,7 +562,8 @@ bool StackFrameList::FetchFramesUpTo(uint32_t end_idx,
 
       while (unwind_sc.GetParentOfInlinedScope(
           curr_frame_address, next_frame_sc, next_frame_address)) {
-        next_frame_sc.line_entry.ApplyFileMappings(target_sp);
+        next_frame_sc.line_entry.ApplyFileMappings(target_sp,
+                                                   curr_frame_address);
         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..be22e174bfbc2 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"
@@ -3426,6 +3427,87 @@ 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_metadata_sp =
+      std::make_shared<ScriptedMetadata>(class_name, 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++) {
+      if (ThreadSP thread_sp = thread_list.GetThreadAtIndex(i, false))
+        thread_sp->ClearStackFrames();
+    }
+  }
+
+  return Status();
+}
+
+void Target::ClearScriptedSymbolLocator() {
+  m_scripted_symbol_locator_metadata_sp.reset();
+  m_scripted_symbol_locator_interface_sp.reset();
+  m_scripted_source_file_cache.clear();
+
+  // Invalidate cached stack frames so the next backtrace re-resolves line
+  // entries without the scripted 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++) {
+      if (ThreadSP thread_sp = thread_list.GetThreadAtIndex(i, false))
+        thread_sp->ClearStackFrames();
+    }
+  }
+}
+
+ScriptedSymbolLocatorInterfaceSP Target::GetScriptedSymbolLocatorInterface() {
+  return m_scripted_symbol_locator_interface_sp;
+}
+
+bool Target::LookupScriptedSourceFileCache(
+    llvm::StringRef 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(
+    llvm::StringRef 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;

diff  --git a/lldb/source/Target/ThreadPlanStepRange.cpp 
b/lldb/source/Target/ThreadPlanStepRange.cpp
index 3a9deb6f5c6fd..c75bd68d2e4bb 100644
--- a/lldb/source/Target/ThreadPlanStepRange.cpp
+++ b/lldb/source/Target/ThreadPlanStepRange.cpp
@@ -432,8 +432,8 @@ bool ThreadPlanStepRange::SetNextBranchBreakpoint() {
                 std::make_shared<SupportFile>(call_site_file_spec);
             top_most_line_entry.range = range;
             top_most_line_entry.file_sp = std::make_shared<SupportFile>();
-            top_most_line_entry.ApplyFileMappings(
-                GetThread().CalculateTarget());
+            
top_most_line_entry.ApplyFileMappings(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..71a79cd90f5b2
--- /dev/null
+++ b/lldb/test/API/functionalities/scripted_symbol_locator/main.c
@@ -0,0 +1,5 @@
+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..d016bfb907d04
--- /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[lldb.SBFileSpec]:
+        uuid = module.GetUUIDString()
+        self.calls.append((uuid, original_source_file))
+        if self.resolved_dir:
+            basename = os.path.basename(original_source_file)
+            return lldb.SBFileSpec(os.path.join(self.resolved_dir, basename), 
True)
+        return None
+
+    def locate_executable_object_file(
+        self, module_spec: lldb.SBModuleSpec
+    ) -> Optional[lldb.SBFileSpec]:
+        return None
+
+    def locate_executable_symbol_file(
+        self, module_spec: lldb.SBModuleSpec, default_search_paths: list
+    ) -> Optional[lldb.SBFileSpec]:
+        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[lldb.SBFileSpec]:
+        return None
+
+    def locate_executable_object_file(
+        self, module_spec: lldb.SBModuleSpec
+    ) -> Optional[lldb.SBFileSpec]:
+        return None
+
+    def locate_executable_symbol_file(
+        self, module_spec: lldb.SBModuleSpec, default_search_paths: list
+    ) -> Optional[lldb.SBFileSpec]:
+        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..eebb57b319255 100644
--- a/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
+++ b/lldb/unittests/ScriptInterpreter/Python/PythonTestSuite.cpp
@@ -171,6 +171,26 @@ 
lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFrameList(PyObject *data) {
   return nullptr;
 }
 
+void *
+lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpec(PyObject *data) {
+  return nullptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModuleSpec(
+    PyObject *data) {
+  return nullptr;
+}
+
+void *
+lldb_private::python::LLDBSWIGPython_CastPyObjectToSBModule(PyObject *data) {
+  return nullptr;
+}
+
+void *lldb_private::python::LLDBSWIGPython_CastPyObjectToSBFileSpecList(
+    PyObject *data) {
+  return nullptr;
+}
+
 lldb::ValueObjectSP
 lldb_private::python::SWIGBridge::LLDBSWIGPython_GetValueObjectSPFromSBValue(
     void *data) {


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

Reply via email to