[Lldb-commits] [lldb] [lldb-dap] Fix global-scope evaluate always routing to command interpreter (PR #225243)
Nathan Fusselman via lldb-commits
lldb-commits at lists.llvm.org
Tue Sep 22 07:46:32 PDT 2026
https://github.com/nathanfusselman updated https://github.com/llvm/llvm-project/pull/225243
>From d31ded40aceab92008cee10951bdf42b30290aaf Mon Sep 17 00:00:00 2001
From: Nathan Fusselman <nfusselman at apple.com>
Date: Mon, 21 Sep 2026 17:26:36 -0700
Subject: [PATCH] [lldb-dap] Fix global-scope evaluate always routing to
command interpreter
DetectReplMode returned Command unconditionally when there was no frame,
since it couldn't check FindVariable without one. A frameless evaluate
(no frameId, e.g. `(int)getpid()`) was always treated as an unknown
command instead of an expression.
Fall back to SBTarget::FindFirstGlobalVariable when there's no frame:
DAP::target is valid independent of frame, so globals/statics can still
be checked without one.
rdar://187884135
---
.../lldb-dap/evaluate/TestDAP_evaluate.py | 42 ++++++++++++++++---
.../test/API/tools/lldb-dap/evaluate/main.cpp | 4 ++
lldb/tools/lldb-dap/DAP.cpp | 11 ++---
3 files changed, 47 insertions(+), 10 deletions(-)
diff --git a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
index 2ace6eeccc228..639b32a81d519 100644
--- a/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
+++ b/lldb/test/API/tools/lldb-dap/evaluate/TestDAP_evaluate.py
@@ -54,6 +54,11 @@ def assert_eval_fails(expression: str):
f"expected {expression!r} to fail using {context=!r} in {frame_id=!r}"
)
+ def assert_eval_fails_frameless(expression: str):
+ session.do_evaluate(expression, frameId=None, context=context).error(
+ f"expected {expression!r} to fail using {context=!r} with no frame"
+ )
+
source = "main.cpp"
program = self.getBuildArtifact("a.out")
breakpoint_lines = [
@@ -225,14 +230,41 @@ def assert_eval_fails(expression: str):
session.verify_evaluate(eval_body, matches="20")
if context_parses_expressions:
- # Access global variable without a frame
- # Run in variable mode to avoid interpreting it as a command.
- session.evaluate("`lldb-dap repl-mode variable", context="repl")
-
+ # Access a global variable with no frame at all (frameId omitted).
eval_body = session.evaluate("static_int", context=context)
session.verify_evaluate(eval_body, matches="42", type="int")
- session.evaluate("`lldb-dap repl-mode auto", context="repl")
+ assert_eval_fails_frameless("var1") # local, not global.
+ assert_eval_fails_frameless("totally_bogus_expr_xyz")
+
+ if context in ("repl", None):
+ # `help` shares its name with a real LLDB command (see main.cpp).
+ # With no frame, the global should win and emit an ambiguity warning.
+ event_before_help_eval = session.last_event()
+ eval_body = session.evaluate("help", context=context)
+ session.verify_evaluate(eval_body, matches="99", type="int")
+ session.collect_console(
+ until="Expression 'help' is both an LLDB command and variable",
+ after=event_before_help_eval,
+ )
+
+ # A valid frame should add local lookup without preventing the same
+ # global from taking precedence over the LLDB command.
+ eval_body = a_frame.evaluate("help", context=context)
+ session.verify_evaluate(eval_body, matches="99", type="int")
+
+ # Use a uniquely named alias with deterministic output to verify that a
+ # command with no matching variable is still run as an LLDB command.
+ session.evaluate(
+ "command alias lldb_dap_test_command expression -- 12345",
+ context=context,
+ )
+ eval_body = session.evaluate(
+ "lldb_dap_test_command", context=context
+ )
+ session.verify_evaluate(
+ eval_body, matches="12345", has_mem_ref=False
+ )
# In a_function's own frame these names are out of scope.
assert_eval_fails("var1")
diff --git a/lldb/test/API/tools/lldb-dap/evaluate/main.cpp b/lldb/test/API/tools/lldb-dap/evaluate/main.cpp
index 112726677637c..97ed7bac90750 100644
--- a/lldb/test/API/tools/lldb-dap/evaluate/main.cpp
+++ b/lldb/test/API/tools/lldb-dap/evaluate/main.cpp
@@ -8,6 +8,10 @@ static int static_int = 42;
int non_static_int = 43;
+// Deliberately shares its name with the `help` LLDB command, to test
+// command/variable ambiguity handling for a global variable.
+int help = 99;
+
int a_function(int list) {
return list; // breakpoint 3
}
diff --git a/lldb/tools/lldb-dap/DAP.cpp b/lldb/tools/lldb-dap/DAP.cpp
index 7e59541e6c834..f9f9e3d7c14f2 100644
--- a/lldb/tools/lldb-dap/DAP.cpp
+++ b/lldb/tools/lldb-dap/DAP.cpp
@@ -592,10 +592,6 @@ ReplMode DAP::DetectReplMode(lldb::SBFrame &frame, std::string &expression,
if (repl_mode != ReplMode::Auto)
return repl_mode;
- // We cannot check if expression is a variable without a frame.
- if (!frame)
- return ReplMode::Command;
-
// To determine if the expression is a command or not, check if the first
// term is a variable or command. If it's a variable in scope we will prefer
// that behavior and give a warning to the user if they meant to invoke the
@@ -618,7 +614,12 @@ ReplMode DAP::DetectReplMode(lldb::SBFrame &frame, std::string &expression,
const bool is_command = interpreter.CommandExists(first_cstr) ||
interpreter.UserCommandExists(first_cstr) ||
interpreter.AliasExists(first_cstr);
- const bool is_variable = frame.FindVariable(first_cstr).IsValid();
+ // Check both variables visible in the current frame and globals/statics.
+ // A valid frame should not prevent a global from taking precedence over an
+ // LLDB command with the same name.
+ const bool is_variable =
+ (frame && frame.FindVariable(first_cstr).IsValid()) ||
+ target.FindFirstGlobalVariable(first_cstr).IsValid();
// If we have both a variable and command, warn the user about the conflict.
if (!partial_expression && is_command && is_variable) {
More information about the lldb-commits
mailing list