Skip to content

Commit 6c8e716

Browse files
committed
[lldb] Inline expression evaluator error visualization
This patch is a reworking of Pete Lawrence's (@PortalPete) proposal for better expression evaluator error messages: #80938 Before: ``` $ lldb -o "expr a+b" (lldb) expr a+b error: <user expression 0>:1:1: use of undeclared identifier 'a' a+b ^ error: <user expression 0>:1:3: use of undeclared identifier 'b' a+b ^ ``` After: ``` (lldb) expr a+b ^ ^ │ ╰─ error: use of undeclared identifier 'b' ╰─ error: use of undeclared identifier 'a' ``` This eliminates the confusing `<user expression 0>:1:3` source location and avoids echoing the expression to the console again, which results in a cleaner presentation that makes it easier to grasp what's going on.
1 parent 8c682fc commit 6c8e716

File tree

18 files changed

+275
-45
lines changed

18 files changed

+275
-45
lines changed

lldb/include/lldb/Expression/DiagnosticManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ struct DiagnosticDetail {
3939
unsigned line = 0;
4040
uint16_t column = 0;
4141
uint16_t length = 0;
42+
bool hidden = false;
4243
bool in_user_input = false;
4344
};
4445
/// Contains {{}, 1, 3, 3, true} in the example above.

lldb/include/lldb/Interpreter/CommandObject.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,13 @@ class CommandObject : public std::enable_shared_from_this<CommandObject> {
340340
return false;
341341
}
342342

343+
/// Set the command input as it appeared in the terminal. This
344+
/// is used to have errors refer directly to the original command.
345+
void SetOriginalCommandString(std::string s) { m_original_command = s; }
346+
347+
/// \param offset_in_command is on what column \c args_string
348+
/// appears, if applicable. This enables diagnostics that refer back
349+
/// to the user input.
343350
virtual void Execute(const char *args_string,
344351
CommandReturnObject &result) = 0;
345352

