[lldb] Add evaluation modes to DIL (#178747)

Adding more supported operators to DIL breaks tests in `DWIMPrint` and
`lldb-dap`, which shouldn't be simply adjusted for new DIL capabilities.
They act as a check for the boundaries of what subset of expressions
`DWIMPrint` and `lldb-dap` expect to be evaluated when using
`GetValueForVariableExpressionPath` function. With this patch, the
caller can now pick a mode that limits the expressions DIL can evaluate,
which ensures the expected preexisting behavior. More operators can now
be safely added to DIL, which can still be evaluated by DIL when using
`frame var` command or the API call with Full mode selected (or not
specified at all).

DIL will only attempt evaluating expressions that contain operations
allowed by a selected mode:
 - Simple: identifiers, operators: '.'
 - Legacy: identifiers, integers, operators: '.', '->', '*', '&', '[]'
 - Full: everything supported by DIL
This commit is contained in:
Ilia Kuklin 2026-02-13 18:34:58 +05:00 committed by GitHub
parent b447f5d976
commit 80fffd527c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 145 additions and 35 deletions

View File

@ -182,12 +182,16 @@ public:
// expression result and is not a constant object like
// SBFrame::EvaluateExpression(...) returns, but a child object of the
// variable value.
lldb::SBValue GetValueForVariablePath(const char *var_expr_cstr,
DynamicValueType use_dynamic);
lldb::SBValue
GetValueForVariablePath(const char *var_expr_cstr,
DynamicValueType use_dynamic,
lldb::DILMode mode = lldb::eDILModeFull);
/// The version that doesn't supply a 'use_dynamic' value will use the
/// target's default.
lldb::SBValue GetValueForVariablePath(const char *var_path);
lldb::SBValue
GetValueForVariablePath(const char *var_path,
lldb::DILMode mode = lldb::eDILModeFull);
/// Find variables, register sets, registers, or persistent variables using
/// the frame as the scope.

View File

@ -86,7 +86,8 @@ public:
lldb::ValueObjectSP GetValueForVariableExpressionPath(
llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
uint32_t options, lldb::VariableSP &var_sp, Status &error) override;
uint32_t options, lldb::VariableSP &var_sp, Status &error,
lldb::DILMode mode = lldb::eDILModeFull) override;
bool HasDebugInformation() override;

View File

@ -316,11 +316,16 @@ public:
/// \param[in] error
/// Record any errors encountered while evaluating var_expr.
///
/// \param[in] mode
/// Data Inspection Language (DIL) evaluation mode.
/// \see lldb::DILMode
///
/// \return
/// A shared pointer to the ValueObject described by var_expr.
virtual lldb::ValueObjectSP GetValueForVariableExpressionPath(
llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
uint32_t options, lldb::VariableSP &var_sp, Status &error);
uint32_t options, lldb::VariableSP &var_sp, Status &error,
lldb::DILMode mode = lldb::eDILModeFull);
/// Determine whether this StackFrame has debug information available or not.
///
@ -615,7 +620,8 @@ private:
lldb::ValueObjectSP DILGetValueForVariableExpressionPath(
llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
uint32_t options, lldb::VariableSP &var_sp, Status &error);
uint32_t options, lldb::VariableSP &var_sp, Status &error,
lldb::DILMode mode = lldb::eDILModeFull);
StackFrame(const StackFrame &) = delete;
const StackFrame &operator=(const StackFrame &) = delete;

View File

