https://github.com/DrSergei updated https://github.com/llvm/llvm-project/pull/215228
>From 6d1c6459ea0da00ea7073b455959f012b8510061 Mon Sep 17 00:00:00 2001 From: Sergei Druzhkov <[email protected]> Date: Mon, 10 Aug 2026 12:17:03 +0300 Subject: [PATCH 1/4] [lldb-dap] Preserve watchpoints from console --- .../TestDAP_setDataBreakpoints.py | 41 +++++++++++++++++++ lldb/tools/lldb-dap/DAP.h | 1 + .../SetDataBreakpointsRequestHandler.cpp | 8 +++- lldb/tools/lldb-dap/Watchpoint.h | 2 + 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py index 225a947674351..bad48b0ffbf0f 100644 --- a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py +++ b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py @@ -182,6 +182,47 @@ def test_functionality(self): stop_event = session.continue_to_breakpoint(self.expect_not_none(bp_cond.id)) self.assertEqual(session.top_frame_from(stop_event).locals["x"].value, "10") + @skipIfWindows + def test_console_watchpoint_preserved(self): + """Test setDataBreakpoints must not delete watchpoints created via the console.""" + source = "main.cpp" + program = self.getBuildArtifact("a.out") + session = self.build_and_create_session() + first_loop_break_line = line_number(source, "// first loop breakpoint") + with session.configure(LaunchArgs(program)) as ctx: + session.resolve_source_breakpoints(source, [first_loop_break_line]) + stop_event = session.verify_stopped_on_breakpoint(after=ctx.process_event) + top_frame = session.top_frame_from(stop_event) + + # Create a watchpoint via the LLDB console. + resp_body = session.evaluate("`watchpoint set variable x", context="repl") + session.verify_evaluate(resp_body, matches=r".*Watchpoint created.*") + resp_body = session.evaluate("`watchpoint list", context="repl") + session.verify_evaluate(resp_body, matches=r".*Watchpoint 1:.*") + + # Set a data breakpoint via DAP. + arr = top_frame.locals["arr"] + arr_var_ref = self.expect_not_none(arr.variablesReference) + response_arr_2 = session.data_breakpoint_info( + "[2]", arr_var_ref, top_frame.frame.id + ) + arr_2_data_id = self.expect_not_none(response_arr_2.body.dataId) + set_response = session.set_data_breakpoints( + [DataBreakpoint(dataId=arr_2_data_id, accessType="write")] + ) + [bp_arr_2] = set_response.body.breakpoints + self.assertTrue(bp_arr_2.verified) + + resp_body = session.evaluate("`watchpoint list", context="repl") + session.verify_evaluate(resp_body, matches=r".*Watchpoint 1:.*") + + session.set_data_breakpoints([]) + resp_body = session.evaluate("`watchpoint list", context="repl") + session.verify_evaluate(resp_body, matches=r".*Watchpoint 1:.*") + + session.evaluate("`watchpoint delete 1", context="repl") + session.continue_to_exit() + @skipIfWindows def test_bytes(self): """Tests setting data breakpoints on memory range.""" diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index 3e2b1d4782147..c09b149ae3f53 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -107,6 +107,7 @@ struct DAP final : public DAPTransport::MessageHandler { FunctionBreakpointMap function_breakpoints; InstructionBreakpointMap instruction_breakpoints; std::vector<ExceptionBreakpoint> exception_breakpoints; + std::vector<lldb::watch_id_t> data_breakpoints; /// Map step in target id to list of function targets that user can choose. llvm::DenseMap<lldb::addr_t, std::string> step_in_targets; diff --git a/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp index 1caaa23bf06f6..0540dfcd9d255 100644 --- a/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp @@ -25,7 +25,10 @@ SetDataBreakpointsRequestHandler::Run( const protocol::SetDataBreakpointsArguments &args) const { std::vector<protocol::Breakpoint> response_breakpoints; - dap.target.DeleteAllWatchpoints(); + for (lldb::watch_id_t watch_id : dap.data_breakpoints) + dap.target.DeleteWatchpoint(watch_id); + dap.data_breakpoints.clear(); + std::vector<Watchpoint> watchpoints; for (const auto &bp : args.breakpoints) watchpoints.emplace_back(dap, bp); @@ -38,6 +41,9 @@ SetDataBreakpointsRequestHandler::Run( if (addresses.count(iter->GetAddress()) == 0) { iter->SetWatchpoint(); addresses.insert(iter->GetAddress()); + if (lldb::watch_id_t watch_id = iter->GetID(); + watch_id != LLDB_INVALID_WATCH_ID) + dap.data_breakpoints.push_back(watch_id); } } for (auto wp : watchpoints) diff --git a/lldb/tools/lldb-dap/Watchpoint.h b/lldb/tools/lldb-dap/Watchpoint.h index d943e1218bdcd..f73b89e674329 100644 --- a/lldb/tools/lldb-dap/Watchpoint.h +++ b/lldb/tools/lldb-dap/Watchpoint.h @@ -34,6 +34,8 @@ class Watchpoint : public BreakpointBase { lldb::addr_t GetAddress() const { return m_addr; } + lldb::watch_id_t GetID() { return m_wp.GetID(); } + protected: lldb::addr_t m_addr; size_t m_size; >From 9f479360cf0993483cc44cbfe6d48621c7c8c16c Mon Sep 17 00:00:00 2001 From: Sergei Druzhkov <[email protected]> Date: Wed, 12 Aug 2026 18:32:13 +0300 Subject: [PATCH 2/4] Fix review comments --- .../lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py index bad48b0ffbf0f..a3cf2bbb5b360 100644 --- a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py +++ b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py @@ -5,7 +5,7 @@ from lldbsuite.test.decorators import skipIfWasm, skipIfWindows from lldbsuite.test.lldbtest import line_number from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase -from lldbsuite.test.tools.lldb_dap.types import DataBreakpoint, LaunchArgs +from lldbsuite.test.tools.lldb_dap.types import DataBreakpoint, LaunchArgs, StoppedReason @skipIfWasm # data breakpoints map to watchpoints. @@ -220,6 +220,10 @@ def test_console_watchpoint_preserved(self): resp_body = session.evaluate("`watchpoint list", context="repl") session.verify_evaluate(resp_body, matches=r".*Watchpoint 1:.*") + # Verify watchpoint from console. + stop_event = session.continue_to_next_stop(exp_reason=StoppedReason.DATA_BREAKPOINT) + self.assertEqual(session.top_frame_from(stop_event).locals["x"].value, "2") + session.evaluate("`watchpoint delete 1", context="repl") session.continue_to_exit() >From f0696d7bc73dc946fec40693f48612f8ba5d2796 Mon Sep 17 00:00:00 2001 From: Sergei Druzhkov <[email protected]> Date: Wed, 12 Aug 2026 18:48:29 +0300 Subject: [PATCH 3/4] Fix formatting --- .../databreakpoint/TestDAP_setDataBreakpoints.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py index a3cf2bbb5b360..87ec0c64b7c95 100644 --- a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py +++ b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py @@ -5,8 +5,11 @@ from lldbsuite.test.decorators import skipIfWasm, skipIfWindows from lldbsuite.test.lldbtest import line_number from lldbsuite.test.tools.lldb_dap import DAPTestCaseBase -from lldbsuite.test.tools.lldb_dap.types import DataBreakpoint, LaunchArgs, StoppedReason - +from lldbsuite.test.tools.lldb_dap.types import ( + DataBreakpoint, + LaunchArgs, + StoppedReason, +) @skipIfWasm # data breakpoints map to watchpoints. class TestDAP_setDataBreakpoints(DAPTestCaseBase): @@ -221,7 +224,9 @@ def test_console_watchpoint_preserved(self): session.verify_evaluate(resp_body, matches=r".*Watchpoint 1:.*") # Verify watchpoint from console. - stop_event = session.continue_to_next_stop(exp_reason=StoppedReason.DATA_BREAKPOINT) + stop_event = session.continue_to_next_stop( + exp_reason=StoppedReason.DATA_BREAKPOINT + ) self.assertEqual(session.top_frame_from(stop_event).locals["x"].value, "2") session.evaluate("`watchpoint delete 1", context="repl") >From 87375281c860858a4860235f665ae802b743fa9b Mon Sep 17 00:00:00 2001 From: Sergei Druzhkov <[email protected]> Date: Fri, 14 Aug 2026 13:20:03 +0300 Subject: [PATCH 4/4] Try to keep existing watchpoints --- .../TestDAP_setDataBreakpoints.py | 106 ++++++++++++++++++ lldb/tools/lldb-dap/DAP.h | 4 +- .../SetDataBreakpointsRequestHandler.cpp | 63 ++++++++--- lldb/tools/lldb-dap/Watchpoint.cpp | 8 ++ lldb/tools/lldb-dap/Watchpoint.h | 2 + 5 files changed, 166 insertions(+), 17 deletions(-) diff --git a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py index 87ec0c64b7c95..d6a055234d4a3 100644 --- a/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py +++ b/lldb/test/API/tools/lldb-dap/databreakpoint/TestDAP_setDataBreakpoints.py @@ -232,6 +232,112 @@ def test_console_watchpoint_preserved(self): session.evaluate("`watchpoint delete 1", context="repl") session.continue_to_exit() + @skipIfWindows + def test_hit_count_preserved(self): + """Test setDataBreakpoints preserves hit counts of existing watchpoints.""" + source = "main.cpp" + program = self.getBuildArtifact("a.out") + session = self.build_and_create_session() + first_loop_break_line = line_number(source, "// first loop breakpoint") + with session.configure(LaunchArgs(program)) as ctx: + session.resolve_source_breakpoints(source, [first_loop_break_line]) + stop_event = session.verify_stopped_on_breakpoint(after=ctx.process_event) + + second_loop_break_line = line_number(source, "// second loop breakpoint") + breakpoint_ids = session.resolve_source_breakpoints( + source, [second_loop_break_line] + ) + stop_event = session.continue_to_any_breakpoint(breakpoint_ids) + + top_frame_ctx = session.top_frame_from(stop_event) + frame_id = top_frame_ctx.frame.id + locals_ref = top_frame_ctx.locals.variablesReference + + response_x = session.data_breakpoint_info("x", locals_ref, frame_id) + arr = top_frame_ctx.locals["arr"] + arr_var_ref = self.expect_not_none(arr.variablesReference) + response_arr_2 = session.data_breakpoint_info("[2]", arr_var_ref, frame_id) + + x_data_id = self.expect_not_none(response_x.body.dataId) + arr_2_data_id = self.expect_not_none(response_arr_2.body.dataId) + + set_response = session.set_data_breakpoints( + [DataBreakpoint(dataId=x_data_id, accessType="write")] + ) + [bp_x] = set_response.body.breakpoints + self.assertTrue(bp_x.verified) + x_bp_id = self.expect_not_none(bp_x.id) + + # Hit the watchpoint on `x` twice. + stop_event = session.continue_to_breakpoint(x_bp_id) + self.assertEqual(session.top_frame_from(stop_event).locals["x"].value, "2") + stop_event = session.continue_to_breakpoint(x_bp_id) + self.assertEqual(session.top_frame_from(stop_event).locals["x"].value, "3") + + resp_body = session.evaluate("`watchpoint list -v", context="repl") + session.verify_evaluate(resp_body, matches=r"hit_count = 2\s") + + # Set additional data breakpoint on different variable. + set_response = session.set_data_breakpoints( + [ + DataBreakpoint(dataId=x_data_id, accessType="write"), + DataBreakpoint(dataId=arr_2_data_id, accessType="write"), + ] + ) + [bp_x2, bp_arr_2] = set_response.body.breakpoints + self.assertTrue(bp_x2.verified) + self.assertTrue(bp_arr_2.verified) + self.assertEqual(bp_x2.id, x_bp_id) + + resp_body = session.evaluate("`watchpoint list -v", context="repl") + session.verify_evaluate(resp_body, matches=r"hit_count = 2\s") + + session.set_data_breakpoints([]) + session.continue_to_exit() + + @skipIfWindows + def test_type_change_recreates(self): + """Test setDataBreakpoints recreates watchpoint in case of changing type.""" + source = "main.cpp" + program = self.getBuildArtifact("a.out") + session = self.build_and_create_session() + first_loop_break_line = line_number(source, "// first loop breakpoint") + with session.configure(LaunchArgs(program)) as ctx: + session.resolve_source_breakpoints(source, [first_loop_break_line]) + stop_event = session.verify_stopped_on_breakpoint(after=ctx.process_event) + + top_frame_ctx = session.top_frame_from(stop_event) + frame_id = top_frame_ctx.frame.id + locals_ref = top_frame_ctx.locals.variablesReference + response_x = session.data_breakpoint_info("x", locals_ref, frame_id) + x_data_id = self.expect_not_none(response_x.body.dataId) + + set_response = session.set_data_breakpoints( + [DataBreakpoint(dataId=x_data_id, accessType="write")] + ) + [bp_write] = set_response.body.breakpoints + self.assertTrue(bp_write.verified) + write_id = self.expect_not_none(bp_write.id) + + stop_event = session.continue_to_breakpoint(write_id) + self.assertEqual(session.top_frame_from(stop_event).locals["x"].value, "2") + + resp_body = session.evaluate("`watchpoint list -v", context="repl") + session.verify_evaluate(resp_body, matches=r"hit_count = 1\s") + + set_response = session.set_data_breakpoints( + [DataBreakpoint(dataId=x_data_id, accessType="readWrite")] + ) + [bp_rw] = set_response.body.breakpoints + self.assertTrue(bp_rw.verified) + self.assertNotEqual(bp_rw.id, write_id) + + resp_body = session.evaluate("`watchpoint list -v", context="repl") + session.verify_evaluate(resp_body, matches=r"hit_count = 0\s") + + session.set_data_breakpoints([]) + session.continue_to_exit() + @skipIfWindows def test_bytes(self): """Tests setting data breakpoints on memory range.""" diff --git a/lldb/tools/lldb-dap/DAP.h b/lldb/tools/lldb-dap/DAP.h index c09b149ae3f53..68a401919b3be 100644 --- a/lldb/tools/lldb-dap/DAP.h +++ b/lldb/tools/lldb-dap/DAP.h @@ -22,6 +22,7 @@ #include "SourceBreakpoint.h" #include "Transport.h" #include "Variables.h" +#include "Watchpoint.h" #include "lldb/API/SBBroadcaster.h" #include "lldb/API/SBCommandInterpreter.h" #include "lldb/API/SBDebugger.h" @@ -63,6 +64,7 @@ typedef std::map<std::pair<uint32_t, uint32_t>, SourceBreakpoint> typedef llvm::StringMap<FunctionBreakpoint> FunctionBreakpointMap; typedef llvm::DenseMap<lldb::addr_t, InstructionBreakpoint> InstructionBreakpointMap; +typedef llvm::DenseMap<lldb::addr_t, Watchpoint> WatchpointMap; using AdapterFeature = protocol::AdapterFeature; using ClientFeature = protocol::ClientFeature; @@ -107,7 +109,7 @@ struct DAP final : public DAPTransport::MessageHandler { FunctionBreakpointMap function_breakpoints; InstructionBreakpointMap instruction_breakpoints; std::vector<ExceptionBreakpoint> exception_breakpoints; - std::vector<lldb::watch_id_t> data_breakpoints; + WatchpointMap data_breakpoints; /// Map step in target id to list of function targets that user can choose. llvm::DenseMap<lldb::addr_t, std::string> step_in_targets; diff --git a/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp b/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp index 0540dfcd9d255..f6b1dec7b6cf9 100644 --- a/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp +++ b/lldb/tools/lldb-dap/Handler/SetDataBreakpointsRequestHandler.cpp @@ -11,7 +11,8 @@ #include "Protocol/ProtocolRequests.h" #include "RequestHandler.h" #include "Watchpoint.h" -#include <set> +#include "llvm/ADT/DenseSet.h" +#include <algorithm> namespace lldb_dap { @@ -23,32 +24,62 @@ namespace lldb_dap { llvm::Expected<protocol::SetDataBreakpointsResponseBody> SetDataBreakpointsRequestHandler::Run( const protocol::SetDataBreakpointsArguments &args) const { - std::vector<protocol::Breakpoint> response_breakpoints; - - for (lldb::watch_id_t watch_id : dap.data_breakpoints) - dap.target.DeleteWatchpoint(watch_id); - dap.data_breakpoints.clear(); - std::vector<Watchpoint> watchpoints; + watchpoints.reserve(args.breakpoints.size()); for (const auto &bp : args.breakpoints) watchpoints.emplace_back(dap, bp); + llvm::DenseSet<lldb::addr_t> outdated( + llvm::from_range, llvm::make_first_range(dap.data_breakpoints)); + + std::vector<protocol::Breakpoint> response_breakpoints; + response_breakpoints.reserve(watchpoints.size()); // If two watchpoints start at the same address, the latter overwrite the // former. So, we only enable those at first-seen addresses when iterating // backward. - std::set<lldb::addr_t> addresses; - for (auto iter = watchpoints.rbegin(); iter != watchpoints.rend(); ++iter) { - if (addresses.count(iter->GetAddress()) == 0) { - iter->SetWatchpoint(); - addresses.insert(iter->GetAddress()); - if (lldb::watch_id_t watch_id = iter->GetID(); + llvm::DenseSet<lldb::addr_t> addresses; + for (auto it = watchpoints.rbegin(); it != watchpoints.rend(); ++it) { + const lldb::addr_t addr = it->GetAddress(); + if (addresses.contains(addr)) { + response_breakpoints.push_back(it->ToProtocolBreakpoint()); + continue; + } + addresses.insert(addr); + outdated.erase(addr); + + auto existing = dap.data_breakpoints.find(addr); + if (existing == dap.data_breakpoints.end()) { + // Set the new one. + it->SetWatchpoint(); + dap.data_breakpoints.try_emplace(addr, *it); + response_breakpoints.push_back(it->ToProtocolBreakpoint()); + } else if (existing->second.HasSameSizeAndType(*it)) { + // Update existing. + existing->second.UpdateBreakpoint(*it); + response_breakpoints.push_back(existing->second.ToProtocolBreakpoint()); + } else { + // Delete existing and set the new one. + if (lldb::watch_id_t watch_id = existing->second.GetID(); watch_id != LLDB_INVALID_WATCH_ID) - dap.data_breakpoints.push_back(watch_id); + dap.target.DeleteWatchpoint(watch_id); + dap.data_breakpoints.erase(existing); + it->SetWatchpoint(); + dap.data_breakpoints.try_emplace(addr, *it); + response_breakpoints.push_back(it->ToProtocolBreakpoint()); } } - for (auto wp : watchpoints) - response_breakpoints.push_back(wp.ToProtocolBreakpoint()); + for (lldb::addr_t addr : outdated) { + auto it = dap.data_breakpoints.find(addr); + if (it == dap.data_breakpoints.end()) + continue; + if (lldb::watch_id_t watch_id = it->second.GetID(); + watch_id != LLDB_INVALID_WATCH_ID) + dap.target.DeleteWatchpoint(watch_id); + dap.data_breakpoints.erase(it); + } + + std::reverse(response_breakpoints.begin(), response_breakpoints.end()); return protocol::SetDataBreakpointsResponseBody{ std::move(response_breakpoints)}; } diff --git a/lldb/tools/lldb-dap/Watchpoint.cpp b/lldb/tools/lldb-dap/Watchpoint.cpp index f9512104128d2..e9a46e43dfd99 100644 --- a/lldb/tools/lldb-dap/Watchpoint.cpp +++ b/lldb/tools/lldb-dap/Watchpoint.cpp @@ -60,4 +60,12 @@ void Watchpoint::SetWatchpoint() { if (!m_hit_condition.empty()) SetHitCondition(); } + +bool Watchpoint::HasSameSizeAndType(const Watchpoint &wp) const { + return m_size == wp.m_size && + m_options.GetWatchpointTypeRead() == + wp.m_options.GetWatchpointTypeRead() && + m_options.GetWatchpointTypeWrite() == + wp.m_options.GetWatchpointTypeWrite(); +} } // namespace lldb_dap diff --git a/lldb/tools/lldb-dap/Watchpoint.h b/lldb/tools/lldb-dap/Watchpoint.h index f73b89e674329..c094027dd7b65 100644 --- a/lldb/tools/lldb-dap/Watchpoint.h +++ b/lldb/tools/lldb-dap/Watchpoint.h @@ -36,6 +36,8 @@ class Watchpoint : public BreakpointBase { lldb::watch_id_t GetID() { return m_wp.GetID(); } + bool HasSameSizeAndType(const Watchpoint &wp) const; + protected: lldb::addr_t m_addr; size_t m_size; _______________________________________________ lldb-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits
