https://github.com/ashgti updated 
https://github.com/llvm/llvm-project/pull/180858

>From 2000bbe4c9ec8cc19e776273dbe4459106f4f505 Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Thu, 29 Jan 2026 16:22:19 -0800
Subject: [PATCH 1/5] [lldb-dap] IO Handling refactor to support raw input
 mode.

Updating lldb-dap to use a pty (when supported) for the debugger input/output 
handles. This enables us to support raw-input mode commands like `script` or 
`breakpoint command add`.

As a fallback on platforms that don't support a pty we use a pipe, which also 
works but has some usability issues. With a pipe, we do NOT see the prompt and 
some commands do not show us any helpful information, e.g. `breakpoint command 
add` does NOT tell you to use `DONE` to end the command. The command does work, 
just not as helpful as with a pty.
With a pty we see the prompt in the console output and it often shows up in the 
command output as well.
---
 lldb/packages/Python/lldbsuite/test/dotest.py |  11 +-
 .../test/tools/lldb-dap/dap_server.py         |  40 ++-
 .../test/tools/lldb-dap/lldbdap_testcase.py   |   4 +-
 .../Interpreter/embedded_interpreter.py       |   3 +
 .../tools/lldb-dap/cancel/TestDAP_cancel.py   |  41 +--
 .../API/tools/lldb-dap/cancel/busy_loop.py    |  17 ++
 .../completions/TestDAP_completions.py        |   3 +-
 .../tools/lldb-dap/console/TestDAP_console.py |   4 +
 .../lldb-dap/evaluate/TestDAP_evaluate.py     |  10 +-
 .../lldb-dap/launch/TestDAP_launch_version.py |   7 +-
 .../lldb-dap/progress/TestDAP_Progress.py     |   5 +-
 .../repl-mode/TestDAP_repl_mode_detection.py  |  13 +-
 .../lldb-dap/send-event/TestDAP_sendEvent.py  |   3 +-
 .../lldb-dap/variables/TestDAP_variables.py   |   8 +-
 lldb/tools/lldb-dap/CMakeLists.txt            |   1 +
 lldb/tools/lldb-dap/DAP.cpp                   | 247 ++++++++++++++----
 lldb/tools/lldb-dap/DAP.h                     |  47 +++-
 lldb/tools/lldb-dap/EvaluateContext.cpp       | 127 +++++++++
 lldb/tools/lldb-dap/EvaluateContext.h         |  48 ++++
 lldb/tools/lldb-dap/EventHelper.cpp           |  52 ++--
 .../Handler/CompletionsRequestHandler.cpp     |   1 +
 .../ConfigurationDoneRequestHandler.cpp       |   3 +
 .../Handler/EvaluateRequestHandler.cpp        | 106 +++-----
 .../Handler/VariablesRequestHandler.cpp       |  62 +++--
 lldb/tools/lldb-dap/ProgressEvent.cpp         |   2 +-
 .../lldb-dap/Protocol/ProtocolRequests.cpp    |   4 +-
 .../lldb-dap/Protocol/ProtocolRequests.h      |   4 +-
 lldb/tools/lldb-dap/Variables.cpp             |  21 ++
 lldb/tools/lldb-dap/Variables.h               |   4 +
 lldb/unittests/DAP/Handler/DisconnectTest.cpp |  11 +-
 lldb/unittests/DAP/TestBase.cpp               |  23 +-
 lldb/unittests/DAP/TestBase.h                 |   1 -
 32 files changed, 679 insertions(+), 254 deletions(-)
 create mode 100644 lldb/test/API/tools/lldb-dap/cancel/busy_loop.py
 create mode 100644 lldb/tools/lldb-dap/EvaluateContext.cpp
 create mode 100644 lldb/tools/lldb-dap/EvaluateContext.h

diff --git a/lldb/packages/Python/lldbsuite/test/dotest.py 
b/lldb/packages/Python/lldbsuite/test/dotest.py
index 533be0a065e3a..f357db9d58f14 100644
--- a/lldb/packages/Python/lldbsuite/test/dotest.py
+++ b/lldb/packages/Python/lldbsuite/test/dotest.py
@@ -569,11 +569,12 @@ def setupSysPath():
 
     lldbDir = os.path.dirname(lldbtest_config.lldbExec)
 
-    lldbDAPExec = os.path.join(
-        lldbDir, "lldb-dap.exe" if sys.platform == "win32" else "lldb-dap"
-    )
-    if is_exe(lldbDAPExec):
-        os.environ["LLDBDAP_EXEC"] = lldbDAPExec
+    if not os.environ.get("LLDBDAP_EXEC", None):
+        lldbDAPExec = os.path.join(
+            lldbDir, "lldb-dap.exe" if sys.platform == "win32" else "lldb-dap"
+        )
+        if is_exe(lldbDAPExec):
+            os.environ["LLDBDAP_EXEC"] = lldbDAPExec
 
     configuration.yaml2macho_core = shutil.which("yaml2macho-core", 
path=lldbDir)
 
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 840c087ceec4d..f3efc7b23214b 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
@@ -299,7 +299,7 @@ def __init__(
         self.threads: Optional[dict] = None
         self.stopped_thread: Optional[dict] = None
         self.thread_stacks: Optional[Dict[int, List[dict]]]
-        self.thread_stop_reasons: Dict[str, Any] = {}
+        self.thread_stop_reasons: Dict[int, Any] = {}
         self.focused_tid: Optional[int] = None
         self.frame_scopes: Dict[str, Any] = {}
         # keyed by breakpoint id
@@ -486,9 +486,10 @@ def _process_recv_packets(self) -> None:
         with self._recv_condition:
             for packet in self._recv_packets:
                 if packet and ("seq" not in packet or packet["seq"] == 0):
-                    raise ValueError(
-                        f"received a malformed packet, expected 'seq != 0' for 
{packet!r}"
-                    )
+                    pass
+                    # raise ValueError(
+                    #     f"received a malformed packet, expected 'seq != 0' 
for {packet!r}"
+                    # )
                 if packet:
                     self._log.messages.append((Log.Dir.RECV, packet))
                 # Handle events that may modify any stateful properties of
@@ -540,10 +541,13 @@ def _handle_event(self, packet: Event) -> None:
             # reasons since the 'threads' command doesn't return
             # that information.
             self._process_stopped()
-            tid = body["threadId"]
-            self.thread_stop_reasons[tid] = body
-            if "preserveFocusHint" not in body or not 
body["preserveFocusHint"]:
-                self.focused_tid = tid
+            if "allThreadsStopped" in body and body["allThreadsStopped"]:
+                self.thread_stop_reasons = {}
+            if "threadId" in body:
+                tid = body["threadId"]
+                self.thread_stop_reasons[tid] = body
+                if "preserveFocusHint" not in body or not 
body["preserveFocusHint"]:
+                    self.focused_tid = tid
         elif event.startswith("progress"):
             # Progress events come in as 'progressStart', 'progressUpdate',
             # and 'progressEnd' events. Keep these around in case test
@@ -663,11 +667,13 @@ def predicate(p: ProtocolMessage):
 
         return cast(Response, self._recv_packet(predicate=predicate))
 
-    def wait_for_event(self, filter: List[str]) -> Event:
+    def wait_for_event(
+        self, filter: List[str], pred: Callable[[Event], bool] = lambda _: True
+    ) -> Event:
         """Wait for the first event that matches the filter."""
 
         def predicate(p: ProtocolMessage):
-            return p["type"] == "event" and p["event"] in filter
+            return p["type"] == "event" and p["event"] in filter and pred(p)
 
         return cast(Event, self._recv_packet(predicate=predicate))
 
@@ -706,6 +712,18 @@ def ensure_initialized(self) -> None:
         ), "configuration done has already been sent, 'initialized' should 
have already occurred"
         self.wait_for_initialized()
 
+    def collect_progress(self, title: str) -> List[Event]:
+        start_event = self.wait_for_event(
+            ["progressStart"], lambda event: event["body"]["title"] == title
+        )
+        progress_id = start_event["body"]["progressId"]
+        self.wait_for_event(
+            ["progressEnd"], lambda event: event["body"]["progressId"] == 
progress_id
+        )
+        return [
+            e for e in self.progress_events if e["body"]["progressId"] == 
progress_id
+        ]
+
     def wait_for_stopped(self) -> List[Event]:
         """Wait for the next 'stopped' event to occur, coalescing all stopped 
events within a given quiet period."""
         return self.collect_events(["stopped", "exited"])
