[lldb-dap] Add 'source' references to stack frames without source files. (#128268)

This adds 'source' references to all stack frames. When opening a stack
frame users will see the disassembly of the frame if the source is not
available.

This works around the odd behavior of navigating frames without the
VSCode disassembly view open, which causes 'step' to step in the first
frame with a source instead of the active frame.

This fixes #128260

Old behavior:

https://github.com/user-attachments/assets/3f40582d-ac96-451a-a5ae-498a323bf30e

New behavior:

https://github.com/user-attachments/assets/3a3f9ac6-3e6c-4795-9bb2-1132b3916b6f

---------

Co-authored-by: Jonas Devlieghere <jonas@devlieghere.com>
diff --git a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
index 00e068d..391378c 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py
@@ -1068,6 +1068,19 @@
                 print("[%3u] %s" % (idx, name))
         return response
 
+    def request_source(self, sourceReference):
+        """Request a source from a 'Source' reference."""
+        command_dict = {
+            "command": "source",
+            "type": "request",
+            "arguments": {
+                "source": {"sourceReference": sourceReference},
+                # legacy version of the request
+                "sourceReference": sourceReference,
+            },
+        }
+        return self.send_recv(command_dict)
+
     def request_threads(self):
         """Request a list of all threads and combine any information from any
         "stopped" events since those contain more information about why a
diff --git a/lldb/test/API/tools/lldb-dap/source/Makefile b/lldb/test/API/tools/lldb-dap/source/Makefile
new file mode 100644
index 0000000..1049594
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/source/Makefile
@@ -0,0 +1,3 @@
+C_SOURCES := main.c
+
+include Makefile.rules
diff --git a/lldb/test/API/tools/lldb-dap/source/TestDAP_source.py b/lldb/test/API/tools/lldb-dap/source/TestDAP_source.py
new file mode 100644
index 0000000..edf0af0
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/source/TestDAP_source.py
@@ -0,0 +1,106 @@
+"""
+Test lldb-dap source request
+"""
+
+
+import os
+
+import lldbdap_testcase
+from lldbsuite.test.decorators import *
+from lldbsuite.test.lldbtest import *
+
+
+class TestDAP_source(lldbdap_testcase.DAPTestCaseBase):
+    @skipIfWindows
+    def test_source(self):
+        """
+        Tests the 'source' packet.
+        """
+        program = self.getBuildArtifact("a.out")
+        self.build_and_launch(program)
+        source = self.getSourcePath("main.c")
+        breakpoint_line = line_number(source, "breakpoint")
+
+        lines = [breakpoint_line]
+        breakpoint_ids = self.set_source_breakpoints(source, lines)
+        self.assertEqual(
+            len(breakpoint_ids), len(lines), "expect correct number of breakpoints"
+        )
+
+        self.continue_to_breakpoints(breakpoint_ids)
+
+        response = self.dap_server.request_source(sourceReference=0)
+        self.assertFalse(response["success"], "verify invalid sourceReference fails")
+
+        (stackFrames, totalFrames) = self.get_stackFrames_and_totalFramesCount()
+        frameCount = len(stackFrames)
+        self.assertGreaterEqual(frameCount, 3, "verify we got up to main at least")
+        self.assertEqual(
+            totalFrames,
+            frameCount,
+            "verify total frames returns a speculative page size",
+        )
+        wantFrames = [
+            {
+                "name": "handler",
+                "line": 8,
+                "source": {
+                    "name": "main.c",
+                    "path": source,
+                    "containsSourceReference": False,
+                },
+            },
+            {
+                "name": "add",
+                "source": {
+                    "name": "add",
+                    "path": program + "`add",
+                    "containsSourceReference": True,
+                },
+            },
+            {
+                "name": "main",
+                "line": 12,
+                "source": {
+                    "name": "main.c",
+                    "path": source,
+                    "containsSourceReference": False,
+                },
+            },
+        ]
+        for idx, want in enumerate(wantFrames):
+            got = stackFrames[idx]
+            name = self.get_dict_value(got, ["name"])
+            self.assertEqual(name, want["name"])
+
+            if "line" in want:
+                line = self.get_dict_value(got, ["line"])
+                self.assertEqual(line, want["line"])
+
+            wantSource = want["source"]
+            source_name = self.get_dict_value(got, ["source", "name"])
+            self.assertEqual(source_name, wantSource["name"])
+
+            source_path = self.get_dict_value(got, ["source", "path"])
+            self.assertEqual(source_path, wantSource["path"])
+
+            if wantSource["containsSourceReference"]:
+                sourceReference = self.get_dict_value(
+                    got, ["source", "sourceReference"]
+                )
+                response = self.dap_server.request_source(
+                    sourceReference=sourceReference
+                )
+                self.assertTrue(response["success"])
+                self.assertGreater(
+                    len(self.get_dict_value(response, ["body", "content"])),
+                    0,
+                    "verify content returned disassembly",
+                )
+                self.assertEqual(
+                    self.get_dict_value(response, ["body", "mimeType"]),
+                    "text/x-lldb.disassembly",
+                    "verify mime type returned",
+                )
+            else:
+                self.assertNotIn("sourceReference", got["source"])
diff --git a/lldb/test/API/tools/lldb-dap/source/main.c b/lldb/test/API/tools/lldb-dap/source/main.c
new file mode 100644
index 0000000..39420db
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/source/main.c
@@ -0,0 +1,14 @@
+#include <stdio.h>
+
+__attribute__((nodebug)) static void add(int i, int j, void handler(int)) {
+  handler(i + j);
+}
+
+static void handler(int result) {
+  printf("result %d\n", result); // breakpoint
+}
+
+int main(int argc, char const *argv[]) {
+  add(2, 3, handler);
+  return 0;
+}
diff --git a/lldb/tools/lldb-dap/Handler/SourceRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/SourceRequestHandler.cpp
index 9844d8c..6b587b1 100644
--- a/lldb/tools/lldb-dap/Handler/SourceRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/SourceRequestHandler.cpp
@@ -9,7 +9,15 @@
 #include "DAP.h"
 #include "EventHelper.h"
 #include "JSONUtils.h"
+#include "LLDBUtils.h"
 #include "RequestHandler.h"
+#include "lldb/API/SBFrame.h"
+#include "lldb/API/SBInstructionList.h"
+#include "lldb/API/SBProcess.h"
+#include "lldb/API/SBStream.h"
+#include "lldb/API/SBTarget.h"
+#include "lldb/API/SBThread.h"
+#include "llvm/Support/JSON.h"
 
 namespace lldb_dap {
 
@@ -74,8 +82,37 @@
 void SourceRequestHandler::operator()(const llvm::json::Object &request) const {
   llvm::json::Object response;
   FillResponse(request, response);
-  llvm::json::Object body{{"content", ""}};
-  response.try_emplace("body", std::move(body));
+  const auto *arguments = request.getObject("arguments");
+  const auto *source = arguments->getObject("source");
+  llvm::json::Object body;
+  int64_t source_ref = GetUnsigned(
+      source, "sourceReference", GetUnsigned(arguments, "sourceReference", 0));
+
+  if (source_ref) {
+    lldb::SBProcess process = dap.target.GetProcess();
+    // Upper 32 bits is the thread index ID
+    lldb::SBThread thread =
+        process.GetThreadByIndexID(GetLLDBThreadIndexID(source_ref));
+    // Lower 32 bits is the frame index
+    lldb::SBFrame frame = thread.GetFrameAtIndex(GetLLDBFrameID(source_ref));
+    if (!frame.IsValid()) {
+      response["success"] = false;
+      response["message"] = "source not found";
+    } else {
+      lldb::SBInstructionList insts =
+          frame.GetSymbol().GetInstructions(dap.target);
+      lldb::SBStream stream;
+      insts.GetDescription(stream);
+      body["content"] = stream.GetData();
+      body["mimeType"] = "text/x-lldb.disassembly";
+      response.try_emplace("body", std::move(body));
+    }
+  } else {
+    response["success"] = false;
+    response["message"] =
+        "invalid arguments, expected source.sourceReference to be set";
+  }
+
   dap.SendJSON(llvm::json::Value(std::move(response)));
 }
 
diff --git a/lldb/tools/lldb-dap/JSONUtils.cpp b/lldb/tools/lldb-dap/JSONUtils.cpp
index 6ca4dfb..9f08efb 100644
--- a/lldb/tools/lldb-dap/JSONUtils.cpp
+++ b/lldb/tools/lldb-dap/JSONUtils.cpp
@@ -7,7 +7,6 @@
 //===----------------------------------------------------------------------===//
 
 #include "JSONUtils.h"
-
 #include "BreakpointBase.h"
 #include "DAP.h"
 #include "ExceptionBreakpoint.h"
@@ -41,11 +40,11 @@
 #include "llvm/Support/Compiler.h"
 #include "llvm/Support/Format.h"
 #include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/JSON.h"
 #include "llvm/Support/Path.h"
 #include "llvm/Support/ScopedPrinter.h"
 #include "llvm/Support/raw_ostream.h"
 #include <chrono>
-#include <climits>
 #include <cstddef>
 #include <iomanip>
 #include <optional>
@@ -698,16 +697,6 @@
   return llvm::json::Value(std::move(source));
 }
 
-static std::optional<llvm::json::Value> CreateSource(lldb::SBFrame &frame) {
-  auto line_entry = frame.GetLineEntry();
-  // A line entry of 0 indicates the line is compiler generated i.e. no source
-  // file is associated with the frame.
-  if (line_entry.GetFileSpec().IsValid() && line_entry.GetLine() != 0)
-    return CreateSource(line_entry);
-
-  return {};
-}
-
 // "StackFrame": {
 //   "type": "object",
 //   "description": "A Stackframe contains the source location.",
@@ -799,21 +788,40 @@
 
   EmplaceSafeString(object, "name", frame_name);
 
-  auto source = CreateSource(frame);
-
-  if (source) {
-    object.try_emplace("source", *source);
-    auto line_entry = frame.GetLineEntry();
-    auto line = line_entry.GetLine();
-    if (line && line != LLDB_INVALID_LINE_NUMBER)
-      object.try_emplace("line", line);
-    else
-      object.try_emplace("line", 0);
+  auto line_entry = frame.GetLineEntry();
+  // A line entry of 0 indicates the line is compiler generated i.e. no source
+  // file is associated with the frame.
+  if (line_entry.GetFileSpec().IsValid() &&
+      (line_entry.GetLine() != 0 ||
+       line_entry.GetLine() != LLDB_INVALID_LINE_NUMBER)) {
+    object.try_emplace("source", CreateSource(line_entry));
+    object.try_emplace("line", line_entry.GetLine());
     auto column = line_entry.GetColumn();
     object.try_emplace("column", column);
-  } else {
-    object.try_emplace("line", 0);
-    object.try_emplace("column", 0);
+  } else if (frame.GetSymbol().IsValid()) {
+    // If no source is associated with the frame, use the DAPFrameID to track
+    // the 'source' and generate assembly.
+    llvm::json::Object source;
+    EmplaceSafeString(source, "name", frame_name);
+    char buf[PATH_MAX] = {0};
+    size_t size = frame.GetModule().GetFileSpec().GetPath(buf, PATH_MAX);
+    EmplaceSafeString(source, "path",
+                      std::string(buf, size) + '`' + frame_name);
+    source.try_emplace("sourceReference", MakeDAPFrameID(frame));
+    // Mark the source as deemphasized since users will only be able to view
+    // assembly for these frames.
+    EmplaceSafeString(source, "presentationHint", "deemphasize");
+    object.try_emplace("source", std::move(source));
+
+    // Calculate the line of the current PC from the start of the current
+    // symbol.
+    lldb::addr_t inst_offset = frame.GetPCAddress().GetOffset() -
+                               frame.GetSymbol().GetStartAddress().GetOffset();
+    lldb::addr_t inst_line =
+        inst_offset / (frame.GetThread().GetProcess().GetAddressByteSize() / 2);
+    // Line numbers are 1-based.
+    object.try_emplace("line", inst_line + 1);
+    object.try_emplace("column", 1);
   }
 
   const auto pc = frame.GetPC();