@@ -404,6 +411,7 @@ class CommandObject : public std::enable_shared_from_this<CommandObject> {
404411
std::string m_cmd_help_short;
405412
std::string m_cmd_help_long;
406413
std::string m_cmd_syntax;
414+
std::string m_original_command;
407415
Flags m_flags;
408416
std::vector<CommandArgumentEntry> m_arguments;
409417
lldb::CommandOverrideCallback m_deprecated_command_override_callback;

lldb/source/Commands/CommandObjectExpression.cpp

Lines changed: 146 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include "CommandObjectExpression.h"
1212
#include "lldb/Core/Debugger.h"
13+
#include "lldb/Expression/DiagnosticManager.h"
1314
#include "lldb/Expression/ExpressionVariable.h"
1415
#include "lldb/Expression/REPL.h"
1516
#include "lldb/Expression/UserExpression.h"
@@ -398,6 +399,122 @@ CanBeUsedForElementCountPrinting(ValueObject &valobj) {
398399
return Status();
399400
}
400401

402+
static llvm::raw_ostream &PrintSeverity(Stream &stream,
403+
lldb::Severity severity) {
404+
llvm::HighlightColor color;
405+
llvm::StringRef text;
406+
switch (severity) {
407+
case eSeverityError:
408+
color = llvm::HighlightColor::Error;
409+
text = "error: ";
410+
break;
411+
case eSeverityWarning:
412+
color = llvm::HighlightColor::Warning;
413+
text = "warning: ";
414+
break;
415+
case eSeverityInfo:
416+
color = llvm::HighlightColor::Remark;
417+
text = "note: ";
418+
break;
419+
}
420+
return llvm::WithColor(stream.AsRawOstream(), color, llvm::ColorMode::Enable)
421+
<< text;
422+
}
423+
424+
namespace lldb_private {
425+
// Public for unittesting.
426+
void RenderDiagnosticDetails(Stream &stream,
427+
std::optional<uint16_t> offset_in_command,
428+
bool multiline,
429+
llvm::ArrayRef<DiagnosticDetail> details) {
430+
if (details.empty())
431+
return;
432+
433+
if (!offset_in_command) {
434+
for (const DiagnosticDetail &detail : details) {
435+
PrintSeverity(stream, detail.severity);
436+
stream << detail.rendered << '\n';
437+
}
438+
return;
439+
}
440+
441+
// Print a line with caret indicator(s) below the lldb prompt + command.
442+
const size_t padding = *offset_in_command;
443+
stream << std::string(padding, ' ');
444+
445+
size_t offset = 1;
446+
std::vector<DiagnosticDetail> remaining_details, other_details,
447+
hidden_details;
448+
for (const DiagnosticDetail &detail : details) {
449+
if (multiline || !detail.source_location) {
450+
other_details.push_back(detail);
451+
continue;
452+
}
453+
if (detail.source_location->hidden) {
454+
hidden_details.push_back(detail);
455+
continue;
456+
}
457+
if (!detail.source_location->in_user_input) {
458+
other_details.push_back(detail);
459+
continue;
460+
}
461+
462+
auto &loc = *detail.source_location;
463+
remaining_details.push_back(detail);
464+
if (offset > loc.column)
465+
continue;
466+
stream << std::string(loc.column - offset, ' ') << '^';
467+
if (loc.length > 1)
468+
stream << std::string(loc.length - 1, '~');
469+
offset = loc.column + 1;
470+
}
471+
stream << '\n';
472+
473+
// Work through each detail in reverse order using the vector/stack.
474+
bool did_print = false;
475+
for (auto detail = remaining_details.rbegin();
476+
detail != remaining_details.rend();
477+
++detail, remaining_details.pop_back()) {
478+
// Get the information to print this detail and remove it from the stack.
479+
// Print all the lines for all the other messages first.
480+
stream << std::string(padding, ' ');
481+
size_t offset = 1;
482+
for (auto &remaining_detail :
483+
llvm::ArrayRef(remaining_details).drop_back(1)) {
484+
uint16_t column = remaining_detail.source_location->column;
485+
stream << std::string(column - offset, ' ') << "";
486+
offset = column + 1;
487+
}
488+
489+
// Print the line connecting the ^ with the error message.
490+
uint16_t column = detail->source_location->column;
491+
if (offset <= column)
492+
stream << std::string(column - offset, ' ') << "╰─ ";
493+
494+
// Print a colorized string based on the message's severity type.
495+
PrintSeverity(stream, detail->severity);
496+
497+
// Finally, print the message and start a new line.
498+
stream << detail->message << '\n';
499+
did_print = true;
500+
}
501+
502+
// Print the non-located details.
503+
for (const DiagnosticDetail &detail : other_details) {
504+
PrintSeverity(stream, detail.severity);
505+
stream << detail.rendered << '\n';
506+
did_print = true;
507+
}
508+
509+
// Print the hidden details as a last resort.
510+
if (!did_print)
511+
for (const DiagnosticDetail &detail : hidden_details) {
512+
PrintSeverity(stream, detail.severity);
513+
stream << detail.rendered << '\n';
514+
}
515+
}
516+
} // namespace lldb_private
517+
401518
bool CommandObjectExpression::EvaluateExpression(llvm::StringRef expr,
402519
Stream &output_stream,
403520
Stream &error_stream,
@@ -486,19 +603,37 @@ bool CommandObjectExpression::EvaluateExpression(llvm::StringRef expr,
486603

487604
result.SetStatus(eReturnStatusSuccessFinishResult);
488605
} else {
489-
const char *error_cstr = result_valobj_sp->GetError().AsCString();
490-
if (error_cstr && error_cstr[0]) {
491-
const size_t error_cstr_len = strlen(error_cstr);
492-
const bool ends_with_newline = error_cstr[error_cstr_len - 1] == '\n';
493-
if (strstr(error_cstr, "error:") != error_cstr)
494-
error_stream.PutCString("error: ");
495-
error_stream.Write(error_cstr, error_cstr_len);
496-
if (!ends_with_newline)
497-
error_stream.EOL();
606+
// Retrieve the diagnostics.
607+
std::vector<DiagnosticDetail> details;
608+
llvm::consumeError(
609+
llvm::handleErrors(result_valobj_sp->GetError().ToError(),
610+
[&](DetailedExpressionError &error) {
611+
details.push_back(error.GetDetail());
612+
}));
613+
// Find the position of the expression in the command.
614+
std::optional<uint16_t> expr_pos;
615+
size_t nchar = m_original_command.find(expr);
616+
if (nchar != std::string::npos)
617+
expr_pos = nchar + GetDebugger().GetPrompt().size();
618+
619+
if (!details.empty()) {
620+
bool multiline = expr.contains('\n');
621+
RenderDiagnosticDetails(error_stream, expr_pos, multiline, details);
498622
} else {
499-
error_stream.PutCString("error: unknown error\n");
623+
const char *error_cstr = result_valobj_sp->GetError().AsCString();
624+
if (error_cstr && error_cstr[0]) {
625+
const size_t error_cstr_len = strlen(error_cstr);
626+
const bool ends_with_newline =
627+
error_cstr[error_cstr_len - 1] == '\n';
628+
if (strstr(error_cstr, "error:") != error_cstr)
629+
error_stream.PutCString("error: ");
630+
error_stream.Write(error_cstr, error_cstr_len);
631+
if (!ends_with_newline)
632+
error_stream.EOL();
633+
} else {
634+
error_stream.PutCString("error: unknown error\n");
635+
}
500636
}
501-
502637
result.SetStatus(eReturnStatusFailed);
503638
}
504639
}

lldb/source/Interpreter/CommandInterpreter.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1887,7 +1887,8 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
18871887
CommandReturnObject &result,
18881888
bool force_repeat_command) {
18891889
std::string command_string(command_line);
1890-
std::string original_command_string(command_line);
1890+
std::string original_command_string(command_string);
1891+
std::string real_original_command_string(command_string);
18911892

18921893
Log *log = GetLog(LLDBLog::Commands);
18931894
llvm::PrettyStackTraceFormat stack_trace("HandleCommand(command = \"%s\")",
@@ -2076,6 +2077,7 @@ bool CommandInterpreter::HandleCommand(const char *command_line,
20762077
}
20772078

20782079
ElapsedTime elapsed(execute_time);
2080+
cmd_obj->SetOriginalCommandString(real_original_command_string);
20792081
cmd_obj->Execute(remainder.c_str(), result);
20802082
}
20812083

lldb/source/Plugins/ExpressionParser/Clang/ClangExpressionParser.cpp

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,6 @@ class ClangDiagnosticManagerAdapter : public clang::DiagnosticConsumer {
215215
m_os->flush();
216216

217217
DiagnosticDetail detail;
218-
bool make_new_diagnostic = true;
219-
220218
switch (DiagLevel) {
221219
case DiagnosticsEngine::Level::Fatal:
222220
case DiagnosticsEngine::Level::Error:
@@ -230,9 +228,6 @@ class ClangDiagnosticManagerAdapter : public clang::DiagnosticConsumer {
230228
detail.severity = lldb::eSeverityInfo;
231229
break;
232230
case DiagnosticsEngine::Level::Note:
233-
m_manager->AppendMessageToDiagnostic(m_output);
234-
make_new_diagnostic = false;
235-
236231
// 'note:' diagnostics for errors and warnings can also contain Fix-Its.
237232
// We add these Fix-Its to the last error diagnostic to make sure
238233
// that we later have all Fix-Its related to an 'error' diagnostic when
@@ -250,7 +245,6 @@ class ClangDiagnosticManagerAdapter : public clang::DiagnosticConsumer {
250245
AddAllFixIts(clang_diag, Info);
251246
break;
252247
}
253-
if (make_new_diagnostic) {
254248
// ClangDiagnostic messages are expected to have no whitespace/newlines
255249
// around them.
256250
std::string stripped_output =
@@ -270,6 +264,7 @@ class ClangDiagnosticManagerAdapter : public clang::DiagnosticConsumer {
270264
loc.line = fsloc.getSpellingLineNumber();
271265
loc.column = fsloc.getSpellingColumnNumber();
272266
loc.in_user_input = filename == m_filename;
267+
loc.hidden = filename.starts_with("<lldb wrapper ");
273268

274269
// Find the range of the primary location.
275270
for (const auto &range : Info.getRanges()) {
@@ -299,7 +294,6 @@ class ClangDiagnosticManagerAdapter : public clang::DiagnosticConsumer {
299294
AddAllFixIts(new_diagnostic.get(), Info);
300295

301296
m_manager->AddDiagnostic(std::move(new_diagnostic));
302-
}
303297
}
304298

305299
void BeginSourceFile(const LangOptions &LO, const Preprocessor *PP) override {

lldb/source/Plugins/TraceExporter/ctf/CommandObjectThreadTraceExportCTF.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class CommandObjectThreadTraceExportCTF : public CommandObjectParsed {
4848
Options *GetOptions() override { return &m_options; }
4949

5050
protected:
51-
void DoExecute(Args &command, CommandReturnObject &result) override;
51+
void DoExecute(Args &args, CommandReturnObject &result) override;
5252

5353
CommandOptions m_options;
5454
};

lldb/source/Utility/Status.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "lldb/Utility/Status.h"
1010

11+
#include "lldb/Expression/DiagnosticManager.h"
1112
#include "lldb/Utility/LLDBLog.h"
1213
#include "lldb/Utility/Log.h"
1314
#include "lldb/Utility/VASPrintf.h"
@@ -279,8 +280,6 @@ ErrorType Status::GetType() const {
279280
else if (error.convertToErrorCode().category() == lldb_generic_category() ||
280281
error.convertToErrorCode() == llvm::inconvertibleErrorCode())
281282
result = eErrorTypeGeneric;
282-
else
283-
result = eErrorTypeInvalid;
284283
});
285284
return result;
286285
}

lldb/test/API/commands/expression/diagnostics/TestExprDiagnostics.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,53 @@ def test_source_locations_from_objc_modules(self):
183183
# The NSLog definition source line should be printed. Return value and
184184
# the first argument are probably stable enough that this test can check for them.
185185
self.assertIn("void NSLog(NSString *format", value.GetError().GetCString())
186+
187+
def test_command_expr_formatting(self):
188+
"""Test that the source and caret positions LLDB prints are correct"""
189+
self.build()
190+
191+
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
192+
self, "// Break here", self.main_source_spec
193+
)
194+
frame = thread.GetFrameAtIndex(0)
195+
196+
def check(input_ref):
197+
self.expect(input_ref[0], error=True, substrs=input_ref[1:])
198+
199+
check(
200+
[
201+
"expression -- a+b",
202+
" ^ ^",
203+
" │ ╰─ error: use of undeclared identifier 'b'",
204+
" ╰─ error: use of undeclared identifier 'a'",
205+
]
206+
)
207+
208+
check(
209+
[
210+
"expr -- a",
211+
" ^",
212+
" ╰─ error: use of undeclared identifier 'a'",
213+
]
214+
)
215+
check(
216+
[
217+
"expr -i 0 -o 0 -- a",
218+
" ^",
219+
" ╰─ error: use of undeclared identifier 'a'",
220+
]
221+
)
222+
223+
self.expect(
224+
"expression --top-level -- template<typename T> T FOO(T x) { return x/2;}"
225+
)
226+
check(
227+
[
228+
'expression -- FOO("")',
229+
" ^",
230+
" ╰─ note: in instantiation of function template specialization 'FOO<const char *>' requested here",
231+
"error: <user expression",
232+
"invalid operands to binary expression",
233+
]
234+
)
235+
check(["expression --\na\n+\nb", "error: <user", "a", "error: <user", "b"])

lldb/test/API/commands/expression/persistent_variables/TestPersistentVariables.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def test_persistent_variables(self):
5656
self.expect(
5757
"expr int $i = 123",
5858
error=True,
59-
substrs=["error: redefinition of persistent variable '$i'"],
59+
substrs=["redefinition of persistent variable '$i'"],
6060
)
6161
self.expect_expr("$i", result_type="int", result_value="5")
6262

@@ -65,7 +65,7 @@ def test_persistent_variables(self):
6565
self.expect(
6666
"expr long $i = 123",
6767
error=True,
68-
substrs=["error: redefinition of persistent variable '$i'"],
68+
substrs=["redefinition of persistent variable '$i'"],
6969
)
7070
self.expect_expr("$i", result_type="int", result_value="5")
7171

lldb/test/API/commands/expression/static-initializers/TestStaticInitializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_failing_init(self):
3535
self.expect(
3636
"expr -p -- struct Foo2 { Foo2() { do_abort(); } }; Foo2 f;",
3737
error=True,
38-
substrs=["error: couldn't run static initializer:"],
38+
substrs=["couldn't run static initializer:"],
3939
)
4040

4141
def test_without_process(self):

lldb/test/API/lang/cpp/template-function/TestTemplateFunctions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ def do_test_template_function(self, add_cast):
2121
"expr b1 <=> b2",
2222
error=True,
2323
substrs=[
24-
"warning: <user expression 0>:1:4: '<=>' is a single token in C++20; add a space to avoid a change in behavior"
24+
"warning:",
25+
"'<=>' is a single token in C++20; add a space to avoid a change in behavior",
2526
],
2627
)
2728

lldb/test/API/lang/mixed/TestMixedLanguages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def cleanup():
4040
self.runCmd("run")
4141
self.expect("thread backtrace", substrs=["`main", "lang=c"])
4242
# Make sure evaluation of C++11 fails.
43-
self.expect("expr foo != nullptr", error=True, startstr="error")
43+
self.expect("expr foo != nullptr", error=True, substrs=["error"])
4444

4545
# Run to BP at foo (in foo.cpp) and test that the language is C++.
4646
self.runCmd("breakpoint set -n foo")

0 commit comments

Comments
 (0)