@@ -1162,6 +1180,8 @@ def request_evaluate(
             args_dict["frameId"] = stackFrame["id"]
 
         if context:
+            if context == "repl" and not self.configuration_done_sent:
+                raise ValueError("configurtionDone must be called to activate 
the repl")
             args_dict["context"] = context
         if is_hex is not None:
             args_dict["format"] = {"hex": is_hex}
diff --git 
a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py 
b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
index f3c16bd849a48..daa3823cac77e 100644
--- a/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
+++ b/lldb/packages/Python/lldbsuite/test/tools/lldb-dap/lldbdap_testcase.py
@@ -241,11 +241,13 @@ def verify_stop_exception_info(
     def verify_stop_on_entry(self) -> None:
         """Waits for the process to be stopped and then verifies at least one
         thread has the stop reason 'entry'."""
+        if not self.dap_server.configuration_done_sent:
+            self.verify_configuration_done()
         self.dap_server.wait_for_stopped()
         self.assertIn(
             "entry",
             (t["reason"] for t in 
self.dap_server.thread_stop_reasons.values()),
-            "Expected at least one thread to report stop reason 'entry' in 
{self.dap_server.thread_stop_reasons}",
+            f"Expected at least one thread to report stop reason 'entry' in 
{self.dap_server.thread_stop_reasons}",
         )
 
     def verify_commands(self, flavor: str, output: str, commands: List[str]):
diff --git a/lldb/source/Interpreter/embedded_interpreter.py 
b/lldb/source/Interpreter/embedded_interpreter.py
index 12c47bd712816..b8d0cfeccd94e 100644
--- a/lldb/source/Interpreter/embedded_interpreter.py
+++ b/lldb/source/Interpreter/embedded_interpreter.py
@@ -49,6 +49,8 @@ def readfunc(prompt):
 
 
 def readfunc_stdio(prompt):
+    # Flush stdout and stderr to ensure consistency.
+    sys.stderr.flush()
     sys.stdout.write(prompt)
     sys.stdout.flush()
     line = sys.stdin.readline()
@@ -77,6 +79,7 @@ def run_python_interpreter(local_dict):
         if e.code:
             print("Script exited with code %s" % e.code)
 
+
 def run_one_line(local_dict, input_string):
     global g_run_one_line_str
     try:
diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py 
b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
index 14789a6694686..8429941ad574f 100644
--- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
+++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
@@ -2,10 +2,8 @@
 Test lldb-dap cancel request
 """
 
+import os
 import time
-
-from lldbsuite.test.decorators import *
-from lldbsuite.test.lldbtest import *
 import lldbdap_testcase
 
 
@@ -13,25 +11,21 @@ class TestDAP_cancel(lldbdap_testcase.DAPTestCaseBase):
     def send_async_req(self, command: str, arguments: dict = {}) -> int:
         return self.dap_server.send_packet(
             {
+                "seq": 0,
                 "type": "request",
                 "command": command,
                 "arguments": arguments,
             }
         )
 
-    def async_blocking_request(self, duration: float) -> int:
+    def async_blocking_request(self, count: int) -> int:
         """
-        Sends an evaluate request that will sleep for the specified duration to
+        Sends an evaluate request that will sleep for the specified count to
         block the request handling thread.
         """
         return self.send_async_req(
             command="evaluate",
-            arguments={
-                "expression": '`script import time; print("starting sleep", 
file=lldb.debugger.GetOutputFileHandle()); time.sleep({})'.format(
-                    duration
-                ),
-                "context": "repl",
-            },
+            arguments={"expression": f"`busy-loop {count}", "context": "repl"},
         )
 
     def async_cancel(self, requestId: int) -> int:
@@ -42,14 +36,20 @@ def test_pending_request(self):
         Tests cancelling a pending request.
         """
         program = self.getBuildArtifact("a.out")
-        self.build_and_launch(program)
+        busy_loop = os.path.join(os.path.dirname(__file__), "busy_loop.py")
+        self.build_and_launch(
+            program,
+            initCommands=[f"command script import {busy_loop}"],
+            stopOnEntry=True,
+        )
+        self.verify_stop_on_entry()
 
         # Use a relatively short timeout since this is only to ensure the
         # following request is queued.
-        blocking_seq = 
self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 10)
+        blocking_seq = self.async_blocking_request(count=1)
         # Use a longer timeout to ensure we catch if the request was 
interrupted
         # properly.
-        pending_seq = 
self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 2)
+        pending_seq = self.async_blocking_request(count=10)
         cancel_seq = self.async_cancel(requestId=pending_seq)
 
         blocking_resp = self.dap_server.receive_response(blocking_seq)
@@ -74,11 +74,18 @@ def test_inflight_request(self):
         Tests cancelling an inflight request.
         """
         program = self.getBuildArtifact("a.out")
-        self.build_and_launch(program)
+        busy_loop = os.path.join(os.path.dirname(__file__), "busy_loop.py")
+        self.build_and_launch(
+            program,
+            initCommands=[f"command script import {busy_loop}"],
+            stopOnEntry=True,
+        )
+        self.verify_configuration_done()
+        self.verify_stop_on_entry()
 
-        blocking_seq = 
self.async_blocking_request(duration=self.DEFAULT_TIMEOUT / 2)
+        blocking_seq = self.async_blocking_request(count=10)
         # Wait for the sleep to start to cancel the inflight request.
-        self.collect_console(pattern="starting sleep")
+        time.sleep(0.5)
         cancel_seq = self.async_cancel(requestId=blocking_seq)
 
         blocking_resp = self.dap_server.receive_response(blocking_seq)
diff --git a/lldb/test/API/tools/lldb-dap/cancel/busy_loop.py 
b/lldb/test/API/tools/lldb-dap/cancel/busy_loop.py
new file mode 100644
index 0000000000000..21108d7e32e45
--- /dev/null
+++ b/lldb/test/API/tools/lldb-dap/cancel/busy_loop.py
@@ -0,0 +1,17 @@
+import time
+import lldb
+
+
[email protected](command_name="busy-loop")
+def busy_loop(debugger, command, exe_ctx, result, internal_dict):
+    """Test helper as a busy loop."""
+    if not command:
+        command = "10"
+    count = int(command)
+    print("Starting loop...", count)
+    for i in range(count):
+        if debugger.InterruptRequested():
+            print("interrupt requested, stopping loop", i)
+            break
+        print("No interrupted requested, sleeping", i)
+        time.sleep(1)
diff --git a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py 
b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py
index dd5fdcc4bbad4..db7d04829f046 100644
--- a/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py
+++ b/lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py
@@ -377,14 +377,13 @@ def test_auto_completions(self):
         Tests completion requests in "repl-mode=auto"
         """
         self.setup_debuggee()
+        self.continue_to_next_stop()
 
         res = self.dap_server.request_evaluate(
             "`lldb-dap repl-mode auto", context="repl"
         )
         self.assertTrue(res["success"])
 
-        self.continue_to_next_stop()
-
         # Stopped at breakpoint 1
         # 'var' variable is in scope, completions should not show any warning.
         self.dap_server.get_completions("var ")
diff --git a/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py 
b/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py
index ceddaeb50cd3b..cd0a8e715de42 100644
--- a/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py
+++ b/lldb/test/API/tools/lldb-dap/console/TestDAP_console.py
@@ -164,8 +164,12 @@ def test_exit_status_message_ok(self):
         )
 
     def test_diagnositcs(self):
+        source = "main.cpp"
         program = self.getBuildArtifact("a.out")
         self.build_and_launch(program)
+        breakpoint1_line = line_number(source, "// breakpoint 1")
+        breakpoint_ids = self.set_source_breakpoints(source, 
[breakpoint1_line])
+        self.continue_to_breakpoints(breakpoint_ids)
 
         core = self.getBuildArtifact("minidump.core")
         self.yaml2obj("minidump.yaml", core)
