feat(debug): enhance linked list demo with item count and message output
feat(debug): improve GDB pretty-printer support for structured types fix(debug): update dump_debug_graph script to reflect changes in main.cpp feat(visualization): add type hint display to node table visualizer
This commit is contained in:
@@ -55,16 +55,21 @@ private:
|
||||
|
||||
int main() {
|
||||
LinkedList<int> list;
|
||||
|
||||
int count=0;
|
||||
char * message1 = "hello world";
|
||||
// Demonstrate pushing items
|
||||
list.push_front(10);
|
||||
count++;
|
||||
list.push_front(20);
|
||||
count++;
|
||||
list.push_front(30);
|
||||
count++;
|
||||
|
||||
// Demonstrate peeking at the top item
|
||||
if (const int* top = list.peek_front()) {
|
||||
std::cout << "Top element: " << *top << "\n"; // Output: 30
|
||||
std::cout << "Top element: " << *top << "\n"<<"count: "<<count<<"\n"; // Output: 30
|
||||
}
|
||||
std::cout<<message1<<"\n";
|
||||
|
||||
// Demonstrate popping items
|
||||
while (auto value = list.pop_front()) {
|
||||
|
||||
@@ -103,7 +103,60 @@ def _flatten_variant(val, t, label):
|
||||
return
|
||||
|
||||
|
||||
def _process_member(label, member_val, node_id, fields_out, edges, nodes, visited):
|
||||
def _pretty_printer_summary(val):
|
||||
"""Returns (leaves, empty_text) if val has a registered gdb
|
||||
pretty-printer (e.g. one of libstdc++'s STL printers, loaded via
|
||||
-enable-pretty-printing), else None.
|
||||
|
||||
`leaves` is a list of (label, gdb.Value) pairs from the printer's
|
||||
children() -- e.g. std::optional<T>'s single "[contained value]" child
|
||||
-- to recurse into exactly like a struct's own fields, so a contained
|
||||
pointer still becomes a proper edge instead of an opaque string.
|
||||
`empty_text` is what to show when there are no children (e.g. an
|
||||
empty/disengaged std::optional, or an empty std::vector): the
|
||||
printer's own to_string().
|
||||
|
||||
Leaning on gdb's pretty-printers rather than hardcoding STL internals
|
||||
(std::optional's actual data lives behind private base-class
|
||||
subobjects our own field walk would otherwise skip) means this works
|
||||
uniformly for std::optional, std::unique_ptr, std::vector, etc. --
|
||||
the same principle as _flatten_variant leaning on gdb for Rust enums
|
||||
instead of reimplementing niche-optimization layout ourselves."""
|
||||
printer = gdb.default_visualizer(val)
|
||||
if printer is None:
|
||||
return None
|
||||
leaves = []
|
||||
if hasattr(printer, "children"):
|
||||
try:
|
||||
leaves = [(str(name), child_val) for name, child_val in printer.children()]
|
||||
except gdb.error:
|
||||
leaves = []
|
||||
if leaves:
|
||||
return leaves, None
|
||||
try:
|
||||
return [], str(printer.to_string())
|
||||
except gdb.error:
|
||||
return [], "<pretty-printer error>"
|
||||
|
||||
|
||||
def _type_name(t):
|
||||
"""Best-effort display name for a type (e.g. "std::optional<int>",
|
||||
"core::option::Option<...>::Some"), used as a typeHint so unwrapped
|
||||
fields/roots don't lose what wrapper they came from."""
|
||||
try:
|
||||
return t.tag or str(t)
|
||||
except gdb.error:
|
||||
return None
|
||||
|
||||
|
||||
def _append_field(fields_out, name, value, is_pointer=False, type_hint=None):
|
||||
field = {"name": name, "value": value, "isPointer": is_pointer}
|
||||
if type_hint:
|
||||
field["typeHint"] = type_hint
|
||||
fields_out.append(field)
|
||||
|
||||
|
||||
def _process_member(label, member_val, node_id, fields_out, edges, nodes, visited, type_hint=None):
|
||||
member_type = member_val.type.strip_typedefs()
|
||||
|
||||
if member_type.code == gdb.TYPE_CODE_PTR:
|
||||
@@ -111,31 +164,62 @@ def _process_member(label, member_val, node_id, fields_out, edges, nodes, visite
|
||||
ptr_int = int(member_val)
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
fields_out.append({"name": label, "value": _addr_str(ptr_int), "isPointer": True})
|
||||
_append_field(fields_out, label, _addr_str(ptr_int), is_pointer=True, type_hint=type_hint)
|
||||
if ptr_int:
|
||||
edges.append((node_id, _addr_str(ptr_int), label))
|
||||
_visit_pointer(member_val, ptr_int, nodes, edges, visited)
|
||||
return
|
||||
|
||||
if member_type.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
summary = _pretty_printer_summary(member_val)
|
||||
if summary is not None:
|
||||
leaves, empty_text = summary
|
||||
wrapper_name = _type_name(member_type)
|
||||
if not leaves:
|
||||
_append_field(fields_out, label, empty_text, type_hint=wrapper_name)
|
||||
elif len(leaves) == 1:
|
||||
_process_member(label, leaves[0][1], node_id, fields_out, edges, nodes, visited, wrapper_name)
|
||||
else:
|
||||
for leaf_label, leaf_val in leaves:
|
||||
_process_member(
|
||||
"%s.%s" % (label, leaf_label), leaf_val, node_id, fields_out, edges, nodes, visited,
|
||||
wrapper_name,
|
||||
)
|
||||
return
|
||||
|
||||
if _is_variant_like(member_type):
|
||||
leaves = list(_flatten_variant(member_val, member_type, label))
|
||||
wrapper_name = _type_name(member_type)
|
||||
if leaves:
|
||||
for leaf_label, leaf_val in leaves:
|
||||
_process_member(leaf_label, leaf_val, node_id, fields_out, edges, nodes, visited)
|
||||
_process_member(leaf_label, leaf_val, node_id, fields_out, edges, nodes, visited, wrapper_name)
|
||||
else:
|
||||
# Unit variant (e.g. None) or an active variant we couldn't
|
||||
# resolve -- fall back to gdb's own rendering of it.
|
||||
fields_out.append(
|
||||
{"name": label, "value": _format_scalar(member_val), "isPointer": False}
|
||||
)
|
||||
_append_field(fields_out, label, _format_scalar(member_val), type_hint=wrapper_name)
|
||||
return
|
||||
|
||||
fields_out.append({"name": label, "value": _format_scalar(member_val), "isPointer": False})
|
||||
_append_field(fields_out, label, _format_scalar(member_val), type_hint=type_hint)
|
||||
|
||||
|
||||
def _describe_struct(val, node_id, nodes, edges, visited):
|
||||
"""val: a gdb.Value of struct/union type (already dereferenced)."""
|
||||
fields_out = []
|
||||
|
||||
summary = _pretty_printer_summary(val)
|
||||
if summary is not None:
|
||||
leaves, empty_text = summary
|
||||
wrapper_name = _type_name(val.type.strip_typedefs())
|
||||
if not leaves:
|
||||
_append_field(fields_out, "value", empty_text, type_hint=wrapper_name)
|
||||
elif len(leaves) == 1:
|
||||
_process_member("value", leaves[0][1], node_id, fields_out, edges, nodes, visited, wrapper_name)
|
||||
else:
|
||||
for leaf_label, leaf_val in leaves:
|
||||
_process_member(leaf_label, leaf_val, node_id, fields_out, edges, nodes, visited, wrapper_name)
|
||||
nodes[node_id] = fields_out
|
||||
return
|
||||
|
||||
t = val.type.strip_typedefs()
|
||||
for f in t.fields():
|
||||
if f.name is None or getattr(f, "is_base_class", False):
|
||||
@@ -170,6 +254,22 @@ def _visit_pointer(ptr_val, ptr_int, nodes, edges, visited):
|
||||
visited.add(node_id)
|
||||
pointee = ptr_val.dereference()
|
||||
_describe_struct(pointee, node_id, nodes, edges, visited)
|
||||
elif pointee_type.code in (gdb.TYPE_CODE_INT, gdb.TYPE_CODE_CHAR) and pointee_type.sizeof == 1:
|
||||
# char*/const char*: show the whole C string, not just *ptr (one
|
||||
# byte) -- the node's own id/header is already the pointer's
|
||||
# address, so this gives address + full value like everything else.
|
||||
# No explicit `length`: that disables stopping at the NUL
|
||||
# terminator entirely (reads exactly N bytes, running into
|
||||
# whatever garbage memory follows a shorter real string).
|
||||
if node_id in nodes:
|
||||
return
|
||||
try:
|
||||
text = ptr_val.string()
|
||||
if len(text) > 500:
|
||||
text = text[:500] + "…"
|
||||
except (gdb.error, UnicodeDecodeError):
|
||||
text = _format_scalar(ptr_val.dereference())
|
||||
nodes[node_id] = [{"name": "value", "value": text, "isPointer": False}]
|
||||
else:
|
||||
if node_id in nodes:
|
||||
return # already described (richly or otherwise) via another path
|
||||
@@ -205,9 +305,10 @@ def _walk_primary_root(root_val, nodes, edges, visited):
|
||||
|
||||
def _add_named_root(name, val, kind, nodes, edges, visited, roots_out, skip_addrs):
|
||||
"""Walks `val` (a local/global/watched variable) same as a primary root,
|
||||
then records it as a (name, value, kind) root entry -- unless its
|
||||
then records it as a (name, value, kind, type) root entry -- unless its
|
||||
address is in skip_addrs (already covered by the primary root)."""
|
||||
t = val.type.strip_typedefs()
|
||||
type_name = _type_name(t)
|
||||
if t.code == gdb.TYPE_CODE_PTR:
|
||||
try:
|
||||
ptr_int = int(val)
|
||||
@@ -219,7 +320,7 @@ def _add_named_root(name, val, kind, nodes, edges, visited, roots_out, skip_addr
|
||||
if addr in skip_addrs:
|
||||
return
|
||||
_visit_pointer(val, ptr_int, nodes, edges, visited)
|
||||
roots_out.append((name, addr, kind))
|
||||
roots_out.append((name, addr, kind, type_name))
|
||||
elif t.code in (gdb.TYPE_CODE_STRUCT, gdb.TYPE_CODE_UNION):
|
||||
try:
|
||||
addr = _addr_str(int(val.address))
|
||||
@@ -230,9 +331,9 @@ def _add_named_root(name, val, kind, nodes, edges, visited, roots_out, skip_addr
|
||||
if addr not in visited:
|
||||
visited.add(addr)
|
||||
_describe_struct(val, addr, nodes, edges, visited)
|
||||
roots_out.append((name, addr, kind))
|
||||
roots_out.append((name, addr, kind, type_name))
|
||||
else:
|
||||
roots_out.append((name, _format_scalar(val), kind))
|
||||
roots_out.append((name, _format_scalar(val), kind, type_name))
|
||||
|
||||
|
||||
def _frame_local_symbols(frame):
|
||||
@@ -293,7 +394,7 @@ def build_graph_json(root_val, watched_roots):
|
||||
except gdb.error:
|
||||
ptr_int = 0
|
||||
addr = _addr_str(ptr_int)
|
||||
roots.append((name, addr, "watched"))
|
||||
roots.append((name, addr, "watched", _type_name(ptr_val.type.strip_typedefs())))
|
||||
if ptr_int and addr not in skip_addrs:
|
||||
_visit_pointer(ptr_val, ptr_int, nodes, edges, visited)
|
||||
|
||||
@@ -329,7 +430,12 @@ def build_graph_json(root_val, watched_roots):
|
||||
for (src, dst, label) in edges
|
||||
if dst in nodes
|
||||
]
|
||||
roots_json = [{"name": name, "value": value, "kind": kind} for (name, value, kind) in roots]
|
||||
roots_json = [
|
||||
{"name": name, "value": value, "kind": kind, "type": type_name}
|
||||
if type_name
|
||||
else {"name": name, "value": value, "kind": kind}
|
||||
for (name, value, kind, type_name) in roots
|
||||
]
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
|
||||
@@ -103,14 +103,18 @@ def verify_top_delves_into_list(data):
|
||||
edges = data.get("edges", [])
|
||||
roots_by_name = {r.get("name"): r for r in data.get("roots", [])}
|
||||
|
||||
bare_leaves = [
|
||||
# Other auto-discovered locals (e.g. a `char*` local) can legitimately
|
||||
# produce their own bare scalar-leaf nodes -- only care whether *some*
|
||||
# node looks like a real Node<int> (data+next), i.e. top's address got
|
||||
# upgraded into the full struct rather than stopping at a lone int.
|
||||
node_shaped = [
|
||||
n for n in nodes
|
||||
if [f.get("name") for f in n.get("fields", [])] == ["value"]
|
||||
if [f.get("name") for f in n.get("fields", [])] == ["data", "next"]
|
||||
]
|
||||
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])
|
||||
if not node_shaped:
|
||||
print(" VERIFY FAIL: $debug_graph(top) produced no data+next Node-shaped node -- "
|
||||
"top's address was never upgraded to the full Node it coincides with "
|
||||
"(nodes=%r)" % [n.get("id") for n in nodes])
|
||||
return False
|
||||
|
||||
next_edges = [e for e in edges if e.get("label") == "next"]
|
||||
@@ -162,9 +166,9 @@ 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)
|
||||
|
||||
ok_cpp = dump("cpp", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir)
|
||||
ok_cpp = dump("cpp", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 70, out_dir)
|
||||
ok_cpp_top = dump(
|
||||
"cpp_top", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 66, out_dir,
|
||||
"cpp_top", "gdb", str(REPO / "cpp" / "list_example_cpp"), "cpp/main.cpp", 70, out_dir,
|
||||
expr="$debug_graph(top)",
|
||||
verify=verify_top_delves_into_list,
|
||||
)
|
||||
|
||||
@@ -28,6 +28,7 @@ module.exports = function (register, lib) {
|
||||
name: sj.sString(),
|
||||
value: sj.sString(),
|
||||
isPointer: sj.sOptionalProp(sj.sBoolean(), {}),
|
||||
typeHint: sj.sOptionalProp(sj.sString(), {}),
|
||||
});
|
||||
|
||||
var sNode = sj.sOpenObject({
|
||||
@@ -45,6 +46,7 @@ module.exports = function (register, lib) {
|
||||
name: sj.sString(),
|
||||
value: sj.sString(),
|
||||
kind: sj.sOptionalProp(sj.sString(), {}),
|
||||
type: sj.sOptionalProp(sj.sString(), {}),
|
||||
});
|
||||
|
||||
var sData = sj.sOpenObject({
|
||||
@@ -293,6 +295,7 @@ module.exports = function (register, lib) {
|
||||
bodyBg: "#252526",
|
||||
fieldName: "#9cdcfe",
|
||||
fieldValue: "#d4d4d4",
|
||||
fieldType: "#7a7a7a",
|
||||
pointerValue: "#ce9178",
|
||||
edge: "#8a8a8a",
|
||||
viewportBg: "#1e1e1e",
|
||||
@@ -304,6 +307,7 @@ module.exports = function (register, lib) {
|
||||
bodyBg: "#ffffff",
|
||||
fieldName: "#0451a5",
|
||||
fieldValue: "#1a1a1a",
|
||||
fieldType: "#8a8a8a",
|
||||
pointerValue: "#a31515",
|
||||
edge: "#6b6b6b",
|
||||
viewportBg: "#fafafa",
|
||||
@@ -532,13 +536,23 @@ module.exports = function (register, lib) {
|
||||
|
||||
n.fields.forEach(function (f) {
|
||||
var row = el("tr");
|
||||
if (f.typeHint) row.title = f.name + ": " + f.typeHint + " = " + f.value;
|
||||
var nameCell = el("td", {
|
||||
padding: "2px 8px",
|
||||
color: colors.fieldName,
|
||||
whiteSpace: "nowrap",
|
||||
height: ROW_HEIGHT - 4 + "px",
|
||||
});
|
||||
nameCell.textContent = f.name;
|
||||
nameCell.appendChild(document.createTextNode(f.name));
|
||||
if (f.typeHint) {
|
||||
var typeSpan = el("span", {
|
||||
color: colors.fieldType,
|
||||
fontSize: "10px",
|
||||
marginLeft: "5px",
|
||||
});
|
||||
typeSpan.textContent = " " + f.typeHint;
|
||||
nameCell.appendChild(typeSpan);
|
||||
}
|
||||
var valueCell = el("td", {
|
||||
padding: "2px 8px",
|
||||
color: f.isPointer ? colors.pointerValue : colors.fieldValue,
|
||||
@@ -579,7 +593,7 @@ module.exports = function (register, lib) {
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
});
|
||||
badge.title = r.name + " = " + r.value;
|
||||
badge.title = r.name + (r.type ? " : " + r.type : "") + " = " + r.value;
|
||||
badge.textContent = r.name + (matched ? "" : " (" + r.value + ")");
|
||||
world.appendChild(badge);
|
||||
var line = null;
|
||||
@@ -850,7 +864,7 @@ module.exports = function (register, lib) {
|
||||
if (rb.line) rb.line.style.display = targetId ? "" : "none";
|
||||
if (!targetId) return;
|
||||
var redirected = targetId !== rb.root.value;
|
||||
rb.badge.title = rb.root.name + " = " + rb.root.value +
|
||||
rb.badge.title = rb.root.name + (rb.root.type ? " : " + rb.root.type : "") + " = " + rb.root.value +
|
||||
(redirected ? " (inside collapsed " + targetId + ")" : "");
|
||||
var target = byId[targetId];
|
||||
var stackIndex = badgesPerTarget[targetId] || 0;
|
||||
|
||||
Reference in New Issue
Block a user