[3/3] Return global scope from DAP scopes request

Message ID 20240516-dap-global-scope-v1-3-07c493009505@adacore.com
State New
Headers
Series Return a global scope from DAP scopes request |

Checks

Context Check Description
linaro-tcwg-bot/tcwg_gdb_build--master-aarch64 success Testing passed
linaro-tcwg-bot/tcwg_gdb_build--master-arm success Testing passed
linaro-tcwg-bot/tcwg_gdb_check--master-arm success Testing passed
linaro-tcwg-bot/tcwg_gdb_check--master-aarch64 success Testing passed

Commit Message

Tom Tromey May 16, 2024, 6:01 p.m. UTC
  A co-worker requested that the DAP code emit a scope for global
variables.  It's not really practical to do this for all globals, but
it seemed reasonable to do this for globals coming from the frame's
compilation unit.  For Ada in particular, this is convenient as it
exposes package-scoped variables.
---
 gdb/NEWS                              |  3 ++
 gdb/data-directory/Makefile.in        |  1 +
 gdb/python/lib/gdb/dap/globalvars.py  | 97 +++++++++++++++++++++++++++++++++++
 gdb/python/lib/gdb/dap/scopes.py      |  4 ++
 gdb/testsuite/gdb.dap/ptrref.exp      |  8 +--
 gdb/testsuite/gdb.dap/rust-slices.exp |  4 +-
 6 files changed, 113 insertions(+), 4 deletions(-)
  

Comments

Eli Zaretskii May 16, 2024, 6:30 p.m. UTC | #1
> From: Tom Tromey <tromey@adacore.com>
> Date: Thu, 16 May 2024 12:01:13 -0600
> 
> A co-worker requested that the DAP code emit a scope for global
> variables.  It's not really practical to do this for all globals, but
> it seemed reasonable to do this for globals coming from the frame's
> compilation unit.  For Ada in particular, this is convenient as it
> exposes package-scoped variables.
> ---
>  gdb/NEWS                              |  3 ++
>  gdb/data-directory/Makefile.in        |  1 +
>  gdb/python/lib/gdb/dap/globalvars.py  | 97 +++++++++++++++++++++++++++++++++++
>  gdb/python/lib/gdb/dap/scopes.py      |  4 ++
>  gdb/testsuite/gdb.dap/ptrref.exp      |  8 +--
>  gdb/testsuite/gdb.dap/rust-slices.exp |  4 +-
>  6 files changed, 113 insertions(+), 4 deletions(-)
> 
> diff --git a/gdb/NEWS b/gdb/NEWS
> index 691abff420b..ab252c81ab6 100644
> --- a/gdb/NEWS
> +++ b/gdb/NEWS
> @@ -179,6 +179,9 @@ show unwind-on-signal
>    ** The "set debug dap-log-file" command is now documented.  This
>       command was available in GDB 14 but not documented.
>  
> +  ** The "scopes" request will now return a scope holding global
> +     variables from the stack frame's compilation unit.
> +
>  * Guile API

The NEWS part is okay, thanks.

Reviewed-By: Eli Zaretskii <eliz@gnu.org>
  

Patch

diff --git a/gdb/NEWS b/gdb/NEWS
index 691abff420b..ab252c81ab6 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -179,6 +179,9 @@  show unwind-on-signal
   ** The "set debug dap-log-file" command is now documented.  This
      command was available in GDB 14 but not documented.
 
+  ** The "scopes" request will now return a scope holding global
+     variables from the stack frame's compilation unit.
+
 * Guile API
 
   ** New constants SYMBOL_TYPE_DOMAIN, SYMBOL_FUNCTION_DOMAIN, and
diff --git a/gdb/data-directory/Makefile.in b/gdb/data-directory/Makefile.in
index 98a43529b07..f529656ca05 100644
--- a/gdb/data-directory/Makefile.in
+++ b/gdb/data-directory/Makefile.in
@@ -98,6 +98,7 @@  PYTHON_FILE_LIST = \
 	gdb/dap/evaluate.py \
 	gdb/dap/events.py \
 	gdb/dap/frames.py \
+	gdb/dap/globalvars.py \
 	gdb/dap/__init__.py \
 	gdb/dap/io.py \
 	gdb/dap/launch.py \
