[Lldb-commits] [lldb] [lldb] Report a scripted frame's variables with the frame's value type (PR #221708)
Med Ismail Bennani via lldb-commits
lldb-commits at lists.llvm.org
Mon Sep 7 04:31:01 PDT 2026
https://github.com/medismailben created https://github.com/llvm/llvm-project/pull/221708
A scripted frame's variable exists as two objects: a Variable holding the scope the frame assigned it, and a `ValueObject` holding the value the frame built. `SBValue::GetValueType()` reports the `ValueObject`'s, and `GetValueObjectForFrameVariable` handed the value back unwrapped, so the scope never reached a client. A value built in Python is a `ValueObjectConstResult`, which describes how it was produced rather than which variable it stands for, leaving anything that groups a frame's variables by scope unable to place it. `frame variable` was unaffected because it reads the `Variables` directly.
Pair the two in a small delegating `ValueObject` that takes its value type from the variable and everything else from the value. `ValueObjectVariable` already does this for a variable read from debug info, but cannot be reused: it recovers the value from a DWARF location expression, and a frame that builds its own values supplies none.
Wrapping rather than annotating keeps the correction out of the value, so a value a frame passes through from elsewhere is reported as that frame presents it without changing what the same value reports anywhere else. It is also independent of which kind of `ValueObject` the value is, unlike a value type override on `ValueObjectConstResult`, which would stop working as soon as a frame can supply a value that is not a const result.
Reaching a variable by name goes through `Variable::NameMatches`, which dereferences `m_owner_scope` for every entry whose name does not match. A null owner scope is a legal state, `CalculateSymbolContext` handles it explicitly and a frame that makes up its own variables has none, so drop that lookup. The `SymbolContext` it filled in has had no reader since 22b044877d23, which dropped the language argument to `Mangled::NameMatches` along with the only use of it.
Also document that the synthetic flag in `GetValueTypeForVariable`'s return is what exempts a variable from the scope rules a declared variable is subject to. A value with no storage behind it is filtered out of an in-scope-only listing without it.
>From 4f46834a902693d501d2c85e501ca442d8a4443b Mon Sep 17 00:00:00 2001
From: Med Ismail Bennani <ismail at bennani.ma>
Date: Mon, 7 Sep 2026 12:21:06 +0100
Subject: [PATCH] [lldb] Report a scripted frame's variables with the frame's
value type
A scripted frame's variable exists as two objects: a Variable holding the
scope the frame assigned it, and a ValueObject holding the value the frame
built. SBValue::GetValueType() reports the ValueObject's, and
GetValueObjectForFrameVariable handed the value back unwrapped, so the
scope never reached a client. A value built in Python is a
ValueObjectConstResult, which describes how it was produced rather than
which variable it stands for, leaving anything that groups a frame's
variables by scope unable to place it. `frame variable` was unaffected
because it reads the Variables directly.
Pair the two in a small delegating ValueObject that takes its value type
from the variable and everything else from the value. ValueObjectVariable
already does this for a variable read from debug info, but cannot be
reused: it recovers the value from a DWARF location expression, and a
frame that builds its own values supplies none.
Wrapping rather than annotating keeps the correction out of the value, so
a value a frame passes through from elsewhere is reported as that frame
presents it without changing what the same value reports anywhere else.
It is also independent of which kind of ValueObject the value is, unlike
a value type override on ValueObjectConstResult, which would stop working
as soon as a frame can supply a value that is not a const result.
Reaching a variable by name goes through Variable::NameMatches, which
dereferences m_owner_scope for every entry whose name does not match. A
null owner scope is a legal state -- CalculateSymbolContext handles it
explicitly -- and a frame that makes up its own variables has none, so
drop that lookup. The SymbolContext it filled in has had no reader since
22b044877d23, which dropped the language argument to
Mangled::NameMatches along with the only use of it.
Also document that the synthetic flag in GetValueTypeForVariable's return
is what exempts a variable from the scope rules a declared variable is
subject to. A value with no storage behind it is filtered out of an
in-scope-only listing without it.
Signed-off-by: Med Ismail Bennani <ismail at bennani.ma>
---
.../Interfaces/ScriptedFrameInterface.h | 11 ++
.../Process/scripted/ScriptedFrame.cpp | 105 +++++++++++++++++-
lldb/source/Symbol/Variable.cpp | 3 -
.../TestScriptedFrameProvider.py | 17 +++
4 files changed, 131 insertions(+), 5 deletions(-)
diff --git a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
index b2a37bf497504..86be7649cfe4b 100644
--- a/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
+++ b/lldb/include/lldb/Interpreter/Interfaces/ScriptedFrameInterface.h
@@ -52,6 +52,17 @@ class ScriptedFrameInterface : virtual public ScriptedInterface {
virtual lldb::ValueObjectListSP GetVariables() { return nullptr; }
+ /// Choose which kind of variable \a value is presented as, for instance
+ /// \a eValueTypeVariableLocal to have it listed among the frame's locals.
+ ///
+ /// Include \a eValueTypeSyntheticFlag for a value the implementation built
+ /// itself. That flag is what exempts a variable from the scope rules a
+ /// declared variable is subject to, so a value with no storage behind it will
+ /// be filtered out of an in-scope-only listing without it. Leave it off to
+ /// present a variable that really does exist in the frame.
+ ///
+ /// Returning std::nullopt keeps the kind the value already reports, with the
+ /// synthetic flag added.
virtual std::optional<lldb::ValueType>
GetValueTypeForVariable(lldb::ValueObjectSP value) {
return std::nullopt;
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
index cac9f0e44bb73..2ff674e98ea2d 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
@@ -42,6 +42,90 @@
using namespace lldb;
using namespace lldb_private;
+namespace {
+
+/// Presents a value as one of a frame's variables.
+///
+/// A value describes how it was produced, not which of a frame's variables it
+/// stands for; only the variable records that. A frame that builds its own
+/// values pairs the two here, taking the value type from the variable and
+/// everything else from the value.
+///
+/// ValueObjectVariable does the same for a variable read from debug info, but
+/// requires the value to be recoverable from a DWARF location expression.
+class ValueObjectProvidedVariable : public ValueObject {
+public:
+ static lldb::ValueObjectSP Create(ValueObject &parent,
+ const lldb::VariableSP &variable_sp) {
+ return (new ValueObjectProvidedVariable(parent, variable_sp))->GetSP();
+ }
+
+ ~ValueObjectProvidedVariable() override = default;
+
+ lldb::ValueType GetValueType() const override {
+ return m_variable_sp->GetScope();
+ }
+
+ llvm::Expected<uint64_t> GetByteSize() override {
+ return m_parent->GetByteSize();
+ }
+
+ llvm::Expected<uint32_t> CalculateNumChildren(uint32_t max) override {
+ return m_parent->GetNumChildren(max);
+ }
+
+ bool IsInScope() override { return m_parent->IsInScope(); }
+
+ // This wrapper is transparent: consumers that walk parents, such as
+ // expression path construction, must not see an extra level.
+ ValueObject *GetParent() override {
+ return m_parent ? m_parent->GetParent() : nullptr;
+ }
+
+ const ValueObject *GetParent() const override {
+ return m_parent ? m_parent->GetParent() : nullptr;
+ }
+
+protected:
+ ValueObjectProvidedVariable(ValueObject &parent,
+ const lldb::VariableSP &variable_sp)
+ : ValueObject(parent), m_variable_sp(variable_sp) {
+ SetName(parent.GetName());
+ }
+
+ bool UpdateValue() override {
+ SetValueIsValid(false);
+ m_error.Clear();
+
+ if (!m_parent->UpdateValueIfNeeded(false)) {
+ if (m_error.Success() && m_parent->GetError().Fail())
+ m_error = m_parent->GetError().Clone();
+ return false;
+ }
+
+ m_update_point.SetUpdated();
+ m_value = m_parent->GetValue();
+ SetAddressTypeOfChildren(m_parent->GetAddressTypeOfChildren());
+ ExecutionContext exe_ctx(GetExecutionContextRef());
+ m_error = m_value.GetValueAsData(&exe_ctx, m_data, GetModule().get());
+ SetValueDidChange(m_parent->GetValueDidChange());
+ return true;
+ }
+
+ CompilerType GetCompilerTypeImpl() override {
+ return m_parent->GetCompilerType();
+ }
+
+ lldb::VariableSP m_variable_sp;
+
+private:
+ ValueObjectProvidedVariable(const ValueObjectProvidedVariable &) = delete;
+ const ValueObjectProvidedVariable &
+ operator=(const ValueObjectProvidedVariable &) = delete;
+};
+
+} // namespace
+
char ScriptedFrame::ID;
void ScriptedFrame::CheckInterpreterAndScriptObject() const {
@@ -305,8 +389,12 @@ lldb::ValueObjectSP ScriptedFrame::GetValueObjectForFrameVariable(
if (!values)
return {};
- return values->FindValueObjectByValueName(
+ lldb::ValueObjectSP valobj_sp = values->FindValueObjectByValueName(
variable_sp->GetName().AsCString(nullptr));
+ if (!valobj_sp)
+ return {};
+
+ return ValueObjectProvidedVariable::Create(*valobj_sp, variable_sp);
}
lldb::ValueObjectSP ScriptedFrame::FindVariable(ConstString name) {
@@ -315,7 +403,20 @@ lldb::ValueObjectSP ScriptedFrame::FindVariable(ConstString name) {
if (!values)
return {};
- return values->FindValueObjectByValueName(name.AsCString(nullptr));
+ lldb::ValueObjectSP valobj_sp =
+ values->FindValueObjectByValueName(name.AsCString(nullptr));
+ if (!valobj_sp)
+ return {};
+
+ // Classify the value the same way the frame's variable list would, so a value
+ // reached by name is not reported differently from the same value reached by
+ // enumeration.
+ if (m_variable_list_sp) {
+ if (VariableSP variable_sp = m_variable_list_sp->FindVariable(name))
+ return ValueObjectProvidedVariable::Create(*valobj_sp, variable_sp);
+ }
+
+ return valobj_sp;
}
lldb::ValueObjectSP ScriptedFrame::GetValueForVariableExpressionPath(
diff --git a/lldb/source/Symbol/Variable.cpp b/lldb/source/Symbol/Variable.cpp
index 3fc5c1cc38408..8ada6f6f4d2c7 100644
--- a/lldb/source/Symbol/Variable.cpp
+++ b/lldb/source/Symbol/Variable.cpp
@@ -92,9 +92,6 @@ ConstString Variable::GetUnqualifiedName() const { return m_name; }
bool Variable::NameMatches(ConstString name) const {
if (m_name == name)
return true;
- SymbolContext variable_sc;
- m_owner_scope->CalculateSymbolContext(&variable_sc);
-
return m_mangled.NameMatches(name);
}
bool Variable::NameMatches(const RegularExpression ®ex) const {
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
index eb327d2a34c90..6e9ecd925d759 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -866,6 +866,23 @@ def test_get_values(self):
self.assertEqual(variables.GetSize(), 1)
self.assertEqual(variables.GetValueAtIndex(0).name, "_handler_one")
+ # A variable reports the value type the frame classified it as, not the
+ # one its underlying value has. `_handler_one` is built from an
+ # expression, so these differ.
+ self.assertEqual(
+ variables.GetValueAtIndex(0).GetValueType(),
+ lldb.eValueTypeVariableLocal | lldb.eValueTypeSyntheticFlag,
+ )
+ # Classifying a variable must not cost its value or its type.
+ self.assertEqual(variables.GetValueAtIndex(0).GetValueAsUnsigned(), 1)
+ self.assertEqual(variables.GetValueAtIndex(0).GetTypeName(), "uint32_t")
+ # Reaching the same value by name has to agree with the enumeration.
+ self.assertEqual(
+ frame0.FindVariable("_handler_one").GetValueType(),
+ lldb.eValueTypeVariableLocal | lldb.eValueTypeSyntheticFlag,
+ )
+ self.assertEqual(frame0.FindVariable("_handler_one").GetValueAsUnsigned(), 1)
+
# Check the `frame variable` command(s) handle synthetic variables the
# way we expect by printing them.
self.expect("frame var", substrs=["variable_in_main", "_handler_one"])
More information about the lldb-commits
mailing list