Skip to content

[lldb][lldb-dap] Implement jump to cursor #130503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
aa8ce4d
[lldb-dap] implement jump to cursor.
da-viper Mar 9, 2025
d7342ea
[lldb][lldb-dap] add jump to cursor tests
da-viper Mar 9, 2025
d468264
[lldb-dap] Rever removing libXML version
da-viper Mar 9, 2025
bdfa47c
[lldb][lldb-dap] fix code format
da-viper Mar 9, 2025
03f6598
Update lldb/tools/lldb-dap/Handler/GoToTargetsRequestHandler.cpp
da-viper Mar 11, 2025
5bec4a9
Update lldb/tools/lldb-dap/DAP.h
da-viper Mar 11, 2025
31df5a3
Update lldb/tools/lldb-dap/Handler/GoToRequestHandler.cpp
da-viper Mar 11, 2025
f02a341
[lldb][lldb-dap] add review commits
da-viper Mar 11, 2025
988b9ce
[lldb][lldb-dap] add review changes
da-viper Mar 11, 2025
ca875e4
[lldb][lldb-dap] Update jump to cursor test
da-viper Mar 11, 2025
6373d10
[lldb][lldb-dap] add new test for goto execute again.
da-viper Mar 11, 2025
64a2b9c
[lldb][lldb-dap] add helper function `SendThreadGotoEvent`
da-viper Mar 12, 2025
ae46dce
[lldb][lldb-dap] Rename `gotoTarget` to `gotoTargets` and improve val…
da-viper Mar 12, 2025
05a3ebb
Update lldb/test/API/tools/lldb-dap/gotoTargets/TestDAP_gotoTargets.py
da-viper Mar 13, 2025
e8697c7
Update lldb/test/API/tools/lldb-dap/gotoTargets/TestDAP_gotoTargets.py
da-viper Mar 13, 2025
37d3487
Update lldb/test/API/tools/lldb-dap/gotoTargets/TestDAP_gotoTargets.py
da-viper Mar 13, 2025
e038671
Update lldb/test/API/tools/lldb-dap/gotoTargets/TestDAP_gotoTargets.py
da-viper Mar 13, 2025
2d9b7a8
[lldb][lldb-dap] use breakpoints to verify goto location.
da-viper Mar 14, 2025
b36ca14
Add Release notes
da-viper Mar 18, 2025
3977645
[lldb-dap] Update with review changes
da-viper Mar 20, 2025
cdaf3b4
[lldb-dap] update gototargets to use the new protocol
da-viper Mar 20, 2025
9173d8c
[lldb-dap] Fix code format
da-viper Mar 20, 2025
17e6c85
[lldb][lldb-dap] enable gotoTargets capability
da-viper Mar 20, 2025
066f2e2
[lldb][lldb-dap] use std::vector instead of llvm::DenseMap.
da-viper Mar 26, 2025
3ff3281
[lldb][lldb-dap] use llvm::smallVector as gotargets are usually few
da-viper Mar 26, 2025
5c6140c
[lldb][lldb-dap] Update with review changes
da-viper Mar 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,33 @@ def request_exceptionInfo(self, threadId=None):
}
return self.send_recv(command_dict)

def request_goto(self, threadId: int, targetId: int):
command_dict = {
"command": "goto",
"type": "request",
"arguments": {
"threadId": threadId,
"targetId": targetId,
},
}
return self.send_recv(command_dict)

def request_gotoTargets(self, filename: str, path: str, line: int, column: int):
arguments = {
"source": {
"name": filename,
"path": path,
},
"line": line,
"column": column,
}
command_dict = {
"command": "gotoTargets",
"type": "request",
"arguments": arguments,
}
return self.send_recv(command_dict)