@ -9,6 +9,7 @@
#ifndef LLDB_VALUEOBJECT_DILLEXER_H
#define LLDB_VALUEOBJECT_DILLEXER_H
#include "lldb/lldb-enumerations.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FormatVariadic.h"
@ -74,7 +75,8 @@ class DILLexer {
public:
/// Lexes all the tokens in expr and calls the private constructor
/// with the lexed tokens.
static llvm::Expected<DILLexer> Create(llvm::StringRef expr);
static llvm::Expected<DILLexer>
Create(llvm::StringRef expr, lldb::DILMode mode = lldb::eDILModeFull);
/// Return the current token to be handled by the DIL parser.
const Token &GetCurrentToken() { return m_lexed_tokens[m_tokens_idx]; }

View File

@ -1420,6 +1420,19 @@ enum NameMatchStyle {
eNameMatchStyleRegex = eFunctionNameTypeSelector << 1
};
/// Data Inspection Language (DIL) evaluation modes.
/// DIL will only attempt evaluating expressions that contain tokens
/// allowed by a selected mode.
enum DILMode {
/// Allowed: identifiers, operators: '.'.
eDILModeSimple,
/// Allowed: identifiers, integers, operators: '.', '->', '*', '&', '[]'.
eDILModeLegacy,
/// Allowed: everything supported by DIL.
/// \see lldb/docs/dil-expr-lang.ebnf
eDILModeFull
};
} // namespace lldb
#endif // LLDB_LLDB_ENUMERATIONS_H

View File

@ -363,7 +363,8 @@ void SBFrame::Clear() {
m_opaque_sp->Clear();
}
lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path) {
lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path,
lldb::DILMode mode) {
LLDB_INSTRUMENT_VA(this, var_path);
SBValue sb_value;
@ -377,13 +378,14 @@ lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path) {
if (StackFrame *frame = exe_ctx->GetFramePtr()) {
lldb::DynamicValueType use_dynamic =
frame->CalculateTarget()->GetPreferDynamicValue();
sb_value = GetValueForVariablePath(var_path, use_dynamic);
sb_value = GetValueForVariablePath(var_path, use_dynamic, mode);
}
return sb_value;
}
lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path,
DynamicValueType use_dynamic) {
DynamicValueType use_dynamic,
lldb::DILMode mode) {
LLDB_INSTRUMENT_VA(this, var_path, use_dynamic);
SBValue sb_value;
@ -405,7 +407,7 @@ lldb::SBValue SBFrame::GetValueForVariablePath(const char *var_path,
var_path, eNoDynamicValues,
StackFrame::eExpressionPathOptionCheckPtrVsMember |
StackFrame::eExpressionPathOptionsAllowDirectIVarAccess,
var_sp, error));
var_sp, error, mode));
sb_value.SetSP(value_sp, use_dynamic);
}
return sb_value;

View File

