Skip to content

Commit c3a4cd3

Browse files
committed
[lldb-dap] Adjusting how repl-mode auto determines commands vs variable expressions.
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. 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 3edf82d commit c3a4cd3

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>
@@ -1125,21 +1126,33 @@ void request_completions(const llvm::json::Object &request) {
11251126
}
11261127
llvm::json::Array targets;
11271128

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

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

0 commit comments

Comments
 (0)