diff --git a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py 
b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
index 98ed0b9df228a..e2e940c21ad0e 100644
--- a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
+++ b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
@@ -138,11 +138,12 @@ def run_test_evaluate_expressions(
         self.assertEvaluate("var1", "20", want_type="int")
         # Empty expression should equate to the previous expression.
         if context == "repl":
-            self.assertEvaluate("", "20")
+            self.assertEvaluate("", r"\(lldb\) ", want_memref=False)
         else:
             self.assertEvaluateFailure("")
         self.assertEvaluate("var2", "21", want_type="int")
         if context == "repl":
+            self.assertEvaluate("e var2", "21", want_memref=False)
             self.assertEvaluate("", "21", want_type="int")
             self.assertEvaluate("", "21", want_type="int")
         self.assertEvaluate("static_int", "0x0000002a", want_type="int", 
is_hex=True)
@@ -210,15 +211,14 @@ def run_test_evaluate_expressions(
                 "struct3", "0x.*0", want_varref=True, want_type="my_struct *"
             )
 
-        if context == "repl" or context is None:
-            # In repl or unknown context expressions may be interpreted as lldb
-            # commands since no variables have the same name as the command.
+        if context == "repl":
+            # In repl context expressions may be interpreted as lldb commands
+            # since no variables have the same name as the command.
             self.assertEvaluate("list", r".*", want_memref=False)
             # Changing the frame index should not make a difference
             self.assertEvaluate(
                 "version", r".*lldb.+", want_memref=False, frame_index=1
             )
-
         else:
             self.assertEvaluateFailure("list")  # local variable of a_function
 
diff --git a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py 
b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py
index a598db41595a5..b57cff45a6ec6 100644
--- a/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py
+++ b/lldb/test/API/tools/lldb-dap/launch/TestDAP_launch_version.py
@@ -30,7 +30,12 @@ def test(self):
         version_eval_response = self.dap_server.request_evaluate(
             "`version", context="repl"
         )
-        version_eval_output = version_eval_response["body"]["result"]
+        version_eval_output = (
+            version_eval_response["body"]["result"]
+            .removeprefix("(lldb) ")
+            .removesuffix("(lldb) ")
+        )
+        # Strip the prompt which may show up in the repl output.
 
         version_string = self.dap_server.get_capability("$__lldb_version")
         self.assertEqual(
diff --git a/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py 
b/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
index 3f57dfb66024d..7a1b6608c1e43 100755
--- a/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
+++ b/lldb/test/API/tools/lldb-dap/progress/TestDAP_Progress.py
@@ -21,12 +21,10 @@ def verify_progress_events(
         expected_not_in_message=None,
         only_verify_first_update=False,
     ):
-        self.dap_server.wait_for_event(["progressEnd"])
-        self.assertTrue(len(self.dap_server.progress_events) > 0)
         start_found = False
         update_found = False
         end_found = False
-        for event in self.dap_server.progress_events:
+        for event in self.dap_server.collect_progress(expected_title):
             event_type = event["event"]
             if "progressStart" in event_type:
                 title = event["body"]["title"]
@@ -55,6 +53,7 @@ def verify_progress_events(
     def test(self):
         program = self.getBuildArtifact("a.out")
         self.build_and_launch(program, stopOnEntry=True)
+        self.verify_stop_on_entry()
         progress_emitter = os.path.join(os.getcwd(), "Progress_emitter.py")
         self.dap_server.request_evaluate(
             f"`command script import {progress_emitter}", context="repl"
diff --git 
a/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py 
b/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py
index c6f59949d668e..d2d99b6457b77 100644
--- a/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py
+++ b/lldb/test/API/tools/lldb-dap/repl-mode/TestDAP_repl_mode_detection.py
@@ -3,8 +3,6 @@
 """
 
 import lldbdap_testcase
-import dap_server
-from lldbsuite.test import lldbutil
 from lldbsuite.test.decorators import *
 from lldbsuite.test.lldbtest import *
 
@@ -20,20 +18,23 @@ def assertEvaluate(self, expression, regex):
 
     def test_completions(self):
         program = self.getBuildArtifact("a.out")
-        self.build_and_launch(program)
+        self.build_and_launch(program, stopOnEntry=True)
 
         source = "main.cpp"
         breakpoint1_line = line_number(source, "// breakpoint 1")
         breakpoint2_line = line_number(source, "// breakpoint 2")
 
         self.set_source_breakpoints(source, [breakpoint1_line, 
breakpoint2_line])
+        self.verify_stop_on_entry()
 
         # The result of the commands should return the empty string.
-        self.assertEvaluate("`command regex user_command s/^$/platform/", 
r"^$")
-        self.assertEvaluate("`command alias alias_command platform", r"^$")
+        self.assertEvaluate(
+            "`command regex user_command s/^$/platform/", r"^\(lldb\) $"
+        )
+        self.assertEvaluate("`command alias alias_command platform", 
r"^\(lldb\) $")
         self.assertEvaluate(
             "`command alias alias_command_with_arg platform select --sysroot 
%1 remote-linux",
-            r"^$",
+            r"^\(lldb\) $",
         )
 
         self.continue_to_next_stop()
diff --git a/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py 
b/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py
index a01845669666f..ae211b36f7c0d 100644
--- a/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py
+++ b/lldb/test/API/tools/lldb-dap/send-event/TestDAP_sendEvent.py
@@ -61,7 +61,8 @@ def test_send_internal_event(self):
         resp = self.dap_server.request_evaluate(
             "`lldb-dap send-event stopped", context="repl"
         )
+        self.assertFalse(resp["success"], f"Expected error response in {resp}")
         self.assertRegex(
-            resp["body"]["result"],
+            resp["body"]["error"]["format"],
             r"Invalid use of lldb-dap send-event, event \"stopped\" should be 
handled by lldb-dap internally.",
         )
diff --git a/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py 
b/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py
index 1dbb0143e7a55..7e563d1ea402d 100644
--- a/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py
+++ b/lldb/test/API/tools/lldb-dap/variables/TestDAP_variables.py
@@ -139,8 +139,7 @@ def darwin_dwarf_missing_obj(self, initCommands):
                 },
             },
         }
-        varref_dict = {}
-        self.verify_variables(verify_locals, locals, varref_dict)
+        self.verify_variables(verify_locals, locals)
 
     def do_test_scopes_variables_setVariable_evaluate(
         self, enableAutoVariableSummaries: bool
@@ -580,8 +579,9 @@ def do_test_scopes_and_evaluate_expansion(self, 
enableAutoVariableSummaries: boo
 
         var_ref = expr_varref_dict[expandable_expression["name"]]
         response = self.dap_server.request_variables(var_ref)
-        self.verify_variables(
-            expandable_expression["children"], response["body"]["variables"]
+        self.assertFalse(
+            response["success"],
+            f"variable reference should be out of scope in {response}",
         )
 
         # Test that frame scopes have corresponding presentation hints.
diff --git a/lldb/tools/lldb-dap/CMakeLists.txt 
b/lldb/tools/lldb-dap/CMakeLists.txt
index 8566c663fcc06..47ab3669442f7 100644
--- a/lldb/tools/lldb-dap/CMakeLists.txt
+++ b/lldb/tools/lldb-dap/CMakeLists.txt
@@ -11,6 +11,7 @@ add_lldb_library(lldbDAP
   DAPError.cpp
   DAPLog.cpp
   DAPSessionManager.cpp
+  EvaluateContext.cpp
   EventHelper.cpp
   ExceptionBreakpoint.cpp
   FifoFiles.cpp
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 5ef384706aab0..4dd61d6cf6320 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -17,22 +17,21 @@
 #include "LLDBUtils.h"
 #include "OutputRedirector.h"
 #include "Protocol/ProtocolBase.h"
-#include "Protocol/ProtocolEvents.h"
 #include "Protocol/ProtocolRequests.h"
 #include "Protocol/ProtocolTypes.h"
 #include "ProtocolUtils.h"
-#include "Transport.h"
-#include "lldb/API/SBBreakpoint.h"
 #include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBCommandInterpreterRunOptions.h"
 #include "lldb/API/SBEvent.h"
 #include "lldb/API/SBLanguageRuntime.h"
 #include "lldb/API/SBListener.h"
 #include "lldb/API/SBMutex.h"
 #include "lldb/API/SBProcess.h"
-#include "lldb/API/SBStream.h"
-#include "lldb/Host/JSONTransport.h"
+#include "lldb/Host/File.h"
 #include "lldb/Host/MainLoop.h"
 #include "lldb/Host/MainLoopBase.h"
+#include "lldb/Host/Pipe.h"
+#include "lldb/Host/PseudoTerminal.h"
 #include "lldb/Utility/Status.h"
 #include "lldb/lldb-defines.h"
 #include "lldb/lldb-enumerations.h"
@@ -56,12 +55,13 @@
 #include <cstdint>
 #include <cstdio>
 #include <functional>
-#include <future>
 #include <memory>
 #include <mutex>
 #include <optional>
 #include <string>
+#include <sys/fcntl.h>
 #include <thread>
+#include <tuple>
 #include <utility>
 #include <variant>
 
@@ -78,14 +78,6 @@ using namespace lldb_dap;
 using namespace lldb_dap::protocol;
 using namespace lldb_private;
 
-namespace {
-#ifdef _WIN32
-const char DEV_NULL[] = "nul";
-#else
-const char DEV_NULL[] = "/dev/null";
-#endif
-} // namespace
-
 namespace lldb_dap {
 
 static std::string GetStringFromStructuredData(lldb::SBStructuredData &data,
@@ -224,17 +216,92 @@ ExceptionBreakpoint *DAP::GetExceptionBreakpoint(const 
lldb::break_id_t bp_id) {
   return nullptr;
 }
 
+llvm::Expected<std::pair<IOWrapper, IOWrapper>>
+IOWrapper::CreatePseudoTerminal() {
+  // For testing purposes return an error to cause us to fallback to a pair of
+  // pipes.
+  if (getenv("LLDB_DAP_FORCE_REPL_PIPE"))
+    return llvm::createStringError("forced repl pipe");
+
+  lldb_private::PseudoTerminal pty;
+  if (auto err = pty.OpenFirstAvailablePrimary(O_RDWR | O_NOCTTY | O_CLOEXEC))
+    return err;
+  if (auto err = pty.OpenSecondary(O_RDWR | O_NOCTTY | O_CLOEXEC))
+    return err;
+
+  lldb_private::Terminal term(pty.GetPrimaryFileDescriptor());
+  if (llvm::Error err = term.SetCanonical(false))
+    return err;
+  if (llvm::Error err = term.SetRaw())
+    return err;
+
+  int primary_fd = pty.ReleasePrimaryFileDescriptor();
+  int replica_fd = pty.ReleaseSecondaryFileDescriptor();
+
+  return std::make_pair(
+      IOWrapper(
+          /*read=*/std::make_shared<lldb_private::NativeFile>(
+              primary_fd, lldb_private::File::eOpenOptionReadOnly, false),
+          /*write=*/std::make_shared<lldb_private::NativeFile>(
+              primary_fd, lldb_private::File::eOpenOptionWriteOnly, true)),
+      IOWrapper(
+          /*read=*/std::make_shared<lldb_private::NativeFile>(
+              replica_fd, lldb_private::File::eOpenOptionReadOnly, false),
+          /*write=*/std::make_shared<lldb_private::NativeFile>(
+              replica_fd, lldb_private::File::eOpenOptionWriteOnly, true)));
+}
+
+// Hookup a pair of pipes such that reading from one writes to the other.
+llvm::Expected<std::pair<IOWrapper, IOWrapper>> IOWrapper::CreatePipePair() {
+  lldb_private::Pipe primary_pipe, replica_pipe;
+  if (auto err = primary_pipe.CreateNew().takeError())
+    return err;
+  if (auto err = replica_pipe.CreateNew().takeError())
+    return err;
+
+  return std::make_pair(
+      IOWrapper(/*read=*/
+                std::make_shared<lldb_private::NativeFile>(
+                    primary_pipe.ReleaseReadFileDescriptor(),
+                    lldb_private::File::eOpenOptionReadOnly, false),
+                /*write=*/std::make_shared<lldb_private::NativeFile>(
+                    replica_pipe.ReleaseWriteFileDescriptor(),
+                    lldb_private::File::eOpenOptionWriteOnly, true)),
+      IOWrapper(/*read=*/
+                std::make_shared<lldb_private::NativeFile>(
+                    replica_pipe.ReleaseReadFileDescriptor(),
+                    lldb_private::File::eOpenOptionReadOnly, false),
+                /*write=*/std::make_shared<lldb_private::NativeFile>(
+                    primary_pipe.ReleaseWriteFileDescriptor(),
+                    lldb_private::File::eOpenOptionWriteOnly, true)));
+}
+
 llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, std::FILE *overrideErr) {
-  in = lldb::SBFile(std::fopen(DEV_NULL, "r"), /*transfer_ownership=*/true);
+  DAP_LOG(log, "Initializing IO");
+
+  llvm::Expected<std::pair<IOWrapper, IOWrapper>> io =
+      IOWrapper::CreatePseudoTerminal();
+  if (!io) {
+    DAP_LOG_ERROR(
+        log, io.takeError(),
+        "Falling back to a pipe for debugger IO, creating a pty failed: {0}");
+    io = IOWrapper::CreatePipePair();
+    if (!io)
+      return io.takeError();
+  }
+
+  std::tie(primary, replica) = *io;
 
-  if (auto Error = out.RedirectTo(overrideOut, [this](llvm::StringRef output) {
-        SendOutput(OutputType::Console, output);
-      }))
+  if (auto Error = stdout_redirect.RedirectTo(
+          overrideOut, [this](llvm::StringRef output) {
+            SendOutput(OutputType::Console, output);
+          }))
     return Error;
 
-  if (auto Error = err.RedirectTo(overrideErr, [this](llvm::StringRef output) {
-        SendOutput(OutputType::Console, output);
-      }))
+  if (auto Error = stderr_redirect.RedirectTo(
+          overrideErr, [this](llvm::StringRef output) {
+            SendOutput(OutputType::Console, output);
+          }))
     return Error;
 
   return llvm::Error::success();
@@ -596,6 +663,9 @@ lldb::SBFrame DAP::GetLLDBFrame(const llvm::json::Object 
&arguments) {
 
 ReplMode DAP::DetectReplMode(lldb::SBFrame &frame, std::string &expression,
                              bool partial_expression) {
+  if (!debugger.GetCommandInterpreter().IsActive() || expression.empty())
+    return ReplMode::Command;
+
   // Check for the escape hatch prefix.
   if (llvm::StringRef expr_ref = expression;
       expr_ref.consume_front(configuration.commandEscapePrefix)) {
@@ -606,6 +676,9 @@ ReplMode DAP::DetectReplMode(lldb::SBFrame &frame, 
std::string &expression,
   if (repl_mode != ReplMode::Auto)
     return repl_mode;
 
+  if (expression.empty() && (repl_mode != ReplMode::Variable))
+    return ReplMode::Command;
+
   // We cannot check if expression is a variable without a frame.
   if (!frame)
     return ReplMode::Command;
@@ -947,6 +1020,7 @@ llvm::Error DAP::Disconnect(bool terminateDebuggee) {
   }
 
   SendTerminatedEvent();
+  debugger.Clear();
   TerminateLoop();
   return ToError(error);
 }
@@ -959,7 +1033,7 @@ bool DAP::IsCancelled(const protocol::Request &req) {
 void DAP::ClearCancelRequest(const CancelArguments &args) {
   std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
   if (args.requestId)
-    m_cancelled_requests.erase(*args.requestId);
+    m_cancelled_requests.erase(args.requestId);
 }
 
 template <typename T>
@@ -990,7 +1064,7 @@ void DAP::Received(const protocol::Request &request) {
     {
       std::lock_guard<std::mutex> guard(m_cancelled_requests_mutex);
       if (cancel_args->requestId)
-        m_cancelled_requests.insert(*cancel_args->requestId);
+        m_cancelled_requests.insert(cancel_args->requestId);
     }
 
     // If a cancel is requested for the active request, make a best
@@ -1000,6 +1074,22 @@ void DAP::Received(const protocol::Request &request) {
       DAP_LOG(log, "interrupting inflight request (command={0} seq={1})",
               m_active_request->command, m_active_request->seq);
       debugger.RequestInterrupt();
+      debugger.DispatchInputInterrupt();
+      debugger.GetCommandInterpreter().InterruptCommand();
+    }
+
+    // Making a best effort attempt at interrupting in-progress events.
+    if (!cancel_args->progressId.empty()) {
+      DAP_LOG(log, "cancel in progress event, interrupt debugger");
+      // FIXME: Which of these are actually required, they all have different
+      // behavior.
+      debugger.RequestInterrupt();
+      debugger.DispatchInputInterrupt();
+      debugger.GetCommandInterpreter().InterruptCommand();
+
+      std::lock_guard<std::mutex> guard(m_eval_mutex);
+      if (m_evaluate_context)
+        m_evaluate_context->Interrupt();
     }
   }
 
@@ -1055,6 +1145,40 @@ void DAP::TransportHandler() {
     return;
   }
 
+  Status status;
+  m_out_handle = m_loop.RegisterReadObject(
+      primary.read,
+      [this](auto &loop) {
+        std::array<char, OutputBufferSize> buf = {0};
+        size_t num_bytes = buf.size();
+        if (auto err = primary.read->Read(buf.data(), num_bytes).takeError()) {
+          DAP_LOG_ERROR(log, std::move(err), "read failed {0}");
+          return;
+        }
+
+        // EOF detected
+        if (num_bytes == 0) {
+          m_out_handle.reset();
+          return;
+        }
+
+        std::lock_guard<std::mutex> guard(m_eval_mutex);
+
+        if (m_evaluate_context &&
+            m_evaluate_context->HandleOutput({buf.data(), num_bytes}))
+          return;
+
+        SendOutput(OutputType::Console, {buf.data(), num_bytes});
+      },
+      status);
+
+  if (llvm::Error err = status.takeError()) {
+    DAP_LOG_ERROR(log, std::move(err), "registering pty failed: {0}");
+    std::lock_guard<std::mutex> guard(m_queue_mutex);
+    m_error_occurred = true;
+    return;
+  }
+
   if (Status status = m_loop.Run(); status.Fail()) {
     DAP_LOG_ERROR(log, status.takeError(), "MainLoop run failed: {0}");
     std::lock_guard<std::mutex> guard(m_queue_mutex);
@@ -1063,6 +1187,17 @@ void DAP::TransportHandler() {
   }
 }
 
+void DAP::ActivateRepl() {
+  lldb::SBCommandInterpreterRunOptions options;
+  options.SetSpawnThread(true);
+  options.SetAutoHandleEvents(false);
+  options.SetEchoCommands(false);
+  options.SetEchoCommentCommands(false);
+  options.SetAllowRepeats(true);
+  options.SetAddToHistory(true);
+  debugger.RunCommandInterpreter(options);
+}
+
 llvm::Error DAP::Loop() {
   {
     // Reset disconnect flag once we start the loop.
@@ -1074,8 +1209,8 @@ llvm::Error DAP::Loop() {
 
   llvm::scope_exit cleanup([this]() {
     // FIXME: Merge these into the MainLoop handler.
-    out.Stop();
-    err.Stop();
+    stdout_redirect.Stop();
+    stderr_redirect.Stop();
     StopEventHandlers();
 
     // Destroy the debugger when the session ends. This will trigger the
@@ -1246,6 +1381,7 @@ protocol::Capabilities DAP::GetCapabilities() {
       protocol::eAdapterFeatureLogPoints,
       protocol::eAdapterFeatureSteppingGranularity,
       protocol::eAdapterFeatureValueFormattingOptions,
+      protocol::eAdapterFeatureSingleThreadExecutionRequests,
   };
 
   // Capabilities associated with specific requests.
@@ -1343,18 +1479,44 @@ llvm::Error DAP::InitializeDebugger() {
   debugger = lldb::SBDebugger::Create(/*argument_name=*/false);
 
   // Configure input/output/error file descriptors.
-  debugger.SetInputFile(in);
+  debugger.SetInputFile(replica.read);
+  debugger.SetOutputFile(replica.write);
+  debugger.SetErrorFile(replica.write);
+
   target = debugger.GetDummyTarget();
 
-  llvm::Expected<int> out_fd = out.GetWriteFileDescriptor();
-  if (!out_fd)
-    return out_fd.takeError();
-  debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));
+  auto cmd = debugger.GetCommandInterpreter().AddMultiwordCommand(
+      "lldb-dap", "Commands for managing lldb-dap.");
+
+  if (clientFeatures.contains(eClientFeatureStartDebuggingRequest)) {
+    cmd.AddCommand(
+        "start-debugging", new StartDebuggingCommand(*this),
+        "Sends a startDebugging request from the debug adapter to the client "
+        "to start a child debug session of the same type as the caller.");
+  }
+
+  cmd.AddCommand(
+      "repl-mode", new ReplModeCommand(*this),
+      "Get or set the repl behavior of lldb-dap evaluation requests.");
+  cmd.AddCommand("send-event", new SendEventCommand(*this),
+                 "Sends an DAP event to the client.");
+
+  StartEventThreads();
+
+  // Set the print callback to allow us to intercept command return objects 
from
+  // the repl to provide a more detailed view of the output.
+  debugger.GetCommandInterpreter().SetPrintCallback(
+      [](lldb::SBCommandReturnObject &result, void *baton) {
+        DAP *dap = static_cast<DAP *>(baton);
+        std::lock_guard<std::mutex> guard(dap->m_eval_mutex);
 
-  llvm::Expected<int> err_fd = err.GetWriteFileDescriptor();
-  if (!err_fd)
-    return err_fd.takeError();
-  debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));
+        if (dap->m_evaluate_context &&
+            dap->m_evaluate_context->HandleReturnObject(result))
+          return lldb::eCommandReturnObjectPrintCallbackHandled;
+
+        return lldb::eCommandReturnObjectPrintCallbackSkipped;
+      },
+      this);
 
   // The sourceInitFile option is not part of the DAP specification. It is an
   // extension used by the test suite to prevent sourcing `.lldbinit` and
@@ -1374,23 +1536,6 @@ llvm::Error DAP::InitializeDebugger() {
   if (llvm::Error err = RunPreInitCommands())
     return err;
 
-  auto cmd = debugger.GetCommandInterpreter().AddMultiwordCommand(
-      "lldb-dap", "Commands for managing lldb-dap.");
-
-  if (clientFeatures.contains(eClientFeatureStartDebuggingRequest)) {
-    cmd.AddCommand(
-        "start-debugging", new StartDebuggingCommand(*this),
-        "Sends a startDebugging request from the debug adapter to the client "
-        "to start a child debug session of the same type as the caller.");
-  }
-
-  cmd.AddCommand(
-      "repl-mode", new ReplModeCommand(*this),
-      "Get or set the repl behavior of lldb-dap evaluation requests.");
-  cmd.AddCommand("send-event", new SendEventCommand(*this),
-                 "Sends an DAP event to the client.");
-
-  StartEventThreads();
   return llvm::Error::success();
 }
 
diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h
index 657105c8dd6b9..153f2826c8045 100644
--- a/lldb/tools/lldb-dap/DAP.h
+++ b/lldb/tools/lldb-dap/DAP.h
@@ -11,6 +11,7 @@
 
 #include "DAPForward.h"
 #include "DAPSessionManager.h"
+#include "EvaluateContext.h"
 #include "ExceptionBreakpoint.h"
 #include "FunctionBreakpoint.h"
 #include "InstructionBreakpoint.h"
@@ -23,16 +24,15 @@
 #include "Transport.h"
 #include "Variables.h"
 #include "lldb/API/SBBroadcaster.h"
-#include "lldb/API/SBCommandInterpreter.h"
 #include "lldb/API/SBDebugger.h"
 #include "lldb/API/SBError.h"
-#include "lldb/API/SBFile.h"
 #include "lldb/API/SBFormat.h"
 #include "lldb/API/SBFrame.h"
 #include "lldb/API/SBMutex.h"
 #include "lldb/API/SBTarget.h"
 #include "lldb/API/SBThread.h"
 #include "lldb/Host/MainLoop.h"
+#include "lldb/lldb-forward.h"
 #include "lldb/lldb-types.h"
 #include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/DenseSet.h"
@@ -81,6 +81,20 @@ enum class ReplMode { Variable = 0, Command, Auto };
 
 using DAPTransport = 
lldb_private::transport::JSONTransport<ProtocolDescriptor>;
 
+/// Wraps a set of file handles for reading and writing to a pair of handlers.
+/// Typically a pty on supported platforms or a pair of pipes as a fallback.
+struct IOWrapper {
+  IOWrapper() = default;
+  IOWrapper(lldb::FileSP read, lldb::FileSP write)
+      : read(read), write(write) {};
+
+  static llvm::Expected<std::pair<IOWrapper, IOWrapper>> 
CreatePseudoTerminal();
+  static llvm::Expected<std::pair<IOWrapper, IOWrapper>> CreatePipePair();
+
+  lldb::FileSP read;
+  lldb::FileSP write;
+};
+
 struct DAP final : public DAPTransport::MessageHandler {
   friend class DAPSessionManager;
 
@@ -89,9 +103,16 @@ struct DAP final : public DAPTransport::MessageHandler {
 
   Log &log;
   DAPTransport &transport;
-  lldb::SBFile in;
-  OutputRedirector out;
-  OutputRedirector err;
+
+  /// pty handles used for communicating with the debugger input / output
+  /// IOHandler.
+  /// @{
+  IOWrapper primary;
+  IOWrapper replica;
+  /// @}
+
+  OutputRedirector stdout_redirect;
+  OutputRedirector stderr_redirect;
 
   /// Configuration specified by the launch or attach commands.
   protocol::Configuration configuration;
@@ -148,11 +169,7 @@ struct DAP final : public DAPTransport::MessageHandler {
   lldb::SBFormat thread_format;
   llvm::unique_function<void()> on_configuration_done;
 
-  /// This is used to allow request_evaluate to handle empty expressions
-  /// (ie the user pressed 'return' and expects the previous expression to
-  /// repeat). If the previous expression was a command, it will be empty.
-  /// Else it will contain the last valid variable expression.
-  std::string last_valid_variable_expression;
+  void ActivateRepl();
 
   /// The set of features supported by the connected client.
   llvm::DenseSet<ClientFeature> clientFeatures;
@@ -422,6 +439,11 @@ struct DAP final : public DAPTransport::MessageHandler {
   void StartEventThread();
   void StartProgressEventThread();
 
+  void SetEvaluateContext(EvaluateContext *context) {
+    std::lock_guard<std::mutex> guard(m_eval_mutex);
+    m_evaluate_context = context;
+  }
+
   /// DAP debugger initialization functions.
   /// @{
 
@@ -492,6 +514,11 @@ struct DAP final : public DAPTransport::MessageHandler {
 
   const llvm::StringRef m_client_name;
 
+  std::mutex m_eval_mutex;
+  /// An optional context that is used while evaluating an expressions.
+  EvaluateContext *m_evaluate_context = nullptr;
+  lldb_private::MainLoop::ReadHandleUP m_out_handle;
+
   /// List of addresses mapped by sourceReference.
   std::vector<lldb::addr_t> m_source_references;
   std::mutex m_source_references_mutex;
diff --git a/lldb/tools/lldb-dap/EvaluateContext.cpp 
b/lldb/tools/lldb-dap/EvaluateContext.cpp
new file mode 100644
index 0000000000000..3570a08bc800a
--- /dev/null
+++ b/lldb/tools/lldb-dap/EvaluateContext.cpp
@@ -0,0 +1,127 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "EvaluateContext.h"
+#include "DAP.h"
+#include "DAPLog.h"
+#include "lldb/API/SBCommandInterpreter.h"
+#include "lldb/API/SBCommandReturnObject.h"
+#include "lldb/API/SBDebugger.h"
+#include "lldb/API/SBMutex.h"
+#include "lldb/API/SBProgress.h"
+#include "lldb/Host/File.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include <chrono>
+#include <memory>
+#include <string>
+
+using namespace llvm;
+using namespace lldb_dap;
+using lldb_private::Status;
+
+EvaluateContext::EvaluateContext(DAP &dap, StringRef expr)
+    : m_dap(dap), m_expr(expr),
+      m_wants_return_object(dap.debugger.GetCommandInterpreter().IsActive()) {}
+
+void EvaluateContext::Interrupt() {
+  DAP_LOG(m_dap.log, "EvaluateContext::Interrupt");
+  Done(/*immediate=*/true);
+}
+
+bool EvaluateContext::WantsRawInput() {
+  return !m_dap.debugger.GetCommandInterpreter().IsActive();
+}
+
+void EvaluateContext::Done(bool immediate) {
+  DAP_LOG(m_dap.log, "EvaluateContext::Done");
+  if (immediate)
+    m_loop.AddPendingCallback([](auto &loop) { loop.RequestTermination(); });
+  else
+    m_loop.AddCallback([](auto &loop) { loop.RequestTermination(); },
+                       std::chrono::milliseconds(50));
+}
+
+bool EvaluateContext::HandleOutput(StringRef o) {
+  DAP_LOG(m_dap.log, "EvaluateContext::HandleOutput(o={0})", o);
+  // Skip the echo of the input
+  if (o.trim() == m_expr.trim() && !m_echo_detected)
+    m_echo_detected = true;
+  else
+    m_output += o;
+
+  if (m_wants_return_object) {
+    if (m_return_object_reported)
+      Done(/*immediate=*/m_output.ends_with(m_dap.debugger.GetPrompt()));
+  } else {
+    Done(/*immediate=*/m_output.ends_with(m_dap.debugger.GetPrompt()));
+  }
+
+  return true;
+}
+
+bool EvaluateContext::HandleReturnObject(lldb::SBCommandReturnObject &result) {
+  DAP_LOG(m_dap.log, "EvaluateContext::HandleReturnObject");
+  m_success = result.Succeeded();
+
+  m_return_object_reported = true;
+
+  if (result.GetStatus() == lldb::eReturnStatusSuccessFinishResult)
+    m_variables = result.GetValues(lldb::eDynamicDontRunTarget);
+  if (result.GetOutputSize())
+    m_output += StringRef{result.GetOutput(), result.GetOutputSize()};
+  if (result.GetErrorSize())
+    m_output += StringRef{result.GetError(), result.GetErrorSize()};
+
+  if (WantsRawInput())
+    Done(/*immediate=*/false);
+  else
+    Done(/*immediate=*/!m_output.empty());
+
+  return true;
+}
+
+Expected<std::pair<std::string, lldb::SBValueList>>
+EvaluateContext::Run(DAP &dap, StringRef expr) {
+  DAP_LOG(dap.log, "EvaluateContext::Run");
+  EvaluateContext context(dap, expr);
+  dap.SetEvaluateContext(&context);
+
+  lldb::SBProgress progress(/*title=*/"Evaluating expression",
+                            expr.str().data(), dap.debugger);
+
+  lldb::SBMutex api_mutex = dap.GetAPIMutex();
+
+  // While in raw input mode, don't wait for output in case the IOHandler never
+  // writes.
+  if (context.WantsRawInput() && !dap.primary.write->GetIsInteractive())
+    context.Done(false);
+
+  // Unlock to allow the background thread to handle reading/processing.
+  api_mutex.unlock();
+
+  size_t size = expr.size();
+  if (Error err = dap.primary.write->Write(expr.data(), size).takeError())
+    return err;
+
+  Status status = context.m_loop.Run();
+
+  api_mutex.lock();
+  dap.SetEvaluateContext(nullptr);
+
+  if (auto error = status.takeError())
+    return error;
+
+  if (!context.m_success)
+    return createStringError(context.m_output.empty()
+                                 ? "Error evaluating expression"
+                                 : context.m_output.str());
+
+  return std::make_pair(context.m_output.str().str(), context.m_variables);
+}
diff --git a/lldb/tools/lldb-dap/EvaluateContext.h 
b/lldb/tools/lldb-dap/EvaluateContext.h
new file mode 100644
index 0000000000000..c4679c696ca4f
--- /dev/null
+++ b/lldb/tools/lldb-dap/EvaluateContext.h
@@ -0,0 +1,48 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_TOOLS_LLDB_DAP_EVALUATE_CONTEXT_H
+#define LLDB_TOOLS_LLDB_DAP_EVALUATE_CONTEXT_H
+
+#include "DAPForward.h"
+#include "lldb/API/SBValueList.h"
+#include "lldb/Host/MainLoop.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+
+namespace lldb_dap {
+class EvaluateContext {
+public:
+  bool HandleOutput(llvm::StringRef);
+  bool HandleReturnObject(lldb::SBCommandReturnObject &);
+  static llvm::Expected<std::pair<std::string, lldb::SBValueList>>
+  Run(DAP &dap, llvm::StringRef expr);
+
+  void Interrupt();
+
+private:
+  bool WantsRawInput();
+  void Done(bool immediate);
+  EvaluateContext(DAP &dap, llvm::StringRef expr);
+
+  DAP &m_dap;
+  llvm::StringRef m_expr;
+  llvm::SmallString<32> m_output;
+  lldb::SBValueList m_variables;
+  bool m_echo_detected = false;
+  bool m_success = true;
+  bool m_return_object_reported = false;
+  // If the CommandInterpreter is not active, then we're in raw input mode and
+  // will not receive a result from the print object helper.
+  bool m_wants_return_object = false;
+  lldb_private::MainLoop m_loop;
+};
+
+} // namespace lldb_dap
+
+#endif
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp 
b/lldb/tools/lldb-dap/EventHelper.cpp
index dbf4823408b11..6831c2202c50a 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -27,6 +27,7 @@
 #include "lldb/API/SBPlatform.h"
 #include "lldb/API/SBStream.h"
 #include "lldb/API/SBThread.h"
+#include "lldb/API/SBThreadCollection.h"
 #include "lldb/lldb-defines.h"
 #include "lldb/lldb-types.h"
 #include "llvm/Support/Error.h"
@@ -181,9 +182,10 @@ void SendProcessEvent(DAP &dap, LaunchMethod 
launch_method) {
 static void SendStoppedEvent(DAP &dap, lldb::SBThread &thread, bool on_entry,
                              bool all_threads_stopped, bool preserve_focus) {
   protocol::StoppedEventBody body;
+  body.reason = protocol::eStoppedReasonPause;
   if (on_entry) {
     body.reason = protocol::eStoppedReasonEntry;
-  } else {
+  } else if (thread.IsValid()) {
     switch (thread.GetStopReason()) {
     case lldb::eStopReasonTrace:
     case lldb::eStopReasonPlanComplete:
@@ -235,13 +237,14 @@ static void SendStoppedEvent(DAP &dap, lldb::SBThread 
&thread, bool on_entry,
     case lldb::eStopReasonThreadExiting:
     case lldb::eStopReasonInvalid:
     case lldb::eStopReasonNone:
-      return;
+      break;
     }
+
+    lldb::SBStream description;
+    thread.GetStopDescription(description);
+    body.description = {description.GetData(), description.GetSize()};
   }
   lldb::tid_t tid = thread.GetThreadID();
-  lldb::SBStream description;
-  thread.GetStopDescription(description);
-  body.description = {description.GetData(), description.GetSize()};
   body.threadId = tid;
   body.allThreadsStopped = all_threads_stopped;
   body.preserveFocusHint = preserve_focus;
@@ -266,7 +269,8 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool on_entry) 
{
   llvm::DenseSet<lldb::tid_t> old_thread_ids;
   old_thread_ids.swap(dap.thread_ids);
 
-  lldb::tid_t focused_tid = LLDB_INVALID_THREAD_ID;
+  lldb::SBThread focused_thread;
+  std::vector<lldb::SBThread> stopped_threads;
   for (auto thread : process) {
     // Collect all known thread ids for sending thread events.
     dap.thread_ids.insert(thread.GetThreadID());
@@ -274,22 +278,28 @@ llvm::Error SendThreadStoppedEvent(DAP &dap, bool 
on_entry) {
     if (!ThreadHasStopReason(thread))
       continue;
 
-    // When we stop, report allThreadsStopped for the *first* stopped thread to
-    // ensure the list of stopped threads is up to date.
-    bool first_stop = focused_tid == LLDB_INVALID_THREAD_ID;
-    SendStoppedEvent(dap, thread, on_entry, /*all_threads_stopped=*/first_stop,
-                     /*preserve_focus=*/!first_stop);
-
-    // Default focus to the first stopped thread.
-    if (focused_tid == LLDB_INVALID_THREAD_ID)
-      focused_tid = thread.GetThreadID();
+    // Focus on the first stopped thread if the selected thread is not stopped.
+    if (!focused_thread.IsValid())
+      focused_thread = thread;
+    else
+      stopped_threads.push_back(thread);
   }
 
-  if (focused_tid == LLDB_INVALID_THREAD_ID)
+  if (!focused_thread)
+    focused_thread = process.GetSelectedThread();
+
+  if (!focused_thread)
     return make_error<DAPError>("no stopped threads");
 
+  // Notify the focused thread first and tell the client all threads have
+  // stopped.
+  SendStoppedEvent(dap, focused_thread, on_entry, true, false);
+  // Send stopped events for remaining stopped threads.
+  for (auto thread : stopped_threads)
+    SendStoppedEvent(dap, thread, on_entry, false, true);
+
   // Update focused thread.
-  dap.focus_tid = focused_tid;
+  dap.focus_tid = focused_thread.GetThreadID();
 
   for (const auto &tid : old_thread_ids)
     if (!dap.thread_ids.contains(tid))
@@ -314,15 +324,11 @@ void SendStdOutStdErr(DAP &dap, lldb::SBProcess &process) 
{
 // Send a "continued" event to indicate the process is in the running state.
 void SendContinuedEvent(DAP &dap) {
   lldb::SBProcess process = dap.target.GetProcess();
-  if (!process.IsValid()) {
-    return;
-  }
-
   // If the focus thread is not set then we haven't reported any thread status
   // to the client, so nothing to report.
-  if (!dap.configuration_done || dap.focus_tid == LLDB_INVALID_THREAD_ID) {
+  if (!process.IsValid() || !dap.configuration_done ||
+      dap.focus_tid == LLDB_INVALID_THREAD_ID)
     return;
-  }
 
   llvm::json::Object event(CreateEventObject("continued"));
   llvm::json::Object body;
diff --git a/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp 
b/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp
index 423043e6c26dd..4988ccb616b48 100644
--- a/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/CompletionsRequestHandler.cpp
@@ -11,6 +11,7 @@
 #include "Protocol/ProtocolRequests.h"
 #include "Protocol/ProtocolTypes.h"
 #include "RequestHandler.h"
+#include "lldb/API/SBCommandInterpreter.h"
 #include "lldb/API/SBStringList.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/ConvertUTF.h"
diff --git a/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp 
b/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
index 4b84a047cd148..8e589ed68c65c 100644
--- a/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/ConfigurationDoneRequestHandler.cpp
@@ -7,6 +7,7 @@
 
//===----------------------------------------------------------------------===//
 
 #include "DAP.h"
+#include "DAPLog.h"
 #include "EventHelper.h"
 #include "LLDBUtils.h"
 #include "Protocol/ProtocolRequests.h"
@@ -74,6 +75,8 @@ void ConfigurationDoneRequestHandler::PostRun() const {
   dap.on_configuration_done();
   // Clear the callback to ensure any captured resources are released.
   dap.on_configuration_done = nullptr;
+
+  dap.ActivateRepl();
 }
 
 } // namespace lldb_dap
diff --git a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp 
b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
index 0eb5c57732350..5f2ac55f6af0a 100644
--- a/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/EvaluateRequestHandler.cpp
@@ -14,9 +14,11 @@
 #include "Protocol/ProtocolRequests.h"
 #include "Protocol/ProtocolTypes.h"
 #include "RequestHandler.h"
+#include "lldb/API/SBValue.h"
 #include "lldb/lldb-enumerations.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Error.h"
+#include <cstddef>
 
 using namespace llvm;
 using namespace lldb_dap;
@@ -24,25 +26,6 @@ using namespace lldb_dap::protocol;
 
 namespace lldb_dap {
 
-static bool RunExpressionAsLLDBCommand(DAP &dap, lldb::SBFrame &frame,
-                                       std::string &expression,
-                                       EvaluateContext context) {
-  if (context != eEvaluateContextRepl && context != eEvaluateContextUnknown)
-    return false;
-
-  // Since we don't know this context do not try to repeat the last command;
-  if (context == eEvaluateContextUnknown && expression.empty())
-    return false;
-
-  const bool repeat_last_command =
-      expression.empty() && dap.last_valid_variable_expression.empty();
-  if (repeat_last_command)
-    return true;
-
-  const ReplMode repl_mode = dap.DetectReplMode(frame, expression, false);
-  return repl_mode == ReplMode::Command;
-}
-
 static lldb::SBValue EvaluateVariableExpression(lldb::SBTarget &target,
                                                 lldb::SBFrame &frame,
                                                 const std::string &expression,
@@ -73,65 +56,60 @@ static lldb::SBValue 
EvaluateVariableExpression(lldb::SBTarget &target,
 /// The expression has access to any variables and arguments that are in scope.
 Expected<EvaluateResponseBody>
 EvaluateRequestHandler::Run(const EvaluateArguments &arguments) const {
-
   EvaluateResponseBody body;
   lldb::SBFrame frame = dap.GetLLDBFrame(arguments.frameId);
-  std::string expression = llvm::StringRef(arguments.expression).trim().str();
-  const EvaluateContext evaluate_context = arguments.context;
-  const bool is_repl_context = evaluate_context == eEvaluateContextRepl;
-
-  if (RunExpressionAsLLDBCommand(dap, frame, expression, evaluate_context)) {
-    // Since the current expression is not for a variable, clear the
-    // last_valid_variable_expression field.
-    dap.last_valid_variable_expression.clear();
+  std::string expression = arguments.expression;
+  const bool is_repl_context = arguments.context == eEvaluateContextRepl;
+  const bool run_as_expression = arguments.context != eEvaluateContextHover;
+  lldb::SBValue value;
+
+  if (arguments.context == protocol::eEvaluateContextRepl &&
+      dap.DetectReplMode(frame, expression, false) == ReplMode::Command) {
+    const lldb::StateType process_state = dap.target.GetProcess().GetState();
+    if (!lldb::SBDebugger::StateIsStoppedState(process_state))
+      return llvm::make_error<DAPError>(
+          "Cannot evaluate expressions while the process is running. Pause "
+          "the process and try again.",
+          /**error_code=*/llvm::inconvertibleErrorCode(),
+          /**show_user=*/false);
+
     // If we're evaluating a command relative to the current frame, set the
     // focus_tid to the current frame for any thread related events.
-    if (frame.IsValid()) {
+    if (frame.IsValid())
       dap.focus_tid = frame.GetThread().GetThreadID();
-    }
 
-    bool required_command_failed = false;
-    body.result = RunLLDBCommands(
-        dap.debugger, llvm::StringRef(), {expression}, required_command_failed,
-        /*parse_command_directives=*/false, /*echo_commands=*/false);
-    return body;
-  }
+    Expected<std::pair<std::string, lldb::SBValueList>> result =
+        EvaluateContext::Run(dap, expression + "\n");
+    if (!result)
+      return result.takeError();
 
-  const lldb::StateType process_state = dap.target.GetProcess().GetState();
-  if (!lldb::SBDebugger::StateIsStoppedState(process_state))
-    return llvm::make_error<DAPError>(
-        "Cannot evaluate expressions while the process is running. Pause "
-        "the process and try again.",
-        /**error_code=*/llvm::inconvertibleErrorCode(),
-        /**show_user=*/false);
+    lldb::SBValueList values;
+    std::tie(body.result, values) = *result;
 
-  // If the user expression is empty, evaluate the last valid variable
-  // expression.
-  if (expression.empty() && is_repl_context)
-    expression = dap.last_valid_variable_expression;
+    if (values.GetSize() == 1) {
+      value = values.GetValueAtIndex(0);
+      body.type = value.GetDisplayTypeName();
+    } else if (values.GetSize()) {
+      body.variablesReference = dap.variables.Insert(result->second);
+    }
+  } else {
+    value = EvaluateVariableExpression(dap.target, frame, expression,
+                                       run_as_expression);
 
-  const bool run_as_expression = evaluate_context != eEvaluateContextHover;
-  lldb::SBValue value = EvaluateVariableExpression(
-      dap.target, frame, expression, run_as_expression);
+    if (value.GetError().Fail())
+      return ToError(value.GetError(), /*show_user=*/false);
 
-  if (value.GetError().Fail())
-    return ToError(value.GetError(), /*show_user=*/false);
+    if (is_repl_context)
+      value = value.Persist();
 
-  if (is_repl_context) {
-    // save the new variable expression
-    dap.last_valid_variable_expression = std::move(expression);
+    const bool hex = arguments.format ? arguments.format->hex : false;
+    VariableDescription desc(
+        value, dap.configuration.enableAutoVariableSummaries, hex);
 
-    // Freeze dry the value in case users expand it later in the debug console
-    value = value.Persist();
+    body.result = desc.GetResult(arguments.context);
+    body.type = desc.display_type_name;
   }
 
-  const bool hex = arguments.format ? arguments.format->hex : false;
-  VariableDescription desc(value, 
dap.configuration.enableAutoVariableSummaries,
-                           hex);
-
-  body.result = desc.GetResult(evaluate_context);
-  body.type = desc.display_type_name;
-
   if (value.MightHaveChildren() || ValuePointsToCode(value))
     body.variablesReference =
         dap.variables.InsertVariable(value, /*is_permanent=*/is_repl_context);
diff --git a/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp 
b/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp
index ee7cb525d9c29..665bd62f4bd97 100644
--- a/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/VariablesRequestHandler.cpp
@@ -80,7 +80,7 @@ VariablesRequestHandler::Run(const VariablesArguments 
&arguments) const {
         var.name = "<error>";
         var.type = "const char *";
         var.value = var_err;
-        variables.emplace_back(var);
+        variables.emplace_back(std::move(var));
       }
     }
     const int64_t end_idx = start_idx + ((count == 0) ? num_children : count);
@@ -136,35 +136,39 @@ VariablesRequestHandler::Run(const VariablesArguments 
&arguments) const {
     // We are expanding a variable that has children, so we will return its
     // children.
     lldb::SBValue variable = dap.variables.GetVariable(var_ref);
-    if (variable.IsValid()) {
-      const bool is_permanent =
-          dap.variables.IsPermanentVariableReference(var_ref);
-      auto addChild = [&](lldb::SBValue child,
-                          std::optional<llvm::StringRef> custom_name = {}) {
-        if (!child.IsValid())
-          return;
-        const int64_t child_var_ref =
-            dap.variables.InsertVariable(child, is_permanent);
-        variables.emplace_back(
-            CreateVariable(child, child_var_ref, hex,
-                           dap.configuration.enableAutoVariableSummaries,
-                           dap.configuration.enableSyntheticChildDebugging,
-                           /*is_name_duplicated=*/false, custom_name));
-      };
-      const int64_t num_children = variable.GetNumChildren();
-      const int64_t end_idx = start + ((count == 0) ? num_children : count);
-      int64_t i = start;
-      for (; i < end_idx && i < num_children; ++i)
-        addChild(variable.GetChildAtIndex(i));
-
-      // If we haven't filled the count quota from the request, we insert a new
-      // "[raw]" child that can be used to inspect the raw version of a
-      // synthetic member. That eliminates the need for the user to go to the
-      // debug console and type `frame var <variable> to get these values.
-      if (dap.configuration.enableSyntheticChildDebugging &&
-          variable.IsSynthetic() && i == num_children)
-        addChild(variable.GetNonSyntheticValue(), "[raw]");
+    if (!variable.IsValid()) {
+      if (!variable.IsInScope())
+        return make_error<DAPError>("value is no longer in scope.");
+      return make_error<DAPError>("invalid variable reference");
     }
+
+    const bool is_permanent =
+        dap.variables.IsPermanentVariableReference(var_ref);
+    auto addChild = [&](lldb::SBValue child,
+                        std::optional<llvm::StringRef> custom_name = {}) {
+      if (!child.IsValid())
+        return;
+      const int64_t child_var_ref =
+          dap.variables.InsertVariable(child, is_permanent);
+      variables.emplace_back(
+          CreateVariable(child, child_var_ref, hex,
+                         dap.configuration.enableAutoVariableSummaries,
+                         dap.configuration.enableSyntheticChildDebugging,
+                         /*is_name_duplicated=*/false, custom_name));
+    };
+    const int64_t num_children = variable.GetNumChildren();
+    const int64_t end_idx = start + ((count == 0) ? num_children : count);
+    int64_t i = start;
+    for (; i < end_idx && i < num_children; ++i)
+      addChild(variable.GetChildAtIndex(i));
+
+    // If we haven't filled the count quota from the request, we insert a new
+    // "[raw]" child that can be used to inspect the raw version of a
+    // synthetic member. That eliminates the need for the user to go to the
+    // debug console and type `frame var <variable> to get these values.
+    if (dap.configuration.enableSyntheticChildDebugging &&
+        variable.IsSynthetic() && i == num_children)
+      addChild(variable.GetNonSyntheticValue(), "[raw]");
   }
 
   return VariablesResponseBody{std::move(variables)};
diff --git a/lldb/tools/lldb-dap/ProgressEvent.cpp 
b/lldb/tools/lldb-dap/ProgressEvent.cpp
index 5ef03ccb24bb9..33964922e1ad7 100644
--- a/lldb/tools/lldb-dap/ProgressEvent.cpp
+++ b/lldb/tools/lldb-dap/ProgressEvent.cpp
@@ -114,7 +114,7 @@ json::Value ProgressEvent::ToJSON() const {
 
   if (m_event_type == progressStart) {
     EmplaceSafeString(body, "title", m_message);
-    body.try_emplace("cancellable", false);
+    body.try_emplace("cancellable", true);
   }
 
   if (m_event_type == progressUpdate)
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp 
b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
index a72802a33fc9c..7683b8c26cbe9 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.cpp
@@ -146,8 +146,8 @@ namespace lldb_dap::protocol {
 
 bool fromJSON(const json::Value &Params, CancelArguments &CA, json::Path P) {
   json::ObjectMapper O(Params, P);
-  return O && O.map("requestId", CA.requestId) &&
-         O.map("progressId", CA.progressId);
+  return O && O.mapOptional("requestId", CA.requestId) &&
+         O.mapOptional("progressId", CA.progressId);
 }
 
 bool fromJSON(const json::Value &Params, DisconnectArguments &DA,
diff --git a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h 
b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
index 28c9f48200e0c..5e50322873360 100644
--- a/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
+++ b/lldb/tools/lldb-dap/Protocol/ProtocolRequests.h
@@ -42,13 +42,13 @@ struct CancelArguments {
   /// is cancelled.
   ///
   /// Both a `requestId` and a `progressId` can be specified in one request.
-  std::optional<int64_t> requestId;
+  uint64_t requestId = 0;
 
   /// The ID (attribute `progressId`) of the progress to cancel. If missing no
   /// progress is cancelled.
   ///
   /// Both a `requestId` and a `progressId` can be specified in one request.
-  std::optional<int64_t> progressId;
+  std::string progressId;
 };
 bool fromJSON(const llvm::json::Value &, CancelArguments &, llvm::json::Path);
 
diff --git a/lldb/tools/lldb-dap/Variables.cpp 
b/lldb/tools/lldb-dap/Variables.cpp
index 6d49113243799..e030100b5c3ee 100644
--- a/lldb/tools/lldb-dap/Variables.cpp
+++ b/lldb/tools/lldb-dap/Variables.cpp
@@ -113,6 +113,27 @@ int64_t Variables::InsertVariable(lldb::SBValue variable, 
bool is_permanent) {
   return var_ref;
 }
 
+int64_t Variables::Insert(lldb::SBValueList values) {
+  if (!values.IsValid() || values.GetSize() == 0)
+    return LLDB_DAP_INVALID_VAR_REF;
+
+  if (values.GetSize() == 1) {
+    lldb::SBValue value = values.GetValueAtIndex(0);
+    if (value.MightHaveChildren())
+      return InsertVariable(value, /*is_permanent=*/true);
+    return 0;
+  }
+
+  int64_t var_ref = GetNewVariableReference(true);
+  // Copy the values as persisted results.
+  lldb::SBValueList persisted_values;
+  for (uint32_t i = 0; i < values.GetSize(); i++) {
+    persisted_values.Append(values.GetValueAtIndex(i).Persist());
+  }
+  m_referenced_value_lists.insert({var_ref, persisted_values});
+  return var_ref;
+}
+
 lldb::SBValue Variables::FindVariable(uint64_t variablesReference,
                                       llvm::StringRef name) {
   lldb::SBValue variable;
diff --git a/lldb/tools/lldb-dap/Variables.h b/lldb/tools/lldb-dap/Variables.h
index 9814cabc7036c..2ceb86f48c3cf 100644
--- a/lldb/tools/lldb-dap/Variables.h
+++ b/lldb/tools/lldb-dap/Variables.h
@@ -90,6 +90,7 @@ struct Variables {
   /// Insert a new \p variable.
   /// \return variableReference assigned to this expandable variable.
   int64_t InsertVariable(lldb::SBValue variable, bool is_permanent);
+  int64_t Insert(lldb::SBValueList values);
 
   std::optional<ScopeData> GetTopLevelScope(int64_t variablesReference);
 
@@ -123,6 +124,9 @@ struct Variables {
   /// These are the variables evaluated from debug console REPL.
   llvm::DenseMap<int64_t, lldb::SBValue> m_referencedpermanent_variables;
 
+  /// ValueLists for evaluated commands.
+  llvm::DenseMap<int64_t, lldb::SBValueList> m_referenced_value_lists;
+
   /// Key = dap_frame_id (encodes both thread index ID and frame ID)
   /// Value = scopes for the frame (locals, globals, registers)
   std::map<uint64_t, FrameScopes> m_frames;
diff --git a/lldb/unittests/DAP/Handler/DisconnectTest.cpp 
b/lldb/unittests/DAP/Handler/DisconnectTest.cpp
index 212c5698feea8..05e71534ae455 100644
--- a/lldb/unittests/DAP/Handler/DisconnectTest.cpp
+++ b/lldb/unittests/DAP/Handler/DisconnectTest.cpp
@@ -44,17 +44,18 @@ TEST_F(DisconnectRequestHandlerTest, 
DisconnectTriggersTerminateCommands) {
 
   LoadCore();
 
-  DisconnectRequestHandler handler(*dap);
-
   dap->configuration.terminateCommands = {"?script print(1)",
                                           "script print(2)"};
   EXPECT_EQ(dap->target.GetProcess().GetState(), lldb::eStateStopped);
+
+  DisconnectRequestHandler handler(*dap);
   ASSERT_THAT_ERROR(handler.Run(std::nullopt), Succeeded());
-  EXPECT_CALL(client, Received(Output("1\n")));
-  EXPECT_CALL(client, Received(Output("2\n"))).Times(2);
-  EXPECT_CALL(client, Received(Output("(lldb) script print(2)\n")));
+
   EXPECT_CALL(client, Received(Output("Running terminateCommands:\n")));
+  EXPECT_CALL(client, Received(Output("(lldb) script print(2)\n")));
+  EXPECT_CALL(client, Received(Output("2\n")));
   EXPECT_CALL(client, Received(IsEvent("terminated", _)));
+
   Run();
 }
 #endif
diff --git a/lldb/unittests/DAP/TestBase.cpp b/lldb/unittests/DAP/TestBase.cpp
index a9231085637c9..af477d3c37538 100644
--- a/lldb/unittests/DAP/TestBase.cpp
+++ b/lldb/unittests/DAP/TestBase.cpp
@@ -20,6 +20,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Testing/Support/Error.h"
 #include "gtest/gtest.h"
+#include <chrono>
 #include <cstdio>
 #include <memory>
 
@@ -37,7 +38,7 @@ using lldb_private::Pipe;
 void TransportBase::SetUp() {
   std::tie(to_client, to_server) = TestDAPTransport::createPair();
 
-  log = std::make_unique<Log>(llvm::outs(), log_mutex);
+  log = std::make_unique<Log>(llvm::errs(), log_mutex);
   dap = std::make_unique<DAP>(
       /*log=*/*log,
       /*default_repl_mode=*/ReplMode::Auto,
@@ -56,13 +57,17 @@ void TransportBase::SetUp() {
 }
 
 void TransportBase::Run() {
-  bool addition_succeeded = loop.AddPendingCallback(
-      [](lldb_private::MainLoopBase &loop) { loop.RequestTermination(); });
+  bool addition_succeeded = loop.AddCallback(
+      [](lldb_private::MainLoopBase &loop) {
+        llvm::errs() << "Terminating\n";
+        loop.RequestTermination();
+      },
+      std::chrono::seconds(1));
   EXPECT_TRUE(addition_succeeded);
   EXPECT_THAT_ERROR(loop.Run().takeError(), llvm::Succeeded());
 }
 
-void DAPTestBase::SetUp() { TransportBase::SetUp(); }
+// void DAPTestBase::SetUp() { TransportBase::SetUp(); }
 
 void DAPTestBase::TearDown() {
   if (core)
@@ -107,13 +112,9 @@ void DAPTestBase::CreateDebugger() {
   ASSERT_THAT_ERROR(dap->ConfigureIO(dev_null_stream, dev_null_stream),
                     Succeeded());
 
-  dap->debugger.SetInputFile(dap->in);
-  auto out_fd = dap->out.GetWriteFileDescriptor();
-  ASSERT_THAT_EXPECTED(out_fd, Succeeded());
-  dap->debugger.SetOutputFile(lldb::SBFile(*out_fd, "w", false));
-  auto err_fd = dap->out.GetWriteFileDescriptor();
-  ASSERT_THAT_EXPECTED(err_fd, Succeeded());
-  dap->debugger.SetErrorFile(lldb::SBFile(*err_fd, "w", false));
+  dap->no_lldbinit = true;
+  ASSERT_THAT_ERROR(dap->InitializeDebugger(), Succeeded());
+  ASSERT_THAT_ERROR(dap->ActivateRepl(), Succeeded());
 }
 
 void DAPTestBase::LoadCore() {
diff --git a/lldb/unittests/DAP/TestBase.h b/lldb/unittests/DAP/TestBase.h
index f1c7e6b989729..d8166d04feed2 100644
--- a/lldb/unittests/DAP/TestBase.h
+++ b/lldb/unittests/DAP/TestBase.h
@@ -110,7 +110,6 @@ class DAPTestBase : public TransportBase {
 
   static void SetUpTestSuite();
   static void TeatUpTestSuite();
-  void SetUp() override;
   void TearDown() override;
 
   bool GetDebuggerSupportsTarget(llvm::StringRef platform);

>From 183789c85f77e1adde53e56918c372048ccceddc Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Tue, 10 Feb 2026 15:36:42 -0800
Subject: [PATCH 2/5] Remove testing code.

---
 .../Python/lldbsuite/test/tools/lldb-dap/dap_server.py     | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

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 f3efc7b23214b..acd9b09558eef 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
@@ -486,10 +486,9 @@ def _process_recv_packets(self) -> None:
         with self._recv_condition:
             for packet in self._recv_packets:
                 if packet and ("seq" not in packet or packet["seq"] == 0):
-                    pass
-                    # raise ValueError(
-                    #     f"received a malformed packet, expected 'seq != 0' 
for {packet!r}"
-                    # )
+                    raise ValueError(
+                        f"received a malformed packet, expected 'seq != 0' for 
{packet!r}"
+                    )
                 if packet:
                     self._log.messages.append((Log.Dir.RECV, packet))
                 # Handle events that may modify any stateful properties of

>From 8fb124da84c1aeaa9425023150a88dc79d2ae5d1 Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Wed, 11 Feb 2026 15:16:24 -0800
Subject: [PATCH 3/5] Correcting the stopped-events tests.

---
 .../packages/Python/lldbsuite/test/tools/lldb-dap/dap_server.py | 2 --
 1 file changed, 2 deletions(-)

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 3eafd6d3c0f3b..aeaf5dad3975b 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
@@ -540,8 +540,6 @@ def _handle_event(self, packet: Event) -> None:
             # reasons since the 'threads' command doesn't return
             # that information.
             self._process_stopped()
-            if "allThreadsStopped" in body and body["allThreadsStopped"]:
-                self.thread_stop_reasons = {}
             if "threadId" in body:
                 tid = body["threadId"]
                 self.thread_stop_reasons[tid] = body

>From 0c06bef8480dd464e397f4cf2aa8cc7677439d57 Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Wed, 11 Feb 2026 16:22:49 -0800
Subject: [PATCH 4/5] Fixing DAPTests.

---
 lldb/tools/lldb-dap/DAP.cpp     | 24 +++++++++++++-----------
 lldb/unittests/DAP/TestBase.cpp | 14 ++------------
 2 files changed, 15 insertions(+), 23 deletions(-)

diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 4de0200dabe93..85bb295ff137c 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -297,17 +297,19 @@ llvm::Error DAP::ConfigureIO(std::FILE *overrideOut, 
std::FILE *overrideErr) {
 
   std::tie(primary, replica) = *io;
 
-  if (auto Error = stdout_redirect.RedirectTo(
-          overrideOut, [this](llvm::StringRef output) {
-            SendOutput(OutputType::Console, output);
-          }))
-    return Error;
-
-  if (auto Error = stderr_redirect.RedirectTo(
-          overrideErr, [this](llvm::StringRef output) {
-            SendOutput(OutputType::Console, output);
-          }))
-    return Error;
+  if (overrideOut)
+    if (auto Error = stdout_redirect.RedirectTo(
+            overrideOut, [this](llvm::StringRef output) {
+              SendOutput(OutputType::Console, output);
+            }))
+      return Error;
+
+  if (overrideErr)
+    if (auto Error = stderr_redirect.RedirectTo(
+            overrideErr, [this](llvm::StringRef output) {
+              SendOutput(OutputType::Console, output);
+            }))
+      return Error;
 
   return llvm::Error::success();
 }
diff --git a/lldb/unittests/DAP/TestBase.cpp b/lldb/unittests/DAP/TestBase.cpp
index c0d6ae7e70aac..61986b74a20b6 100644
--- a/lldb/unittests/DAP/TestBase.cpp
+++ b/lldb/unittests/DAP/TestBase.cpp
@@ -29,9 +29,6 @@ using namespace lldb;
 using namespace lldb_dap;
 using namespace lldb_dap::protocol;
 using namespace lldb_dap_tests;
-using lldb_private::File;
-using lldb_private::FileSpec;
-using lldb_private::FileSystem;
 using lldb_private::MainLoop;
 using lldb_private::Pipe;
 
@@ -98,18 +95,11 @@ void DAPTestBase::CreateDebugger() {
   ASSERT_TRUE(dap->debugger);
   dap->target = dap->debugger.GetDummyTarget();
 
-  Expected<lldb::FileUP> dev_null = FileSystem::Instance().Open(
-      FileSpec(FileSystem::DEV_NULL), File::eOpenOptionReadWrite);
-  ASSERT_THAT_EXPECTED(dev_null, Succeeded());
-  lldb::FileSP dev_null_sp = std::move(*dev_null);
-
-  std::FILE *dev_null_stream = dev_null_sp->GetStream();
-  ASSERT_THAT_ERROR(dap->ConfigureIO(dev_null_stream, dev_null_stream),
-                    Succeeded());
+  ASSERT_THAT_ERROR(dap->ConfigureIO(), Succeeded());
 
   dap->no_lldbinit = true;
   ASSERT_THAT_ERROR(dap->InitializeDebugger(), Succeeded());
-  ASSERT_THAT_ERROR(dap->ActivateRepl(), Succeeded());
+  dap->ActivateRepl();
 }
 
 void DAPTestBase::LoadCore() {

>From 8410caf854366bb638bc6810080bd626da7beaae Mon Sep 17 00:00:00 2001
From: John Harrison <[email protected]>
Date: Thu, 12 Feb 2026 09:58:41 -0800
Subject: [PATCH 5/5] Apply suggestions from code review

Co-authored-by: Ebuka Ezike <[email protected]>
---
 lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py 
b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
index 8429941ad574f..4b3e83e3bb57a 100644
--- a/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
+++ b/lldb/test/API/tools/lldb-dap/cancel/TestDAP_cancel.py
@@ -36,7 +36,7 @@ def test_pending_request(self):
         Tests cancelling a pending request.
         """
         program = self.getBuildArtifact("a.out")
-        busy_loop = os.path.join(os.path.dirname(__file__), "busy_loop.py")
+        busy_loop =  self.getSourcePath("busy_loop.py")
         self.build_and_launch(
             program,
             initCommands=[f"command script import {busy_loop}"],

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

Reply via email to