def request_initialize(self, sourceInitFile):
command_dict = {
"command": "initialize",
Expand Down
3 changes: 3 additions & 0 deletions lldb/test/API/tools/lldb-dap/gotoTargets/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
C_SOURCES := main.c

include Makefile.rules
145 changes: 145 additions & 0 deletions lldb/test/API/tools/lldb-dap/gotoTargets/TestDAP_gotoTargets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
Test lldb-dap gotoTarget request
"""

from typing import Dict, Any
from unittest import SkipTest

from lldbsuite.test.lldbtest import line_number
import lldbdap_testcase
import os


class TestDAP_gotoTargets(lldbdap_testcase.DAPTestCaseBase):
def verify_variable(
self, actual_dict: Dict[str, Any], expected_dict: Dict[str, Any]
):
for key, value in expected_dict.items():
actual_value = actual_dict[key]
self.assertEqual(
actual_value,
value,
f"values does not match for key: `{key}` expected_value: `{value}`, actual_value: `{actual_value}`",
)

def test_default(self):
"""
Tests the jump to cursor of a simple program. No arguments,
environment, or anything else is specified.
This does not run any statement between the current breakpoint
and the jump line location.
"""
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)

source_file = "main.c"
self.source_path = os.path.join(os.getcwd(), source_file)
self.set_source_breakpoints(
source_file, [line_number(source_file, "// breakpoint 1")]
)
self.continue_to_next_stop()

first_var_1_object = self.dap_server.get_local_variable("var_1")
self.assertEqual(first_var_1_object["value"], "10")

goto_line = line_number(source_file, "// goto 1")
goto_column = 1
response = self.dap_server.request_gotoTargets(
source_file, self.source_path, goto_line, goto_column
)

self.assertEqual(
response["success"], True, "request for gotoTargets should be successful"
)
target = response["body"]["targets"][0]
self.assertGreaterEqual(
target["id"], 0, "targetId should be greater than or equal to zero"
)

target_id = target["id"]
thread_id = self.dap_server.get_thread_id()
self.assertIsNotNone(thread_id, "threadId should not be none")

response = self.dap_server.request_goto(thread_id, target_id)
self.assertEqual(
response["success"], True, "goto request with targetId should be successful"
)

stopped_events = self.dap_server.wait_for_stopped(timeout=0.200)
is_goto = lambda event: event["body"]["reason"] == "goto"
has_goto_event = any(map(is_goto, stopped_events))
self.assertEqual(
has_goto_event, True, "expected a stopped event with reason `goto`"
)

self.dap_server.request_next(thread_id)
self.continue_to_next_stop()

# Verify that `var_1=10` and `var_2=40`. This combination is only possible by
# skipping execution of a line from the original program. Observing this combination
# hence proves that our `goto` request actually skipped execution of the code line.
var1_variable = self.dap_server.get_local_variable("var_1")
var_1_expected = {
"name": "var_1",
"type": "int",
"value": "10",
"variablesReference": 0,
}
self.verify_variable(var1_variable, var_1_expected)

var2_variable = self.dap_server.get_local_variable("var_2")
var_2_expected = {
"name": "var_2",
"type": "int",
"value": "40",
"variablesReference": 0,
}
self.verify_variable(var2_variable, var_2_expected)

self.continue_to_exit()

def test_execute_again(self):
program = self.getBuildArtifact("a.out")
self.build_and_launch(program)

source_file = "main.c"
self.source_path = os.path.join(os.getcwd(), source_file)
self.set_source_breakpoints(
source_file, [line_number(source_file, "// breakpoint 2")]
)
self.continue_to_next_stop()

end_var_3_value = self.dap_server.get_local_variable_value("var_3")
self.assertEqual(end_var_3_value, "99")

goto_line = line_number(source_file, "// goto 2")
goto_column = 1
response = self.dap_server.request_gotoTargets(
source_file, self.source_path, goto_line, goto_column
)

target = response["body"]["targets"][0]
self.assertGreaterEqual(
target["id"], 0, "targetId should be greater than or equal to zero"
)

target_id = target["id"]
thread_id = self.dap_server.get_thread_id()
self.assertIsNotNone(thread_id, "threadId should not be none")

response = self.dap_server.request_goto(thread_id, target_id)
self.assertEqual(response["success"], True, "expects success to go to targetId")

stopped_events = self.dap_server.wait_for_stopped(timeout=0.200) # 200ms
is_goto = lambda event: event["body"]["reason"] == "goto"
has_goto_event = any(map(is_goto, stopped_events))
self.assertEqual(has_goto_event, True, "expects stopped event with reason goto")

self.dap_server.request_next(thread_id)
self.continue_to_next_stop()

goto_var_3_value = self.dap_server.get_local_variable_value("var_3")
self.assertEqual(goto_var_3_value, "10")

self.continue_to_next_stop()
self.continue_to_exit()
20 changes: 20 additions & 0 deletions lldb/test/API/tools/lldb-dap/gotoTargets/main.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

int test_execute_again() {
int var_3 = 10; // goto 2

var_3 = 99;

return var_3; // breakpoint 2
}

int main() {

int var_1 = 10;

var_1 = 20; // breakpoint 1

int var_2 = 40; // goto 1

int result = test_execute_again();
return 0;
}
2 changes: 2 additions & 0 deletions lldb/tools/lldb-dap/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ add_lldb_tool(lldb-dap
Handler/DisconnectRequestHandler.cpp
Handler/EvaluateRequestHandler.cpp
Handler/ExceptionInfoRequestHandler.cpp
Handler/GoToRequestHandler.cpp
Handler/GoToTargetsRequestHandler.cpp
Handler/InitializeRequestHandler.cpp
Handler/LaunchRequestHandler.cpp
Handler/LocationsRequestHandler.cpp
Expand Down
18 changes: 16 additions & 2 deletions lldb/tools/lldb-dap/DAP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ DAP::DAP(llvm::StringRef path, Log *log, const ReplMode default_repl_mode,
configuration_done_sent(false), waiting_for_run_in_terminal(false),
progress_event_reporter(
[&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }),
reverse_request_seq(0), repl_mode(default_repl_mode) {}
reverse_request_seq(0), repl_mode(default_repl_mode), gotos() {}

DAP::~DAP() = default;

Expand Down Expand Up @@ -841,6 +841,20 @@ lldb::SBError DAP::WaitForProcessToStop(uint32_t seconds) {
return error;
}

std::optional<lldb::SBLineEntry> Gotos::GetLineEntry(uint64_t id) const {
if (id > line_entries.size())
return std::nullopt;

return line_entries[id - 1]; // id starts at one.
}

uint64_t Gotos::InsertLineEntry(lldb::SBLineEntry line_entry) {
line_entries.emplace_back(line_entry);
return line_entries.size();
}

void Gotos::Clear() { line_entries.clear(); }

void Variables::Clear() {
locals.Clear();
globals.Clear();
Expand Down Expand Up @@ -1153,13 +1167,13 @@ llvm::StringMap<bool> DAP::GetCapabilities() {
capabilities["supportsDelayedStackTraceLoading"] = true;
capabilities["supportsEvaluateForHovers"] = true;
capabilities["supportsExceptionOptions"] = true;
capabilities["supportsGotoTargetsRequest"] = true;
capabilities["supportsLogPoints"] = true;
capabilities["supportsProgressReporting"] = true;
capabilities["supportsSteppingGranularity"] = true;
capabilities["supportsValueFormattingOptions"] = true;

// Unsupported capabilities.
capabilities["supportsGotoTargetsRequest"] = false;
capabilities["supportsLoadedSourcesRequest"] = false;
capabilities["supportsRestartFrame"] = false;
capabilities["supportsStepBack"] = false;
Expand Down
24 changes: 23 additions & 1 deletion lldb/tools/lldb-dap/DAP.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ enum class PacketStatus {

enum class ReplMode { Variable = 0, Command, Auto };

class Gotos {
public:
/// \return the line_entry corresponding with \p id
///
/// If \p id is invalid std::nullopt is returned.
std::optional<lldb::SBLineEntry> GetLineEntry(uint64_t id) const;

/// Insert a new \p line_entry.
/// \return id assigned to this line_entry.
uint64_t InsertLineEntry(lldb::SBLineEntry line_entry);

/// Clears all line entries and reset the generated ids.
void Clear();

private:
llvm::SmallVector<lldb::SBLineEntry> line_entries;
};

struct Variables {
/// Variable_reference start index of permanent expandable variable.
static constexpr int64_t PermanentVariableStartIndex = (1ll << 32);
Expand Down Expand Up @@ -205,6 +223,7 @@ struct DAP {
// empty; if the previous expression was a variable expression, this string
// will contain that expression.
std::string last_nonempty_var_expression;
Gotos gotos;

/// Creates a new DAP sessions.
///
Expand Down Expand Up @@ -367,7 +386,10 @@ struct DAP {
llvm::StringMap<bool> GetCapabilities();

/// Debuggee will continue from stopped state.
void WillContinue() { variables.Clear(); }
void WillContinue() {
variables.Clear();
gotos.Clear();
}

/// Poll the process to wait for it to reach the eStateStopped state.
///
Expand Down
62 changes: 62 additions & 0 deletions lldb/tools/lldb-dap/Handler/GoToRequestHandler.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//===-- GoToRequestHandler.cpp --------------------------------------===//
//
// 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 "DAP.h"
#include "EventHelper.h"
#include "JSONUtils.h"
#include "Protocol/ProtocolRequests.h"
#include "RequestHandler.h"

namespace lldb_dap {

/// Creates an \p StoppedEvent with the reason \a goto.
static void SendThreadGotoEvent(DAP &dap, lldb::tid_t thread_id) {
llvm::json::Object event(CreateEventObject("stopped"));
llvm::json::Object body;
body.try_emplace("reason", "goto");
body.try_emplace("description", "Paused on Jump To Cursor");
body.try_emplace("threadId", thread_id);
body.try_emplace("preserveFocusHint", false);
body.try_emplace("allThreadsStopped", true);

event.try_emplace("body", std::move(body));
dap.SendJSON(llvm::json::Value(std::move(event)));
}

llvm::Expected<protocol::GotoResponseBody>
GotoRequestHandler::Run(const protocol::GotoArguments &args) const {
const lldb::tid_t thread_id = args.threadId;
lldb::SBThread current_thread =
dap.target.GetProcess().GetThreadByID(thread_id);

if (!current_thread.IsValid()) {
return llvm::createStringError(llvm::formatv("Thread id `{0}` is not valid",
current_thread.GetThreadID()));
}

const uint64_t target_id = args.targetId;
const std::optional<lldb::SBLineEntry> line_entry =
dap.gotos.GetLineEntry(target_id);
if (!line_entry) {
return llvm::createStringError(
llvm::formatv("Target id `{0}` is not valid", thread_id));
}

lldb::SBFileSpec file_spec = line_entry->GetFileSpec();
const lldb::SBError error =
current_thread.JumpToLine(file_spec, line_entry->GetLine());

if (error.Fail()) {
return llvm::make_error<DAPError>(error.GetCString());
}

SendThreadGotoEvent(dap, thread_id);
return protocol::GotoResponseBody();
}

} // namespace lldb_dap
Loading