Skip to content

Commit 4ea1994

Browse files
authored
[lldb-dap] Adjusting how repl-mode auto determines commands vs variable expressions. (#78005)
The previous logic for determining if an expression was a command or variable expression in the repl would incorrectly identify the context in many common cases where a local variable name partially overlaps with the repl input. For example: ``` int foo() { int var = 1; // break point, evaluating "p var", previously emitted a warning } ``` Instead of checking potentially multiple conflicting values against the expression input, I updated the heuristic to only consider the first term. This is much more reliable at eliminating false positives when the input does not actually hide a local variable. Additionally, I updated the warning on conflicts to occur anytime the conflict is detected since the specific conflict can change based on the current input. This also includes additional details on how users can change the behavior. Example Debug Console output from lldb/test/API/tools/lldb-dap/evaluate/main.cpp:11 breakpoint 3. ``` lldb-dap> var + 3 Warning: Expression 'var' is both an LLDB command and variable. It will be evaluated as a variable. To evaluate the expression as an LLDB command, use '`' as a prefix. 45 lldb-dap> var + 1 Warning: Expression 'var' is both an LLDB command and variable. It will be evaluated as a variable. To evaluate the expression as an LLDB command, use '`' as a prefix. 43 ```
1 parent 2cd013a commit 4ea1994

File tree

5 files changed

+75
-57
lines changed

5 files changed

+75
-57
lines changed

lldb/test/API/tools/lldb-dap/completions/TestDAP_completions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,13 @@ def test_completions(self):
4141
{
4242
"text": "var",
4343
"label": "var -- vector<baz> &",
44-
}
45-
],
46-
[
44+
},
4745
{
4846
"text": "var",
4947
"label": "var -- Show variables for the current stack frame. Defaults to all arguments and local variables in scope. Names of argument, local, file static and file global variables can be specified.",
5048
},
49+
],
50+
[
5151
{"text": "var1", "label": "var1 -- int &"},
5252
],
5353
)

lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ def run_test_evaluate_expressions(
8787
)
8888
self.assertEvaluate("struct3", "0x.*0")
8989

90-
self.assertEvaluateFailure("var") # local variable of a_function
90+
if context == "repl":
91+
# In the repl context expressions may be interpreted as lldb
92+
# commands since no variables have the same name as the command.
93+
self.assertEvaluate("var", r"\(lldb\) var\n.*")
94+
else:
95+
self.assertEvaluateFailure("var") # local variable of a_function
96+
9197
self.assertEvaluateFailure("my_struct") # type name
9298
self.assertEvaluateFailure("int") # type name
9399
self.assertEvaluateFailure("foo") # member of my_struct

lldb/tools/lldb-dap/DAP.cpp