@ -170,8 +170,8 @@ void CommandObjectDWIMPrint::DoExecute(StringRef command,
Status status;
auto valobj_sp = frame->GetValueForVariableExpressionPath(
expr, eval_options.GetUseDynamic(),
StackFrame::eExpressionPathOptionsAllowDirectIVarAccess, var_sp,
status);
StackFrame::eExpressionPathOptionsAllowDirectIVarAccess, var_sp, status,
lldb::eDILModeSimple);
if (valobj_sp && status.Success() && valobj_sp->GetError().Success()) {
if (!suppress_result) {
if (auto persisted_valobj = valobj_sp->Persist())

View File

@ -316,7 +316,8 @@ lldb::ValueObjectSP ScriptedFrame::GetValueObjectForFrameVariable(
lldb::ValueObjectSP ScriptedFrame::GetValueForVariableExpressionPath(
llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
uint32_t options, lldb::VariableSP &var_sp, Status &error) {
uint32_t options, lldb::VariableSP &var_sp, Status &error,
lldb::DILMode mode) {
// Unless the frame implementation knows how to create variables (which it
// doesn't), we can't construct anything for the variable. This may seem
// somewhat out of place, but it's basically because of how this API is used -

View File

@ -76,7 +76,8 @@ public:
lldb::ValueObjectSP GetValueForVariableExpressionPath(
llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
uint32_t options, lldb::VariableSP &var_sp, Status &error) override;
uint32_t options, lldb::VariableSP &var_sp, Status &error,
lldb::DILMode mode = lldb::eDILModeFull) override;
bool isA(const void *ClassID) const override {
return ClassID == &ID || StackFrame::isA(ClassID);

View File

@ -99,9 +99,9 @@ BorrowedStackFrame::GetInScopeVariableList(bool get_file_globals,
ValueObjectSP BorrowedStackFrame::GetValueForVariableExpressionPath(
llvm::StringRef var_expr, DynamicValueType use_dynamic, uint32_t options,
VariableSP &var_sp, Status &error) {
VariableSP &var_sp, Status &error, lldb::DILMode mode) {
return m_borrowed_frame_sp->GetValueForVariableExpressionPath(
var_expr, use_dynamic, options, var_sp, error);
var_expr, use_dynamic, options, var_sp, error, mode);
}
bool BorrowedStackFrame::HasDebugInformation() {

View File

@ -524,13 +524,13 @@ StackFrame::GetInScopeVariableList(bool get_file_globals,
ValueObjectSP StackFrame::GetValueForVariableExpressionPath(
llvm::StringRef var_expr, DynamicValueType use_dynamic, uint32_t options,
VariableSP &var_sp, Status &error) {
VariableSP &var_sp, Status &error, lldb::DILMode mode) {
ExecutionContext exe_ctx;
CalculateExecutionContext(exe_ctx);
bool use_DIL = exe_ctx.GetTargetRef().GetUseDIL(&exe_ctx);
if (use_DIL)
return DILGetValueForVariableExpressionPath(var_expr, use_dynamic, options,
var_sp, error);
var_sp, error, mode);
return LegacyGetValueForVariableExpressionPath(var_expr, use_dynamic, options,
var_sp, error);
@ -538,7 +538,8 @@ ValueObjectSP StackFrame::GetValueForVariableExpressionPath(
ValueObjectSP StackFrame::DILGetValueForVariableExpressionPath(
llvm::StringRef var_expr, lldb::DynamicValueType use_dynamic,
uint32_t options, lldb::VariableSP &var_sp, Status &error) {
uint32_t options, lldb::VariableSP &var_sp, Status &error,
lldb::DILMode mode) {
const bool check_ptr_vs_member =
(options & eExpressionPathOptionCheckPtrVsMember) != 0;
@ -548,7 +549,7 @@ ValueObjectSP StackFrame::DILGetValueForVariableExpressionPath(
(options & eExpressionPathOptionsNoSyntheticChildren) != 0;
// Lex the expression.
auto lex_or_err = dil::DILLexer::Create(var_expr);
auto lex_or_err = dil::DILLexer::Create(var_expr, mode);
if (!lex_or_err) {
error = Status::FromError(lex_or_err.takeError());
return ValueObjectConstResult::Create(nullptr, std::move(error));

View File

@ -111,12 +111,41 @@ static std::optional<llvm::StringRef> IsNumber(llvm::StringRef &remainder,
return std::nullopt;
}
llvm::Expected<DILLexer> DILLexer::Create(llvm::StringRef expr) {
static llvm::Error IsNotAllowedByMode(llvm::StringRef expr, Token token,
lldb::DILMode mode) {
switch (mode) {
case lldb::eDILModeSimple:
if (!token.IsOneOf({Token::identifier, Token::period, Token::eof})) {
return llvm::make_error<DILDiagnosticError>(
expr, llvm::formatv("{0} is not allowed in DIL simple mode", token),
token.GetLocation());
}
break;
case lldb::eDILModeLegacy:
if (!token.IsOneOf({Token::identifier, Token::integer_constant,
Token::period, Token::arrow, Token::star, Token::amp,
Token::l_square, Token::r_square, Token::eof})) {
return llvm::make_error<DILDiagnosticError>(
expr, llvm::formatv("{0} is not allowed in DIL legacy mode", token),
token.GetLocation());
}
break;
case lldb::eDILModeFull:
break;
}
return llvm::Error::success();
}
llvm::Expected<DILLexer> DILLexer::Create(llvm::StringRef expr,
lldb::DILMode mode) {
std::vector<Token> tokens;
llvm::StringRef remainder = expr;
do {
if (llvm::Expected<Token> t = Lex(expr, remainder)) {
tokens.push_back(std::move(*t));
Token token = *t;
if (llvm::Error error = IsNotAllowedByMode(expr, token, mode))
return error;
tokens.push_back(std::move(token));
} else {
return t.takeError();
}

View File

@ -21,7 +21,7 @@ class TestFrameVarDILGlobalVariableLookup(TestBase):
def test_frame_var(self):
self.build()
lldbutil.run_to_source_breakpoint(
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "Set a breakpoint here", lldb.SBFileSpec("main.cpp")
)
@ -36,3 +36,10 @@ class TestFrameVarDILGlobalVariableLookup(TestBase):
self.expect_var_path("&globalVar", True, type="int *")
self.expect_var_path("&s_str", True, type="const char **")
self.expect_var_path("&argc", True, type="int *")
# Check that '&' is not allowed in simple mode, but allowed in legacy mode
frame = thread.GetFrameAtIndex(0)
simple = frame.GetValueForVariablePath("&x", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("&x", lldb.eDILModeLegacy)
self.assertFailure(simple.GetError())
self.assertSuccess(legacy.GetError())

View File

@ -13,7 +13,7 @@ class TestFrameVarDILArraySubscript(TestBase):
def test_subscript(self):
self.build()
lldbutil.run_to_source_breakpoint(
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "Set a breakpoint here", lldb.SBFileSpec("main.cpp")
)
@ -87,6 +87,13 @@ class TestFrameVarDILArraySubscript(TestBase):
substrs=["subscript of pointer to incomplete type 'void'"],
)
# Check that subscription is not allowed in simple mode, but allowed in legacy mode
frame = thread.GetFrameAtIndex(0)
simple = frame.GetValueForVariablePath("int_arr[0]", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("int_arr[0]", lldb.eDILModeLegacy)
self.assertFailure(simple.GetError())
self.assertSuccess(legacy.GetError())
def test_subscript_synthetic(self):
self.build()
lldbutil.run_to_source_breakpoint(

View File

@ -16,7 +16,7 @@ class TestFrameVarDILLocalVars(TestBase):
def test_frame_var(self):
self.build()
lldbutil.run_to_source_breakpoint(
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "Set a breakpoint here", lldb.SBFileSpec("main.cpp")
)
@ -25,3 +25,10 @@ class TestFrameVarDILLocalVars(TestBase):
self.expect_var_path("b", value="2")
self.expect_var_path("c", value="'\\xfd'")
self.expect_var_path("s", value="4")
# Check that identifiers are allowed in both simple and legacy modes
frame = thread.GetFrameAtIndex(0)
simple = frame.GetValueForVariablePath("a", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("a", lldb.eDILModeLegacy)
self.assertSuccess(simple.GetError())
self.assertSuccess(legacy.GetError())

View File

@ -11,6 +11,7 @@ import os
import shutil
import time
class TestFrameVarDILMemberOf(TestBase):
# If your test case doesn't stress debug info, then
# set this to true. That way it won't be run once for
@ -19,11 +20,11 @@ class TestFrameVarDILMemberOf(TestBase):
def test_frame_var(self):
self.build()
lldbutil.run_to_source_breakpoint(self, "Set a breakpoint here",
lldb.SBFileSpec("main.cpp"))
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "Set a breakpoint here", lldb.SBFileSpec("main.cpp")
)
self.expect("settings set target.experimental.use-DIL true",
substrs=[""])
self.expect("settings set target.experimental.use-DIL true", substrs=[""])
self.expect_var_path("s.x", value="1")
self.expect_var_path("s.r", type="int &")
self.expect_var_path("sr.x", value="1")
@ -48,3 +49,16 @@ class TestFrameVarDILMemberOf(TestBase):
# Test for record typedefs.
self.expect_var_path("sa.x", value="3")
self.expect_var_path("sa.y", value="'\\x04'")
# Check that '.' is allowed in both simple and legacy modes
frame = thread.GetFrameAtIndex(0)
simple = frame.GetValueForVariablePath("s.x", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("s.x", lldb.eDILModeLegacy)
self.assertSuccess(simple.GetError())
self.assertSuccess(legacy.GetError())
# Check that '->' is not allowed in simple mode, but allowed in legacy mode
simple = frame.GetValueForVariablePath("sp->x", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("sp->x", lldb.eDILModeLegacy)
self.assertFailure(simple.GetError())
self.assertSuccess(legacy.GetError())

View File

@ -17,7 +17,7 @@ class TestFrameVarDILPointerDereference(TestBase):
def test_frame_var(self):
self.build()
lldbutil.run_to_source_breakpoint(
(target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
self, "Set a breakpoint here", lldb.SBFileSpec("main.cpp")
)
@ -46,3 +46,10 @@ class TestFrameVarDILPointerDereference(TestBase):
pp_int0_2stars_got.GetValueAsAddress(),
pp_int0_2stars_exp.GetValueAsAddress(),
)
# Check that * is not allowed in simple mode, but allowed in legacy mode
frame = thread.GetFrameAtIndex(0)
simple = frame.GetValueForVariablePath("*p_int0", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("*p_int0", lldb.eDILModeLegacy)
self.assertFailure(simple.GetError())
self.assertSuccess(legacy.GetError())

View File

@ -285,3 +285,10 @@ class TestFrameVarDILCast(TestBase):
error=True,
substrs=["expected 'eof', got: <'InnerFoo' (identifier)>"],
)
# Check that casts are not allowed in both simple and legacy modes
frame = thread.GetFrameAtIndex(0)
simple = frame.GetValueForVariablePath("(char)a", lldb.eDILModeSimple)
legacy = frame.GetValueForVariablePath("(char)a", lldb.eDILModeLegacy)
self.assertFailure(simple.GetError())
self.assertFailure(legacy.GetError())

View File

@ -253,6 +253,7 @@ class TestDAP_evaluate(lldbdap_testcase.DAPTestCaseBase):
self.assertEvaluateFailure("a_function(1)")
self.assertEvaluateFailure("var2 + struct1.foo")
self.assertEvaluateFailure("foo_func")
self.assertEvaluateFailure("(float) var2")
self.assertEvaluate("foo_var", "44")
# Expressions at breakpoint 2, which is an anonymous block

View File

@ -54,8 +54,8 @@ static lldb::SBValue EvaluateVariableExpression(lldb::SBTarget &target,
// Check if it is a variable or an expression path for a variable. i.e.
// 'foo->bar' finds the 'bar' variable. It is more reliable than the
// expression parser in many cases and it is faster.
value = frame.GetValueForVariablePath(expression_cstr,
lldb::eDynamicDontRunTarget);
value = frame.GetValueForVariablePath(
expression_cstr, lldb::eDynamicDontRunTarget, lldb::eDILModeLegacy);
if (value || !run_as_expression)
return value;

View File

@ -397,8 +397,8 @@ bool SourceBreakpoint::BreakpointHitCallback(
// evaluation
const std::string &expr_str = messagePart.text;
const char *expr = expr_str.c_str();
lldb::SBValue value =
frame.GetValueForVariablePath(expr, lldb::eDynamicDontRunTarget);
lldb::SBValue value = frame.GetValueForVariablePath(
expr, lldb::eDynamicDontRunTarget, lldb::eDILModeLegacy);
if (value.GetError().Fail())
value = frame.EvaluateExpression(expr);
output += VariableDescription(