feat(debug): add GDB-based debug visualizer for linked lists

Replace the custom toDebugGraph() method that generated JSON directly in C++ with a generic GDB Python script (debug_graph.py). The script traverses pointers and structures to build a graph representation for the VS Code Debug Visualizer, enabling external pointer display and more extensible visualization.

Update launch configurations to load the script and to disable array/string truncation (needed for the visualizer JSON). Add a debugAdapterConfigurations entry in settings.json to use the expression template $debug_graph(${expr}) for the cppdbg adapter.
This commit is contained in:
mynameisdeleted
2026-07-09 12:14:27 -04:00
parent 273d4dbf2b
commit a8d105005e
6 changed files with 511 additions and 32 deletions

View File

@@ -10,6 +10,11 @@
// Data shape produced by cpp/main.cpp's LinkedList<T>::toDebugGraph():
// nodes: [{ id: "0x...", fields: [{ name, value, isPointer? }, ...] }]
// edges: [{ from: "0x...", to: "0x...", label? }]
// roots: [{ name: "top", value: "0x..." }] -- external pointer variables
// (e.g. a local `top`/`end` in the caller's stack frame); drawn as a
// small pill above the graph with an arrow to whichever node's id
// matches its value, since a node's own address equals its first
// member's address.
module.exports = function (register, lib) {
var sj = lib.semanticJson;
@@ -31,10 +36,16 @@ module.exports = function (register, lib) {
label: sj.sOptionalProp(sj.sString(), {}),
});
var sRoot = sj.sOpenObject({
name: sj.sString(),
value: sj.sString(),
});
var sData = sj.sOpenObject({
kind: sj.sOpenObject({ nodeTable: sj.sLiteral(true) }),
nodes: sj.sArrayOf(sNode),
edges: sj.sArrayOf(sEdge),
roots: sj.sOptionalProp(sj.sArrayOf(sRoot), {}),
});
var HEADER_HEIGHT = 24;
@@ -44,6 +55,8 @@ module.exports = function (register, lib) {
var H_GAP = 70;
var V_GAP = 24;
var PADDING = 20;
var ROOT_ROW_HEIGHT = 50;
var ROOT_BADGE_HEIGHT = 22;
function layout(nodes, edges) {
var ids = nodes.map(function (n) {
@@ -146,6 +159,8 @@ module.exports = function (register, lib) {
};
var positions = layout(data.nodes, data.edges);
var roots = data.roots || [];
var topOffset = roots.length > 0 ? ROOT_ROW_HEIGHT : 0;
var byId = {};
data.nodes.forEach(function (n) {
@@ -164,12 +179,33 @@ module.exports = function (register, lib) {
Object.keys(positions).forEach(function (id) {
var p = positions[id];
var x = p.level * (NODE_WIDTH + H_GAP);
var y = p.col * (Math.max(boxHeights[id], HEADER_HEIGHT + ROW_HEIGHT) + V_GAP);
var y = topOffset + p.col * (Math.max(boxHeights[id], HEADER_HEIGHT + ROW_HEIGHT) + V_GAP);
pixelPos[id] = { x: x, y: y };
maxX = Math.max(maxX, x + NODE_WIDTH);
maxY = Math.max(maxY, y + boxHeights[id]);
});
// Spread root badges above the nodes they point to; unmatched roots
// (e.g. a pointer into the middle of a struct, or a stale/null value)
// queue up along the top-left instead.
var rootPos = {};
var nextUnmatchedX = 0;
var badgesPerTarget = {};
roots.forEach(function (r) {
var target = pixelPos[r.value];
var x;
if (target) {
var stackIndex = badgesPerTarget[r.value] || 0;
badgesPerTarget[r.value] = stackIndex + 1;
x = target.x + stackIndex * 90;
} else {
x = nextUnmatchedX;
nextUnmatchedX += NODE_WIDTH / 2 + 20;
}
rootPos[r.name] = { x: x, y: 0, matched: !!target };
maxX = Math.max(maxX, x + NODE_WIDTH);
});
var canvas = el("div", {
position: "relative",
width: maxX + "px",
@@ -307,6 +343,48 @@ module.exports = function (register, lib) {
canvas.appendChild(box);
});
roots.forEach(function (r) {
var pos = rootPos[r.name];
if (pos.matched) {
var targetPos = pixelPos[r.value];
var x1 = pos.x + NODE_WIDTH / 2;
var y1 = ROOT_BADGE_HEIGHT;
var x2 = targetPos.x + NODE_WIDTH / 2;
var y2 = targetPos.y;
var line = document.createElementNS(svgNs, "path");
line.setAttribute("d", "M" + x1 + "," + y1 + " L " + x2 + "," + y2);
line.setAttribute("fill", "none");
line.setAttribute("stroke", colors.edge);
line.setAttribute("stroke-width", "1.5");
line.setAttribute("stroke-dasharray", "3,3");
line.setAttribute("marker-end", "url(#dv-arrow)");
svg.appendChild(line);
}
var badge = el("div", {
position: "absolute",
left: pos.x + "px",
top: "0px",
width: NODE_WIDTH / 2 + "px",
height: ROOT_BADGE_HEIGHT + "px",
lineHeight: ROOT_BADGE_HEIGHT + "px",
textAlign: "center",
borderRadius: "11px",
border: "1px solid " + colors.border,
background: pos.matched ? colors.headerBg : "transparent",
color: pos.matched ? colors.headerFg : colors.pointerValue,
fontFamily: "var(--vscode-editor-font-family, monospace)",
fontSize: "12px",
fontWeight: "bold",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
});
badge.title = r.name + " = " + r.value;
badge.textContent = r.name + (pos.matched ? "" : " (" + r.value + ")");
canvas.appendChild(badge);
});
target.appendChild(canvas);
}