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:
mynameisdeleted
2026-07-09 13:47:36 -04:00
parent 43e1eaf749
commit cfd638f19b
4 changed files with 155 additions and 26 deletions

View File

@@ -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(
{