[Lldb-commits] [lldb] [lldb-dap] Add single thread execution support for step out (PR #200314)
via lldb-commits
lldb-commits at lists.llvm.org
Mon Jun 1 17:59:01 PDT 2026
https://github.com/kusmour updated https://github.com/llvm/llvm-project/pull/200314
>From c24c1ede54354f47e28a2e43386dc3ad3a015131 Mon Sep 17 00:00:00 2001
From: Wanyi Ye <wanyi at meta.com>
Date: Mon, 1 Jun 2026 10:16:03 -0700
Subject: [PATCH 1/3] [lldb] Support single thread for step out
Summary:
Similar to step in request, we add the support step over with the option stop_other_threads and update the thread plan using the new parameter.
Test Plan:
Added new dap tests
---
lldb/include/lldb/API/SBThread.h | 4 ++
.../test/tools/lldb-dap/dap_server.py | 4 +-
.../test/tools/lldb-dap/lldbdap_testcase.py | 7 ++-
lldb/source/API/SBThread.cpp | 38 ++++++++++++++++
.../API/tools/lldb-dap/step/TestDAP_step.py | 44 ++++++++++++++++++-
.../Handler/StepOutRequestHandler.cpp | 4 +-
6 files changed, 95 insertions(+), 6 deletions(-)
diff --git a/lldb/include/lldb/API/SBThread.h b/lldb/include/lldb/API/SBThread.h
index 97d3b838492fb..5f43f2318d580 100644
--- a/lldb/include/lldb/API/SBThread.h
+++ b/lldb/include/lldb/API/SBThread.h
@@ -121,6 +121,10 @@ class LLDB_API SBThread {
void StepOut(SBError &error);
+ void StepOut(lldb::RunMode stop_other_threads);
+
+ void StepOut(lldb::RunMode stop_other_threads, SBError &error);
+
void StepOutOfFrame(SBFrame &frame);
void StepOutOfFrame(SBFrame &frame, SBError &error);
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 c8acf67b23b99..b8d44d3822703 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
@@ -1294,10 +1294,12 @@ def request_stepInTargets(self, frameId):
}
return self._send_recv(command_dict)
- def request_stepOut(self, threadId):
+ def request_stepOut(self, threadId, singleThread=None):
if self.exit_status is not None:
raise ValueError("request_stepOut called after process exited")
args_dict = {"threadId": threadId}
+ if singleThread is not None:
+ args_dict["singleThread"] = singleThread
command_dict = {"command": "stepOut", "type": "request", "arguments": args_dict}
return self._send_recv(command_dict)
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 714a8c228a76b..22562308614a2 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
@@ -443,8 +443,11 @@ def stepOver(
return self.dap_server.wait_for_stopped()
return None
- def stepOut(self, threadId=None, waitForStop=True):
- self.dap_server.request_stepOut(threadId=threadId)
+ def stepOut(self, threadId=None, waitForStop=True, singleThread=None):
+ response = self.dap_server.request_stepOut(
+ threadId=threadId, singleThread=singleThread
+ )
+ self.assertTrue(response["success"])
if waitForStop:
return self.dap_server.wait_for_stopped()
return None
diff --git a/lldb/source/API/SBThread.cpp b/lldb/source/API/SBThread.cpp
index fafb533771e56..ac04286815452 100644
--- a/lldb/source/API/SBThread.cpp
+++ b/lldb/source/API/SBThread.cpp
@@ -621,6 +621,44 @@ void SBThread::StepOut(SBError &error) {
error = Status::FromErrorString(new_plan_status.AsCString());
}
+void SBThread::StepOut(lldb::RunMode stop_other_threads) {
+ LLDB_INSTRUMENT_VA(this, stop_other_threads);
+
+ SBError error; // Ignored
+ StepOut(stop_other_threads, error);
+}
+
+void SBThread::StepOut(lldb::RunMode stop_other_threads, SBError &error) {
+ LLDB_INSTRUMENT_VA(this, stop_other_threads, error);
+
+ llvm::Expected<StoppedExecutionContext> exe_ctx =
+ GetStoppedExecutionContext(m_opaque_sp);
+ if (!exe_ctx) {
+ error = Status::FromError(exe_ctx.takeError());
+ return;
+ }
+
+ if (!exe_ctx->HasThreadScope()) {
+ error = Status::FromErrorString("this SBThread object is invalid");
+ return;
+ }
+
+ bool abort_other_plans = false;
+
+ Thread *thread = exe_ctx->GetThreadPtr();
+
+ const LazyBool avoid_no_debug = eLazyBoolCalculate;
+ Status new_plan_status;
+ ThreadPlanSP new_plan_sp(thread->QueueThreadPlanForStepOut(
+ abort_other_plans, nullptr, false, stop_other_threads == eOnlyThisThread,
+ eVoteYes, eVoteNoOpinion, 0, new_plan_status, avoid_no_debug));
+
+ if (new_plan_status.Success())
+ error = ResumeNewPlan(std::move(*exe_ctx), new_plan_sp.get());
+ else
+ error = Status::FromErrorString(new_plan_status.AsCString());
+}
+
void SBThread::StepOutOfFrame(SBFrame &sb_frame) {
LLDB_INSTRUMENT_VA(this, sb_frame);
diff --git a/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py b/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py
index 5c055f679aedc..a28caa2f4c5e3 100644
--- a/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py
+++ b/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py
@@ -2,12 +2,11 @@
Test lldb-dap setBreakpoints request
"""
-
import dap_server
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
-from lldbsuite.test import lldbutil
import lldbdap_testcase
+from lldbsuite.test import lldbutil
class TestDAP_step(lldbdap_testcase.DAPTestCaseBase):
@@ -84,6 +83,47 @@ def test_step(self):
# only step one thread that is at the breakpoint and stop
break
+ @skipIfWindows
+ def test_step_out_single_thread(self):
+ """
+ Tests step out with singleThread=true correctly completes the step.
+ """
+ program = self.getBuildArtifact("a.out")
+ self.build_and_launch(program)
+ source = "main.cpp"
+ breakpoint1_line = line_number(source, "// breakpoint 1")
+ lines = [breakpoint1_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)
+ threads = self.dap_server.get_threads()
+ for thread in threads:
+ if "reason" in thread:
+ reason = thread["reason"]
+ if reason == "breakpoint":
+ tid = thread["id"]
+ x1 = self.get_local_as_int("x", threadId=tid)
+ (src1, line1) = self.get_source_and_line(threadId=tid)
+
+ # Step into the recursive call first so we have
+ # a frame to step out of.
+ self.stepIn(threadId=tid, waitForStop=True)
+ x2 = self.get_local_as_int("x", threadId=tid)
+ (src2, line2) = self.get_source_and_line(threadId=tid)
+ self.assertEqual(x1, x2 + 1, "verify step in variable")
+
+ # Now step out with singleThread and verify we
+ # return to the caller frame.
+ self.stepOut(threadId=tid, waitForStop=True, singleThread=True)
+ x3 = self.get_local_as_int("x", threadId=tid)
+ (src3, line3) = self.get_source_and_line(threadId=tid)
+ self.assertEqual(x1, x3, "verify step out variable")
+ self.assertGreaterEqual(line3, line1, "verify step out line")
+ self.assertEqual(src1, src3, "verify step out source")
+ break
+
def test_step_over_inlined_function(self):
"""
Test stepping over when the program counter is in another file.
diff --git a/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp
index 61bde76e08a3d..f573f2fe1e25b 100644
--- a/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp
@@ -41,7 +41,9 @@ Error StepOutRequestHandler::Run(const StepOutArguments &arguments) const {
// "threadCausedFocus" boolean value in the "stopped" events.
dap.focus_tid = thread.GetThreadID();
lldb::SBError error;
- thread.StepOut(error);
+ thread.StepOut(arguments.singleThread.value_or(false) ? eOnlyThisThread
+ : eOnlyDuringStepping,
+ error);
return ToError(error);
}
>From bacba983a91631c6d83dff33b2243883b105a8ce Mon Sep 17 00:00:00 2001
From: Wanyi Ye <wanyi at meta.com>
Date: Thu, 28 May 2026 16:50:57 -0700
Subject: [PATCH 2/3] [lldb-dap] Add missing
supportsSingleThreadExecutionRequests in capabilities
Summary:
Test Plan:
---
lldb/docs/use/lldbdap.md | 2 +-
lldb/tools/lldb-dap/DAP.cpp | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/lldb/docs/use/lldbdap.md b/lldb/docs/use/lldbdap.md
index a4c151a2ff39c..7060a1f8791fd 100644
--- a/lldb/docs/use/lldbdap.md
+++ b/lldb/docs/use/lldbdap.md
@@ -109,7 +109,7 @@ for more information.
| supportsSteppingGranularity | Y | The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests. |
| supportsInstructionBreakpoints | Y | The debug adapter supports adding breakpoints based on instruction references. |
| supportsExceptionFilterOptions | N | The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request. |
-| supportsSingleThreadExecutionRequests | N | The debug adapter supports the `singleThread` property on the execution requests (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`). |
+| supportsSingleThreadExecutionRequests | Y | The debug adapter supports the `singleThread` property on the execution requests (lldb-dap supported: `continue`, `next`, `stepIn`, `stepOut`; lldb-dap not yet supported: `reverseContinue`, `stepBack`). |
| supportsDataBreakpointBytes | Y | The debug adapter supports the `asAddress` and `bytes` fields in the `dataBreakpointInfo` request. |
| breakpointModes | `[]` | Modes of breakpoints supported by the debug adapter, such as 'hardware' or 'software'. If present, the client may allow the user to select a mode and include it in its `setBreakpoints` request. Clients may present the first applicable mode in this array as the 'default' mode in gestures that set breakpoints. |
| supportsANSIStyling | Y | The debug adapter supports ANSI escape sequences in styling of `OutputEvent.output` and `Variable.value` fields. |
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index cb34c7b2fd1e8..81388eae2a275 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -1220,6 +1220,7 @@ protocol::Capabilities DAP::GetCapabilities() {
// Supported capabilities that are not specific to a single request.
capabilities.supportedFeatures = {
protocol::eAdapterFeatureLogPoints,
+ protocol::eAdapterFeatureSingleThreadExecutionRequests,
protocol::eAdapterFeatureSteppingGranularity,
protocol::eAdapterFeatureValueFormattingOptions,
};
>From 9415b7e6ee9914d48b24683cd0233893a60ac456 Mon Sep 17 00:00:00 2001
From: Wanyi Ye <wanyi at meta.com>
Date: Mon, 1 Jun 2026 16:11:43 -0700
Subject: [PATCH 3/3] [lldb-dap] Reflect singleThread execution in continued
event
Make sure we set the allThreadsContinued accordingly after execution control. Reset the value to true after sending each continued event in case user issue any lldb command through expr eval to control the process execution.
---
.../test/tools/lldb-dap/dap_server.py | 12 +++++-
.../test/tools/lldb-dap/lldbdap_testcase.py | 9 ++++-
.../API/tools/lldb-dap/step/TestDAP_step.py | 37 +++++++++++++++++++
lldb/tools/lldb-dap/DAP.h | 3 ++
lldb/tools/lldb-dap/EventHelper.cpp | 6 ++-
.../Handler/ContinueRequestHandler.cpp | 1 +
.../lldb-dap/Handler/NextRequestHandler.cpp | 1 +
.../lldb-dap/Handler/StepInRequestHandler.cpp | 1 +
.../Handler/StepOutRequestHandler.cpp | 6 +--
9 files changed, 68 insertions(+), 8 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 b8d44d3822703..c6420cf72ead5 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
@@ -246,6 +246,7 @@ def __init__(
self.frame_scopes: Dict[str, Any] = {}
# keyed by breakpoint id
self.resolved_breakpoints: dict[int, Breakpoint] = {}
+ self.last_continued_event_body: Optional[Dict] = None
# Modifiers used when replaying a log file.
self._mod = ReplayMods()
@@ -471,6 +472,7 @@ def _handle_event(self, packet: Event) -> None:
elif event == "continued" and body:
# When the process continues, clear the known threads and
# thread_stop_reasons.
+ self.last_continued_event_body = body
all_threads_continued = body.get("allThreadsContinued", True)
tid = body["threadId"]
if tid in self.thread_stop_reasons:
@@ -1262,14 +1264,18 @@ def request_launch(
self.launch_or_attach_sent = True
return self.send_packet(command_dict)
- def request_next(self, threadId, granularity="statement"):
+ def request_next(self, threadId, granularity="statement", singleThread=None):
if self.exit_status is not None:
raise ValueError("request_continue called after process exited")
args_dict = {"threadId": threadId, "granularity": granularity}
+ if singleThread is not None:
+ args_dict["singleThread"] = singleThread
command_dict = {"command": "next", "type": "request", "arguments": args_dict}
return self._send_recv(command_dict)
- def request_stepIn(self, threadId, targetId, granularity="statement"):
+ def request_stepIn(
+ self, threadId, targetId, granularity="statement", singleThread=None
+ ):
if self.exit_status is not None:
raise ValueError("request_stepIn called after process exited")
if threadId is None:
@@ -1279,6 +1285,8 @@ def request_stepIn(self, threadId, targetId, granularity="statement"):
"targetId": targetId,
"granularity": granularity,
}
+ if singleThread is not None:
+ args_dict["singleThread"] = singleThread
command_dict = {"command": "stepIn", "type": "request", "arguments": args_dict}
return self._send_recv(command_dict)
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 22562308614a2..2f6b276c3bcc1 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
@@ -418,9 +418,13 @@ def stepIn(
targetId=None,
waitForStop=True,
granularity="statement",
+ singleThread=None,
):
response = self.dap_server.request_stepIn(
- threadId=threadId, targetId=targetId, granularity=granularity
+ threadId=threadId,
+ targetId=targetId,
+ granularity=granularity,
+ singleThread=singleThread,
)
self.assertTrue(response["success"])
if waitForStop:
@@ -432,9 +436,10 @@ def stepOver(
threadId=None,
waitForStop=True,
granularity="statement",
+ singleThread=None,
):
response = self.dap_server.request_next(
- threadId=threadId, granularity=granularity
+ threadId=threadId, granularity=granularity, singleThread=singleThread
)
self.assertTrue(
response["success"], f"next request failed: response {response}"
diff --git a/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py b/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py
index a28caa2f4c5e3..45dc6f4454772 100644
--- a/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py
+++ b/lldb/test/API/tools/lldb-dap/step/TestDAP_step.py
@@ -124,6 +124,43 @@ def test_step_out_single_thread(self):
self.assertEqual(src1, src3, "verify step out source")
break
+ @skipIfWindows
+ def test_all_threads_continued_event(self):
+ """
+ Tests that the continued event correctly reports allThreadsContinued
+ based on whether singleThread was set in the stepping request.
+
+ Since the continued event logic is identical for all stepping
+ operations, use only two step operations (stepIn + stepOut) to avoid
+ exhausting the small recursive test program on architectures where steps
+ consume source locations more aggressively.
+ """
+ program = self.getBuildArtifact("a.out")
+ self.build_and_launch(program)
+ source = "main.cpp"
+ breakpoint1_line = line_number(source, "// breakpoint 1")
+ breakpoint_ids = self.set_source_breakpoints(source, [breakpoint1_line])
+ self.continue_to_breakpoints(breakpoint_ids)
+
+ tid = self.dap_server.get_thread_id()
+
+ # Step in without singleThread: allThreadsContinued should be true.
+ self.stepIn(threadId=tid, waitForStop=True)
+ self.assertIsNotNone(self.dap_server.last_continued_event_body)
+ self.assertTrue(
+ self.dap_server.last_continued_event_body.get("allThreadsContinued", None),
+ "stepIn without singleThread should report allThreadsContinued=true",
+ )
+
+ # Step out with singleThread=True: allThreadsContinued should be false.
+ self.stepOut(threadId=tid, waitForStop=True, singleThread=True)
+ self.assertFalse(
+ self.dap_server.last_continued_event_body.get("allThreadsContinued", None),
+ "stepOut with singleThread should report allThreadsContinued=false",
+ )
+
+ self.continue_to_exit()
+
def test_step_over_inlined_function(self):
"""
Test stepping over when the program counter is in another file.
diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h
index 3e2b1d4782147..3e20c715f56fb 100644
--- a/lldb/tools/lldb-dap/DAP.h
+++ b/lldb/tools/lldb-dap/DAP.h
@@ -118,6 +118,9 @@ struct DAP final : public DAPTransport::MessageHandler {
/// The focused thread for this DAP session.
lldb::tid_t focus_tid = LLDB_INVALID_THREAD_ID;
+ /// Whether the last continue or step operation resumed all threads.
+ bool all_threads_continued = true;
+
llvm::once_flag terminated_event_flag;
bool stop_at_entry = false;
bool is_attach = false;
diff --git a/lldb/tools/lldb-dap/EventHelper.cpp b/lldb/tools/lldb-dap/EventHelper.cpp
index e9f64d8e72d08..84046eb4537a3 100644
--- a/lldb/tools/lldb-dap/EventHelper.cpp
+++ b/lldb/tools/lldb-dap/EventHelper.cpp
@@ -340,9 +340,13 @@ void SendContinuedEvent(DAP &dap) {
llvm::json::Object event(CreateEventObject("continued"));
llvm::json::Object body;
body.try_emplace("threadId", (int64_t)dap.focus_tid);
- body.try_emplace("allThreadsContinued", true);
+ body.try_emplace("allThreadsContinued", dap.all_threads_continued);
event.try_emplace("body", std::move(body));
dap.SendJSON(llvm::json::Value(std::move(event)));
+
+ // Reset to default so that any unexpected resume (e.g. expression evaluation
+ // running a command) defaults to reporting all threads continued.
+ dap.all_threads_continued = true;
}
// Send a "exited" event to indicate the process has exited.
diff --git a/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp
index 361c86421cf1b..32d59d570079e 100644
--- a/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/ContinueRequestHandler.cpp
@@ -44,6 +44,7 @@ ContinueRequestHandler::Run(const ContinueArguments &args) const {
ContinueResponseBody body;
body.allThreadsContinued = !args.singleThread;
+ dap.all_threads_continued = body.allThreadsContinued;
return body;
}
diff --git a/lldb/tools/lldb-dap/Handler/NextRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/NextRequestHandler.cpp
index 59df5498943f2..2c3b9705b8e3d 100644
--- a/lldb/tools/lldb-dap/Handler/NextRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/NextRequestHandler.cpp
@@ -37,6 +37,7 @@ Error NextRequestHandler::Run(const NextArguments &args) const {
// Remember the thread ID that caused the resume so we can set the
// "threadCausedFocus" boolean value in the "stopped" events.
dap.focus_tid = thread.GetThreadID();
+ dap.all_threads_continued = !args.singleThread;
lldb::SBError error;
if (args.granularity == eSteppingGranularityInstruction) {
thread.StepInstruction(/*step_over=*/true, error);
diff --git a/lldb/tools/lldb-dap/Handler/StepInRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/StepInRequestHandler.cpp
index ba4457e62731c..198165a78d131 100644
--- a/lldb/tools/lldb-dap/Handler/StepInRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/StepInRequestHandler.cpp
@@ -42,6 +42,7 @@ Error StepInRequestHandler::Run(const StepInArguments &args) const {
// Remember the thread ID that caused the resume so we can set the
// "threadCausedFocus" boolean value in the "stopped" events.
dap.focus_tid = thread.GetThreadID();
+ dap.all_threads_continued = !args.singleThread;
lldb::SBError error;
if (args.granularity == eSteppingGranularityInstruction) {
diff --git a/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp
index f573f2fe1e25b..d1d24288cde43 100644
--- a/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp
+++ b/lldb/tools/lldb-dap/Handler/StepOutRequestHandler.cpp
@@ -40,10 +40,10 @@ Error StepOutRequestHandler::Run(const StepOutArguments &arguments) const {
// Remember the thread ID that caused the resume so we can set the
// "threadCausedFocus" boolean value in the "stopped" events.
dap.focus_tid = thread.GetThreadID();
+ bool single_thread = arguments.singleThread.value_or(false);
+ dap.all_threads_continued = !single_thread;
lldb::SBError error;
- thread.StepOut(arguments.singleThread.value_or(false) ? eOnlyThisThread
- : eOnlyDuringStepping,
- error);
+ thread.StepOut(single_thread ? eOnlyThisThread : eOnlyDuringStepping, error);
return ToError(error);
}
More information about the lldb-commits
mailing list