feat(debug): enhance debug graph functionality with auto-discovery of local and global variables

This commit is contained in:
mynameisdeleted
2026-07-09 13:12:57 -04:00
parent 7630ab9050
commit 43e1eaf749
3 changed files with 522 additions and 58 deletions

View File

@@ -17,6 +17,20 @@ Writes debug_cpp.json / debug_rust.json into the output directory (default
~/lg, matching the style of the extension's own captured dumps): either the
resolved data object on success, or a {"PARSE_ERROR": ..., "raw": ...}
diagnostic on failure.
For the C++ build specifically, the breakpoint sits inside
`if (const int* top = list.peek_front())`, so `top` is a live local pointing
at the head node's `data` field (the node's first member, so its address
equals the node's own address, but its *static type* is just `const int*` --
gdb has no way to know it's secretly interior to a `Node<int>`). VS Code's
"debugVisualizer.debugAdapterConfigurations" expressionTemplate wraps
whatever you type in the Watch box as $debug_graph(${expr}), so typing
plain `top` evaluates $debug_graph(top) -- not $debug_graph(list, ...): we
replay that exact call and verify it still delves into the full list. This
relies on gdb/debug_graph.py's auto-discovery of sibling locals (here,
`list`, a struct type it *can* walk) merging into the same node graph as
`top`'s address, rather than requiring `top` to be passed as an explicit
watched root.
"""
import json
import re
@@ -27,14 +41,14 @@ from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
def run_gdb(gdb_bin, binary, break_file, break_line):
def run_gdb(gdb_bin, binary, break_file, break_line, expr="$debug_graph(list)"):
commands = [
"-gdb-set print elements 0",
"-gdb-set print characters 0",
"source %s" % (REPO / "gdb" / "debug_graph.py"),
"-break-insert %s:%d" % (break_file, break_line),
"-exec-run",
'-data-evaluate-expression "$debug_graph(list)"',
'-data-evaluate-expression "%s"' % expr.replace('"', '\\"'),
]
proc = subprocess.run(
[gdb_bin, "--interpreter=mi", binary],
@@ -80,15 +94,55 @@ def parse_like_extension(mi_escaped_value):
return {"ok": False, "error": str(e), "raw": result_text}
def dump(label, gdb_bin, binary, break_file, break_line, out_dir):
def verify_top_delves_into_list(data):
"""Checks that evaluating bare `top` -- i.e. $debug_graph(top), exactly
what VS Code's expressionTemplate produces for a Watch box containing
just "top" -- still shows the whole 30/20/10 list rather than stopping
at a lone scalar leaf. Returns True/False and prints why."""
nodes = data.get("nodes", [])
edges = data.get("edges", [])
roots_by_name = {r.get("name"): r for r in data.get("roots", [])}
bare_leaves = [
n for n in nodes
if [f.get("name") for f in n.get("fields", [])] == ["value"]
]
if bare_leaves:
print(" VERIFY FAIL: $debug_graph(top) produced bare scalar leaf node(s) %s -- "
"top's address was never upgraded to the full Node it coincides with"
% [n.get("id") for n in bare_leaves])
return False
next_edges = [e for e in edges if e.get("label") == "next"]
if len(next_edges) < 2:
print(" VERIFY FAIL: expected 2 'next' edges chaining the 30/20/10 nodes, found %d "
"(edges=%r)" % (len(next_edges), edges))
return False
if "list" not in roots_by_name:
print(" VERIFY FAIL: 'list' was not auto-discovered as a sibling local alongside "
"top (roots=%r)" % data.get("roots"))
return False
print(" VERIFY OK: $debug_graph(top) delves into the full list -- %d nodes, %d 'next' "
"edges, 'list' auto-discovered as a %r root"
% (len(nodes), len(next_edges), roots_by_name["list"].get("kind")))
return True
def dump(label, gdb_bin, binary, break_file, break_line, out_dir, expr="$debug_graph(list)", verify=None):
ok = True
try:
raw_value = run_gdb(gdb_bin, binary, break_file, break_line)
raw_value = run_gdb(gdb_bin, binary, break_file, break_line, expr)
except Exception as e:
result = {"PARSE_ERROR": "gdb invocation failed", "detail": str(e)}
ok = False
else:
parsed = parse_like_extension(raw_value)
if parsed["ok"]:
result = parsed["data"]
if verify is not None:
ok = verify(result)
else:
result = {
"PARSE_ERROR": parsed["error"],
@@ -96,18 +150,28 @@ def dump(label, gdb_bin, binary, break_file, break_line, out_dir):
"note": "This is what the Debug Visualizer extension would fail to parse, "
"triggering its generic byte-walk fallback (constructGraphFromVariablesReference).",
}
ok = False
out_path = out_dir / ("debug_%s.json" % label)
out_path.write_text(json.dumps(result, indent=4))
print("Wrote %s (%s)" % (out_path, "OK" if "PARSE_ERROR" not in result else "PARSE_ERROR"))
return ok
def main():
out_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / "lg"
out_dir.mkdir(parents=True, exist_ok=True)
dump("cpp", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir)
dump("rust", "rust-gdb", str(REPO / "target" / "debug" / "list_example"), "src/main.rs", 51, out_dir)
ok_cpp = dump("cpp", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir)
ok_cpp_top = dump(
"cpp_top", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir,
expr="$debug_graph(top)",
verify=verify_top_delves_into_list,
)
ok_rust = dump("rust", "rust-gdb", str(REPO / "target" / "debug" / "list_example"), "src/main.rs", 51, out_dir)
if not (ok_cpp and ok_cpp_top and ok_rust):
sys.exit(1)
if __name__ == "__main__":