[Lldb-commits] [lldb] [lldb] Support synthetic SBValues in ScriptedFrame variable enumeration (PR #192474)

Alexandre Perez via lldb-commits lldb-commits at lists.llvm.org
Thu Apr 16 08:25:43 PDT 2026


https://github.com/aperez created https://github.com/llvm/llvm-project/pull/192474

ScriptedFrame::PopulateVariableListFromInterface() silently dropped all SBValues from get_variables() that lacked a backing VariableSP. This meant frame variable (no args), frame variable -r, and the DAP Variables panel showed nothing for scripted frames with synthetic variables created via CreateValueFromData or CreateValueFromAddress.

The fix creates lightweight stub Variable objects for ValueObjects without VariableSP backing, as suggested by the existing TODO in PopulateVariableListFromInterface. This allows synthetic values to flow through the existing variable filtering pipeline (IsInRequestedScope, IsInScope, ScopeRequested) unchanged. Stub Variables use eValueTypeVariableLocal scope and null m_owner_scope.

>From b5acef154f7928332babe8a8151817a17f830cd5 Mon Sep 17 00:00:00 2001
From: Alexandre Perez <alexandreperez at meta.com>
Date: Wed, 15 Apr 2026 18:23:31 -0700
Subject: [PATCH] [lldb] Support synthetic SBValues in ScriptedFrame variable
 enumeration

ScriptedFrame::PopulateVariableListFromInterface() silently dropped all
SBValues from get_variables() that lacked a backing VariableSP. This
meant frame variable (no args), frame variable -r, and the DAP Variables
panel showed nothing for scripted frames with synthetic variables created
via CreateValueFromData or CreateValueFromAddress.

The fix creates lightweight stub Variable objects for ValueObjects
without VariableSP backing, as suggested by the existing TODO in
PopulateVariableListFromInterface. This allows synthetic values to flow
through the existing variable filtering pipeline (IsInRequestedScope,
IsInScope, ScopeRequested) unchanged. Stub Variables use
eValueTypeVariableLocal scope and null m_owner_scope.
---
 .../Process/scripted/ScriptedFrame.cpp        | 62 ++++++++++----
 .../Plugins/Process/scripted/ScriptedFrame.h  |  1 +
 lldb/source/Symbol/Variable.cpp               | 10 ++-
 .../TestScriptedFrameProvider.py              | 85 +++++++++++++++++++
 .../test_frame_providers.py                   | 72 ++++++++++++++++
 5 files changed, 210 insertions(+), 20 deletions(-)

diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
index 2b1d229487f23..e056724949464 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.cpp
@@ -13,11 +13,13 @@
 #include "lldb/Core/Debugger.h"
 #include "lldb/Core/Module.h"
 #include "lldb/Core/ModuleList.h"
+#include "lldb/Expression/DWARFExpressionList.h"
 #include "lldb/Interpreter/Interfaces/ScriptedFrameInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedInterface.h"
 #include "lldb/Interpreter/Interfaces/ScriptedThreadInterface.h"
 #include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/SymbolContext.h"
+#include "lldb/Symbol/Variable.h"
 #include "lldb/Symbol/VariableList.h"
 #include "lldb/Target/DynamicRegisterInfo.h"
 #include "lldb/Target/ExecutionContext.h"
@@ -282,37 +284,63 @@ ScriptedFrame::GetInScopeVariableList(bool get_file_globals,
 }
 
 void ScriptedFrame::PopulateVariableListFromInterface() {
-  // Fetch values from the interface.
-  ValueObjectListSP value_list_sp = GetInterface()->GetVariables();
-  if (!value_list_sp)
+  // m_value_object_list_sp serves as the single sentinel for both caches.
+  // m_variable_list_sp is always (re)built when m_value_object_list_sp is set.
+  if (m_value_object_list_sp)
     return;
 
-  // Convert what we can into a variable.
+  m_value_object_list_sp = GetInterface()->GetVariables();
+  if (!m_value_object_list_sp) {
+    // Use an empty list as sentinel so we don't re-invoke GetVariables().
+    m_value_object_list_sp = std::make_shared<ValueObjectList>();
+    return;
+  }
+
   m_variable_list_sp = std::make_shared<VariableList>();
-  for (uint32_t i = 0, e = value_list_sp->GetSize(); i < e; ++i) {
-    ValueObjectSP v = value_list_sp->GetValueObjectAtIndex(i);
+  for (uint32_t i = 0, e = m_value_object_list_sp->GetSize(); i < e; ++i) {
+    ValueObjectSP v = m_value_object_list_sp->GetValueObjectAtIndex(i);
     if (!v)
       continue;
 
     VariableSP var = v->GetVariable();
-    // TODO: We could in theory ask the scripted frame to *produce* a
-    //       variable for this value object.
-    if (!var)
-      continue;
-
+    if (!var) {
+      const char *name = v->GetName().AsCString(nullptr);
+      if (!name)
+        continue; // Skip nameless values — they can't be looked up later.
+
+      // Create a lightweight stub Variable for values without debug-info
+      // backing (e.g. created via CreateValueFromData/Address). This lets
+      // them participate in the standard variable enumeration and filtering
+      // pipeline (scope, IsInScope, regex, etc.). The actual value is
+      // served by GetValueObjectForFrameVariable() which looks up by name
+      // in m_value_object_list_sp.
+      var = std::make_shared<Variable>(
+          /*uid=*/i,
+          /*name=*/name,
+          /*mangled=*/nullptr,
+          /*symfile_type_sp=*/nullptr,
+          /*scope=*/eValueTypeVariableLocal,
+          /*owner_scope=*/nullptr,
+          /*scope_range=*/Variable::RangeList(),
+          /*decl=*/nullptr,
+          /*location=*/DWARFExpressionList(),
+          /*external=*/false,
+          /*artificial=*/true,
+          /*location_is_constant_data=*/false);
+    }
     m_variable_list_sp->AddVariable(var);
   }
 }
 
 lldb::ValueObjectSP ScriptedFrame::GetValueObjectForFrameVariable(
     const lldb::VariableSP &variable_sp, lldb::DynamicValueType use_dynamic) {
-  // Fetch values from the interface.
-  ValueObjectListSP values = m_scripted_frame_interface_sp->GetVariables();
-  if (!values)
-    return {};
+  if (!m_value_object_list_sp)
+    PopulateVariableListFromInterface();
 
-  return values->FindValueObjectByValueName(
-      variable_sp->GetName().AsCString(nullptr));
+  const char *name = variable_sp->GetName().AsCString(nullptr);
+  if (!name)
+    return {};
+  return m_value_object_list_sp->FindValueObjectByValueName(name);
 }
 
 lldb::ValueObjectSP ScriptedFrame::GetValueForVariableExpressionPath(
diff --git a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
index c2fc1df7724df..0454dc42d62c2 100644
--- a/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
+++ b/lldb/source/Plugins/Process/scripted/ScriptedFrame.h
@@ -104,6 +104,7 @@ class ScriptedFrame : public lldb_private::StackFrame {
   lldb::ScriptedFrameInterfaceSP m_scripted_frame_interface_sp;
   lldb_private::StructuredData::GenericSP m_script_object_sp;
   lldb::VariableListSP m_variable_list_sp;
+  lldb::ValueObjectListSP m_value_object_list_sp;
 
   static char ID;
 };
diff --git a/lldb/source/Symbol/Variable.cpp b/lldb/source/Symbol/Variable.cpp
index 336861afbf4bc..563dcd35b5954 100644
--- a/lldb/source/Symbol/Variable.cpp
+++ b/lldb/source/Symbol/Variable.cpp
@@ -66,6 +66,9 @@ lldb::LanguageType Variable::GetLanguage() const {
   if (lang != lldb::eLanguageTypeUnknown)
     return lang;
 
+  if (!m_owner_scope)
+    return lldb::eLanguageTypeUnknown;
+
   if (auto *func = m_owner_scope->CalculateSymbolContextFunction()) {
     if ((lang = func->GetLanguage()) != lldb::eLanguageTypeUnknown)
       return lang;
@@ -90,9 +93,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 &regex) const {
@@ -294,6 +294,10 @@ bool Variable::IsInScope(StackFrame *frame) {
   case eValueTypeVariableArgument:
   case eValueTypeVariableLocal:
     if (frame) {
+      // Synthetic variables without an owner scope have no scope
+      // boundaries and are always considered in-scope.
+      if (!m_owner_scope)
+        return true;
       // We don't have a location list, we just need to see if the block that
       // this variable was defined in is currently
       Block *deepest_frame_block =
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
index 1ac74152b0964..a5ffc8c24ff88 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/TestScriptedFrameProvider.py
@@ -835,6 +835,91 @@ def test_get_values(self):
         varp1 = frame0.GetValueForVariablePath("variable_in_main + 1")
         self.assertEqual(varp1.unsigned, 124)
 
+    def test_get_synthetic_values(self):
+        """Test that GetVariables returns SBValues without VariableSP backing.
+
+        Verifies that synthetic values created via CreateValueFromData (which
+        produce ValueObjectConstResult, not ValueObjectVariable) are visible
+        through all variable enumeration paths: SB API with DAP-style options,
+        CLI frame variable, and regex matching.
+        """
+        self.build()
+        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
+            self,
+            "Breakpoint for variable tests",
+            lldb.SBFileSpec(self.source),
+            only_one_thread=False,
+        )
+
+        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
+        self.runCmd("command script import " + script_path)
+
+        error = lldb.SBError()
+        target.RegisterScriptedFrameProvider(
+            "test_frame_providers.SyntheticValueFrameProvider",
+            lldb.SBStructuredData(),
+            error,
+        )
+        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
+
+        frame0 = thread.GetFrameAtIndex(0)
+        self.assertIsNotNone(frame0)
+        self.assertEqual(frame0.GetFunctionName(), "synthetic_value_frame")
+
+        # SB API: DAP Locals path (args=true, locals=true, statics=false, in_scope=true)
+        locals_vars = frame0.GetVariables(True, True, False, True)
+        self.assertEqual(
+            locals_vars.GetSize(),
+            2,
+            "DAP-Locals-style GetVariables should return synthetic locals",
+        )
+        names = {
+            locals_vars.GetValueAtIndex(i).name
+            for i in range(locals_vars.GetSize())
+        }
+        self.assertIn("synth_local_a", names)
+        self.assertIn("synth_local_b", names)
+        self.assertEqual(
+            locals_vars.GetFirstValueByName("synth_local_a").GetValueAsUnsigned(),
+            42,
+        )
+        self.assertEqual(
+            locals_vars.GetFirstValueByName("synth_local_b").GetValueAsUnsigned(),
+            100,
+        )
+
+        # SB API: DAP Globals path (args=false, locals=false, statics=true, in_scope=true)
+        globals_vars = frame0.GetVariables(False, False, True, True)
+        self.assertEqual(
+            globals_vars.GetSize(),
+            0,
+            "DAP-Globals-style GetVariables should exclude synthetic locals",
+        )
+
+        # Named lookup via GetValueForVariablePath
+        val = frame0.GetValueForVariablePath("synth_local_a")
+        self.assertTrue(val.IsValid())
+        self.assertEqual(val.GetValueAsUnsigned(), 42)
+
+        # CLI: frame variable (no args) lists synthetic locals
+        self.runCmd("frame select 0")
+        self.expect(
+            "frame variable", substrs=["synth_local_a", "synth_local_b"]
+        )
+
+        # CLI: frame variable -r regex finds synthetic locals
+        self.expect(
+            "frame variable -r synth_local",
+            substrs=["synth_local_a", "synth_local_b"],
+        )
+
+        # CLI: frame variable --no-locals excludes synthetic locals
+        self.expect(
+            "frame variable -l",
+            matching=False,
+            substrs=["synth_local_a", "synth_local_b"],
+        )
+
     def test_frame_validity_after_step(self):
         """Test that SBFrame references from ScriptedFrameProvider remain valid after stepping.
 
diff --git a/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
index 71457d3a1c227..a8d75e8c0d2f5 100644
--- a/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
+++ b/lldb/test/API/functionalities/scripted_frame_provider/test_frame_providers.py
@@ -599,3 +599,75 @@ def get_frame_at_index(self, index):
             # Pass through input frames (shifted by 1)
             return index - 1
         return None
+
+
+class SyntheticValueFrame(ScriptedFrame):
+    """Scripted frame providing synthetic values with no VariableSP backing."""
+
+    def __init__(self, thread, idx, pc, function_name):
+        args = lldb.SBStructuredData()
+        super().__init__(thread, args)
+        self.idx = idx
+        self.pc = pc
+        self.function_name = function_name
+
+    def get_id(self):
+        return self.idx
+
+    def get_pc(self):
+        return self.pc
+
+    def get_function_name(self):
+        return self.function_name
+
+    def is_artificial(self):
+        return False
+
+    def is_hidden(self):
+        return False
+
+    def get_register_context(self):
+        return None
+
+    def get_variables(self):
+        out = lldb.SBValueList()
+        target = self.thread.GetProcess().GetTarget()
+        uint_type = target.GetBasicType(lldb.eBasicTypeUnsignedInt)
+        data = lldb.SBData.CreateDataFromUInt32Array(
+            target.GetByteOrder(), target.GetAddressByteSize(), [42]
+        )
+        out.Append(target.CreateValueFromData("synth_local_a", data, uint_type))
+        data2 = lldb.SBData.CreateDataFromUInt32Array(
+            target.GetByteOrder(), target.GetAddressByteSize(), [100]
+        )
+        out.Append(target.CreateValueFromData("synth_local_b", data2, uint_type))
+        return out
+
+    def get_value_for_variable_expression(self, expr, options, error):
+        variables = self.get_variables()
+        for i in range(variables.GetSize()):
+            v = variables.GetValueAtIndex(i)
+            if v.name == expr:
+                return v
+        error.SetErrorString(f"variable '{expr}' not found")
+        return None
+
+
+class SyntheticValueFrameProvider(ScriptedFrameProvider):
+    """Provider that injects a frame with only synthetic (no VariableSP) values."""
+
+    def __init__(self, input_frames, args):
+        super().__init__(input_frames, args)
+
+    @staticmethod
+    def get_description():
+        return "Frame with synthetic-only values"
+
+    def get_frame_at_index(self, index):
+        if index == 0:
+            return SyntheticValueFrame(
+                self.thread, 0, 0xF00, "synthetic_value_frame"
+            )
+        elif index - 1 < len(self.input_frames):
+            return index - 1
+        return None



More information about the lldb-commits mailing list