Lines changed: 35 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ DAP::DAP()
4747
configuration_done_sent(false), waiting_for_run_in_terminal(false),
4848
progress_event_reporter(
4949
[&](const ProgressEvent &event) { SendJSON(event.ToJSON()); }),
50-
reverse_request_seq(0), repl_mode(ReplMode::Auto),
51-
auto_repl_mode_collision_warning(false) {
50+
reverse_request_seq(0), repl_mode(ReplMode::Auto) {
5251
const char *log_file_path = getenv("LLDBDAP_LOG");
5352
#if defined(_WIN32)
5453
// Windows opens stdout and stdin in text mode which converts \n to 13,10
@@ -380,12 +379,12 @@ llvm::json::Value DAP::CreateTopLevelScopes() {
380379
return llvm::json::Value(std::move(scopes));
381380
}
382381

383-
ExpressionContext DAP::DetectExpressionContext(lldb::SBFrame &frame,
384-
std::string &text) {
382+
ExpressionContext DAP::DetectExpressionContext(lldb::SBFrame frame,
383+
std::string &expression) {
385384
// Include the escape hatch prefix.
386-
if (!text.empty() &&
387-
llvm::StringRef(text).starts_with(g_dap.command_escape_prefix)) {
388-
text = text.substr(g_dap.command_escape_prefix.size());
385+
if (!expression.empty() &&
386+
llvm::StringRef(expression).starts_with(g_dap.command_escape_prefix)) {
387+
expression = expression.substr(g_dap.command_escape_prefix.size());
389388
return ExpressionContext::Command;
390389
}
391390

@@ -395,43 +394,40 @@ ExpressionContext DAP::DetectExpressionContext(lldb::SBFrame &frame,
395394
case ReplMode::Command:
396395
return ExpressionContext::Command;
397396
case ReplMode::Auto:
398-
// If the frame is invalid then there is no variables to complete, assume
399-
// this is an lldb command instead.
400-
if (!frame.IsValid()) {
401-
return ExpressionContext::Command;
402-
}
403-
397+
// To determine if the expression is a command or not, check if the first
398+
// term is a variable or command. If it's a variable in scope we will prefer
399+
// that behavior and give a warning to the user if they meant to invoke the
400+
// operation as a command.
401+
//
402+
// Example use case:
403+
// int p and expression "p + 1" > variable
404+
// int i and expression "i" > variable
405+
// int var and expression "va" > command
406+
std::pair<llvm::StringRef, llvm::StringRef> token =
407+
llvm::getToken(expression);
408+
std::string term = token.first.str();
404409
lldb::SBCommandReturnObject result;
405-
debugger.GetCommandInterpreter().ResolveCommand(text.data(), result);
406-
407-
// If this command is a simple expression like `var + 1` check if there is
408-
// a local variable name that is in the current expression. If so, ensure
409-
// the expression runs in the variable context.
410-
lldb::SBValueList variables = frame.GetVariables(true, true, true, true);
411-
llvm::StringRef input = text;
412-
for (uint32_t i = 0; i < variables.GetSize(); i++) {
413-
llvm::StringRef name = variables.GetValueAtIndex(i).GetName();
414-
// Check both directions in case the input is a partial of a variable
415-
// (e.g. input = `va` and local variable = `var1`).
416-
if (input.contains(name) || name.contains(input)) {
417-
if (!auto_repl_mode_collision_warning) {
418-
llvm::errs() << "Variable expression '" << text
419-
<< "' is hiding an lldb command, prefix an expression "
420-
"with '"
421-
<< g_dap.command_escape_prefix
422-
<< "' to ensure it runs as a lldb command.\n";
423-
auto_repl_mode_collision_warning = true;
424-
}
425-
return ExpressionContext::Variable;
426-
}
410+
debugger.GetCommandInterpreter().ResolveCommand(term.c_str(), result);
411+
bool term_is_command = result.Succeeded();
412+
bool term_is_variable = frame.FindVariable(term.c_str()).IsValid();
413+
414+
// If we have both a variable and command, warn the user about the conflict.
415+
if (term_is_command && term_is_variable) {
416+
llvm::errs()
417+
<< "Warning: Expression '" << term
418+
<< "' is both an LLDB command and variable. It will be evaluated as "
419+
"a variable. To evaluate the expression as an LLDB command, use '"
420+
<< g_dap.command_escape_prefix << "' as a prefix.\n";
427421
}
428422

429-
if (result.Succeeded()) {
430-
return ExpressionContext::Command;
431-
}
423+
// Variables take preference to commands in auto, since commands can always
424+
// be called using the command_escape_prefix
425+
return term_is_variable ? ExpressionContext::Variable
426+
: term_is_command ? ExpressionContext::Command
427+
: ExpressionContext::Variable;
432428
}
433429

434-
return ExpressionContext::Variable;
430+
llvm_unreachable("enum cases exhausted.");
435431
}
436432

437433
bool DAP::RunLLDBCommands(llvm::StringRef prefix,

lldb/tools/lldb-dap/DAP.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,6 @@ struct DAP {
189189
StartDebuggingRequestHandler start_debugging_request_handler;
190190
ReplModeRequestHandler repl_mode_request_handler;
191191
ReplMode repl_mode;
192-
bool auto_repl_mode_collision_warning;
193192
std::string command_escape_prefix = "`";
194193
lldb::SBFormat frame_format;
195194
lldb::SBFormat thread_format;
@@ -225,8 +224,12 @@ struct DAP {
225224

226225
llvm::json::Value CreateTopLevelScopes();
227226

228-
ExpressionContext DetectExpressionContext(lldb::SBFrame &frame,
229-
std::string &text);
227+
/// \return
228+
/// Attempt to determine if an expression is a variable expression or
229+
/// lldb command using a hueristic based on the first term of the
230+
/// expression.
231+
ExpressionContext DetectExpressionContext(lldb::SBFrame frame,
232+
std::string &expression);
230233

231234
/// \return
232235
/// \b false if a fatal error was found while executing these commands,

lldb/tools/lldb-dap/lldb-dap.cpp

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
#endif
3838

3939
#include <algorithm>
40+
#include <array>
4041
#include <chrono>
4142
#include <fstream>
4243
#include <map>
@@ -1126,21 +1127,33 @@ void request_completions(const llvm::json::Object &request) {
11261127
}
11271128
llvm::json::Array targets;
11281129

1129-
if (g_dap.DetectExpressionContext(frame, text) ==
1130-
ExpressionContext::Variable) {
1131-
char command[] = "expression -- ";
1132-
text = command + text;
1133-
offset += strlen(command);
1134-
}
1135-
lldb::SBStringList matches;
1136-
lldb::SBStringList descriptions;
1130+
if (!text.empty() &&
1131+
llvm::StringRef(text).starts_with(g_dap.command_escape_prefix)) {
1132+
text = text.substr(g_dap.command_escape_prefix.size());
1133+
}
1134+
1135+
// While the user is typing then we likely have an incomplete input and cannot
1136+
// reliably determine the precise intent (command vs variable), try completing
1137+
// the text as both a command and variable expression, if applicable.
1138+
const std::string expr_prefix = "expression -- ";
1139+
std::array<std::tuple<ReplMode, std::string, uint64_t>, 2> exprs = {
1140+
{std::make_tuple(ReplMode::Command, text, offset),
1141+
std::make_tuple(ReplMode::Variable, expr_prefix + text,
1142+
offset + expr_prefix.size())}};
1143+
for (const auto &[mode, line, cursor] : exprs) {
1144+
if (g_dap.repl_mode != ReplMode::Auto && g_dap.repl_mode != mode)
1145+
continue;
1146+
1147+
lldb::SBStringList matches;
1148+
lldb::SBStringList descriptions;
1149+
if (!g_dap.debugger.GetCommandInterpreter()
1150+
.HandleCompletionWithDescriptions(line.c_str(), cursor, 0, 100,
1151+
matches, descriptions))
1152+
continue;
11371153

1138-
if (g_dap.debugger.GetCommandInterpreter().HandleCompletionWithDescriptions(
1139-
text.c_str(), offset, 0, 100, matches, descriptions)) {
11401154
// The first element is the common substring after the cursor position for
11411155
// all the matches. The rest of the elements are the matches so ignore the
11421156
// first result.
1143-
targets.reserve(matches.GetSize() - 1);
11441157
for (size_t i = 1; i < matches.GetSize(); i++) {
11451158
std::string match = matches.GetStringAtIndex(i);
11461159
std::string description = descriptions.GetStringAtIndex(i);

0 commit comments

Comments
 (0)