diff --git a/gdb/python/lib/gdb/dap/globalvars.py b/gdb/python/lib/gdb/dap/globalvars.py
new file mode 100644
index 00000000000..149c9a8f22f
--- /dev/null
+++ b/gdb/python/lib/gdb/dap/globalvars.py
@@ -0,0 +1,97 @@ 
+# Copyright 2024 Free Software Foundation, Inc.
+
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import gdb
+
+from .sources import make_source
+from .startup import in_gdb_thread
+from .varref import BaseReference
+
+# Map a block identifier to a scope object.
+_id_to_scope = {}
+
+
+# Arrange to clear the scope references when the inferior runs.
+@in_gdb_thread
+def clear(event):
+    global _id_to_scope
+    _id_to_scope = {}
+
+
+gdb.events.cont.connect(clear)
+
+
+# A scope that holds static and/or global variables.
+class _Globals(BaseReference):
+    def __init__(self, filename, var_list):
+        super().__init__("Globals")
+        self.filename = filename
+        self.var_list = var_list
+
+    def to_object(self):
+        result = super().to_object()
+        result["presentationHint"] = "globals"
+        # How would we know?
+        result["expensive"] = False
+        result["namedVariables"] = self.child_count()
+        if self.filename is not None:
+            result["source"] = make_source(self.filename)
+        return result
+
+    def has_children(self):
+        # This object won't even be created if there are no variables
+        # to return.
+        return True
+
+    def child_count(self):
+        return len(self.var_list)
+
+    @in_gdb_thread
+    def fetch_one_child(self, idx):
+        return self.var_list[idx].value()
+
+
+@in_gdb_thread
+def get_global_scope(frame):
+    """Given a frame decorator, return the corresponding global scope
+    object.
+
+    If the frame does not have a block, or if the CU does not have
+    globals (that is, empty static and global blocks), return None."""
+    inf_frame = frame.inferior_frame()
+    # It's unfortunate that this API throws instead of returning None.
+    try:
+        block = inf_frame.block()
+    except RuntimeError:
+        return None
+
+    global _id_to_scope
+    block = block.static_block
+    if block in _id_to_scope:
+        return _id_to_scope[block]
+
+    syms = []
+    block_iter = block
+    while block_iter is not None:
+        syms += [sym for sym in block_iter if sym.is_variable]
+        block_iter = block_iter.superblock
+
+    if len(syms) == 0:
+        return None
+
+    result = _Globals(frame.filename(), syms)
+    _id_to_scope[block] = result
+
+    return result
diff --git a/gdb/python/lib/gdb/dap/scopes.py b/gdb/python/lib/gdb/dap/scopes.py
index 8cd860141d6..ad2f10f4057 100644
--- a/gdb/python/lib/gdb/dap/scopes.py
+++ b/gdb/python/lib/gdb/dap/scopes.py
@@ -16,6 +16,7 @@ 
 import gdb
 
 from .frames import frame_for_id
+from .globalvars import get_global_scope
 from .server import request
 from .startup import in_gdb_thread
 from .varref import BaseReference
@@ -161,4 +162,7 @@  def scopes(*, frameId: int, **extra):
             scopes.append(_ScopeReference("Locals", "locals", frame, locs))
         scopes.append(_RegisterReference("Registers", frame))
         frame_to_scope[frameId] = scopes
+        global_scope = get_global_scope(frame)
+        if global_scope is not None:
+            scopes.append(global_scope)
     return {"scopes": [x.to_object() for x in scopes]}
diff --git a/gdb/testsuite/gdb.dap/ptrref.exp b/gdb/testsuite/gdb.dap/ptrref.exp
index 0552c3b9815..236ffae12d5 100644
--- a/gdb/testsuite/gdb.dap/ptrref.exp
+++ b/gdb/testsuite/gdb.dap/ptrref.exp
@@ -54,12 +54,14 @@  set scopes [dap_check_request_and_response "get scopes" scopes \
 		[format {o frameId [i %d]} $frame_id]]
 set scopes [dict get [lindex $scopes 0] body scopes]
 
-gdb_assert {[llength $scopes] == 2} "two scopes"
+gdb_assert {[llength $scopes] == 3} "three scopes"
 
-lassign $scopes scope reg_scope
+lassign $scopes scope reg_scope global_scope
 gdb_assert {[dict get $scope name] == "Locals"} "scope is locals"
+gdb_assert {[dict get $global_scope name] == "Globals"} "scope is globals"
 
-gdb_assert {[dict get $scope namedVariables] == 4} "three vars in scope"
+gdb_assert {[dict get $scope namedVariables] == 4} "four vars in locals"
+gdb_assert {[dict get $global_scope namedVariables] == 1} "one var in globals"
 
 set num [dict get $scope variablesReference]
 set refs [lindex [dap_check_request_and_response "fetch variables" \
diff --git a/gdb/testsuite/gdb.dap/rust-slices.exp b/gdb/testsuite/gdb.dap/rust-slices.exp
index c85568d69ea..d3bd3050c16 100644
--- a/gdb/testsuite/gdb.dap/rust-slices.exp
+++ b/gdb/testsuite/gdb.dap/rust-slices.exp
@@ -59,7 +59,9 @@  set scopes [dap_check_request_and_response "get scopes" scopes \
 		[format {o frameId [i %d]} $frame_id]]
 set scopes [dict get [lindex $scopes 0] body scopes]
 
-gdb_assert {[llength $scopes] == 2} "two scopes"
+# There are three scopes because an artificial symbol ends up in the
+# DWARF.  See https://github.com/rust-lang/rust/issues/125126.
+gdb_assert {[llength $scopes] == 3} "three scopes"
 
 lassign $scopes scope ignore
 gdb_assert {[dict get $scope name] == "Locals"} "scope is locals"