From 4803298f2014120256965ce811ad098dd802a0f2 Mon Sep 17 00:00:00 2001 From: theo-ai-lab <217547998+theo-ai-lab@users.noreply.github.com> Date: Sat, 15 Aug 2026 14:08:40 -0700 Subject: [PATCH 1/2] [libcudacxx] Add debugger pretty-printers for cuda::std::optional Adds GDB and LLDB pretty-printers for cuda::std::optional, following the pattern of the existing array, tuple, and span printers (#10528, #10534, #10699), plus a test scenario under test/debugging/optional. The disengaged state is rendered without dereferencing the payload storage; optional is handled through its pointer member. Closes #10093. --- libcudacxx/share/libcudacxx/gdb/__init__.py | 2 + libcudacxx/share/libcudacxx/gdb/optional.py | 77 +++++++++++++++ libcudacxx/share/libcudacxx/lldb/__init__.py | 2 + libcudacxx/share/libcudacxx/lldb/optional.py | 96 ++++++++++++++++++ libcudacxx/test/debugging/CMakeLists.txt | 1 + .../test/debugging/optional/CMakeLists.txt | 17 ++++ .../test/debugging/optional/gdb.expected | 49 ++++++++++ .../test/debugging/optional/lldb.expected | 46 +++++++++ libcudacxx/test/debugging/optional/source.cu | 97 +++++++++++++++++++ 9 files changed, 387 insertions(+) create mode 100644 libcudacxx/share/libcudacxx/gdb/optional.py create mode 100644 libcudacxx/share/libcudacxx/lldb/optional.py create mode 100644 libcudacxx/test/debugging/optional/CMakeLists.txt create mode 100644 libcudacxx/test/debugging/optional/gdb.expected create mode 100644 libcudacxx/test/debugging/optional/lldb.expected create mode 100644 libcudacxx/test/debugging/optional/source.cu diff --git a/libcudacxx/share/libcudacxx/gdb/__init__.py b/libcudacxx/share/libcudacxx/gdb/__init__.py index e8e9c8fa2f96..5873b4ede95f 100644 --- a/libcudacxx/share/libcudacxx/gdb/__init__.py +++ b/libcudacxx/share/libcudacxx/gdb/__init__.py @@ -27,6 +27,7 @@ import mdspan # noqa: E402 import memory_pool # noqa: E402 import memory_resource # noqa: E402 +import optional # noqa: E402 import span # noqa: E402 import std_array # noqa: E402 import stream # noqa: E402 @@ -46,6 +47,7 @@ mdspan, memory_pool, span, + optional, ) diff --git a/libcudacxx/share/libcudacxx/gdb/optional.py b/libcudacxx/share/libcudacxx/gdb/optional.py new file mode 100644 index 000000000000..258e00184da0 --- /dev/null +++ b/libcudacxx/share/libcudacxx/gdb/optional.py @@ -0,0 +1,77 @@ +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +"""GDB pretty printer for cuda::std::optional.""" + +from __future__ import annotations + +from collections.abc import Iterator +from types import ModuleType + +import cccl_common + +import gdb +import gdb.printing + + +def _is_cuda_optional(value_type: gdb.Type) -> bool: + value_type = cccl_common.canonical_type(value_type) + template_name = cccl_common.template_name(cccl_common.public_type_name(value_type)) + return template_name == "cuda::std::optional" + + +def _has_field(value_type: gdb.Type, name: str) -> bool: + return any(field.name == name for field in value_type.fields()) + + +class OptionalPrinter: + """Expose the contained value of a cuda::std::optional to GDB.""" + + def __init__(self, value: gdb.Value) -> None: + value = cccl_common.strip_reference_value(value) + self.value = value + self.type = cccl_common.canonical_type(value.type) + self.type_name = cccl_common.public_type_name(self.type) + # optional stores a pointer that is null when disengaged; every other + # specialization keeps an engaged flag next to a union payload. + self.pointer = ( + self.value["__value_"] if _has_field(self.type, "__value_") else None + ) + if self.pointer is not None: + self.engaged = int(self.pointer) != 0 + else: + self.engaged = bool(self.value["__engaged_"]) + + def children(self) -> Iterator[tuple[str, gdb.Value]]: + # The payload is only read once the engaged state says it holds a value; + # a disengaged optional may still carry the bytes of a previous value. + if not self.engaged: + return + if self.pointer is not None: + yield "[contained value]", self.pointer.dereference() + else: + yield "[contained value]", self.value["__storage_"]["__val_"] + + def to_string(self) -> str: + # Match the libstdc++ std::optional printer: "T [no contained value]" when empty. + if self.engaged: + return self.type_name + return f"{self.type_name} [no contained value]" + + +class OptionalPrinterLookup(gdb.printing.PrettyPrinter): + """Select the cuda::std::optional printer by its public class name.""" + + def __init__(self) -> None: + super().__init__("cuda::std::optional") + + def __call__(self, value: gdb.Value) -> OptionalPrinter | None: + if _is_cuda_optional(value.type): + return OptionalPrinter(value) + return None + + +def register(objfile: ModuleType) -> None: + """Register the cuda::std::optional printer with GDB.""" + gdb.printing.register_pretty_printer(objfile, OptionalPrinterLookup(), replace=True) diff --git a/libcudacxx/share/libcudacxx/lldb/__init__.py b/libcudacxx/share/libcudacxx/lldb/__init__.py index 0a9751b7ce2c..6ad28e0a819c 100644 --- a/libcudacxx/share/libcudacxx/lldb/__init__.py +++ b/libcudacxx/share/libcudacxx/lldb/__init__.py @@ -18,6 +18,7 @@ import mdspan import memory_pool import memory_resource +import optional import span import std_array import stream @@ -40,6 +41,7 @@ mdspan, memory_pool, span, + optional, ) InternalDict = dict[str, object] diff --git a/libcudacxx/share/libcudacxx/lldb/optional.py b/libcudacxx/share/libcudacxx/lldb/optional.py new file mode 100644 index 000000000000..6324a127c865 --- /dev/null +++ b/libcudacxx/share/libcudacxx/lldb/optional.py @@ -0,0 +1,96 @@ +# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +"""LLDB pretty printer for cuda::std::optional.""" + +from __future__ import annotations + +import re + +import cccl_common + +import lldb + +_OPTIONAL_PATTERN = re.compile(r"^cuda::std::optional<.+>$") +InternalDict = dict[str, object] + + +def is_cuda_optional(value_type: lldb.SBType, _internal_dict: InternalDict) -> bool: + type_name = cccl_common.canonical_type_name(value_type) + return _OPTIONAL_PATTERN.fullmatch(type_name) is not None + + +def _is_engaged(value: lldb.SBValue) -> bool: + """Return whether an optional holds a value, without touching the payload.""" + # optional stores a pointer that is null when disengaged; every other + # specialization keeps an engaged flag next to a union payload. + pointer = value.GetChildMemberWithName("__value_") + if pointer.IsValid(): + return pointer.GetValueAsUnsigned(0) != 0 + return value.GetChildMemberWithName("__engaged_").GetValueAsUnsigned(0) != 0 + + +def optional_summary(value: lldb.SBValue, _internal_dict: InternalDict) -> str: + """Summarize the engaged state like LLDB's std::optional formatter.""" + value = cccl_common.strip_reference_value(value).GetNonSyntheticValue() + return f"Has Value={'true' if _is_engaged(value) else 'false'}" + + +class OptionalSyntheticProvider: + """Expose the contained value of a cuda::std::optional as an LLDB child.""" + + def __init__(self, value: lldb.SBValue, _internal_dict: InternalDict) -> None: + value = cccl_common.strip_reference_value(value) + self.value = value.GetNonSyntheticValue() + self.update() + + def update(self) -> bool: + self.engaged = _is_engaged(self.value) + return True + + def num_children(self) -> int: + return 1 if self.engaged else 0 + + def has_children(self) -> bool: + return self.engaged + + def get_child_index(self, name: str) -> int: + return 0 if name == "Value" else -1 + + def get_child_at_index(self, index: int) -> lldb.SBValue | None: + # The payload is only read once the engaged state says it holds a value; + # a disengaged optional may still carry the bytes of a previous value. + if index != 0 or not self.engaged: + return None + pointer = self.value.GetChildMemberWithName("__value_") + if pointer.IsValid(): + return pointer.Dereference().Clone("Value") + # The union member's declared type is the ``remove_cv_t`` typedef, which + # leaves the value formatted under an unresolved alias (and lets a nested + # optional's leaf inherit a stray summary). Recover the real value type from + # the class template argument and read the payload through that instead. + value_type = ( + self.value.GetType() + .GetCanonicalType() + .GetUnqualifiedType() + .GetTemplateArgumentType(0) + ) + val = self.value.GetChildMemberWithName("__storage_").GetChildMemberWithName( + "__val_" + ) + if value_type.IsValid(): + val = val.Cast(value_type) + return val.Clone("Value") + + +def register(debugger: lldb.SBDebugger, category: str, module: str) -> None: + """Register the cuda::std::optional formatter in an LLDB category.""" + debugger.HandleCommand( + f"type summary add --category {category} --expand --python-function {module}.optional_summary " + f"--recognizer-function {module}.is_cuda_optional" + ) + debugger.HandleCommand( + f"type synthetic add --category {category} --python-class {module}.OptionalSyntheticProvider " + f"--recognizer-function {module}.is_cuda_optional" + ) diff --git a/libcudacxx/test/debugging/CMakeLists.txt b/libcudacxx/test/debugging/CMakeLists.txt index 523a430ed419..739e078fdd2f 100644 --- a/libcudacxx/test/debugging/CMakeLists.txt +++ b/libcudacxx/test/debugging/CMakeLists.txt @@ -227,6 +227,7 @@ if (CUDAToolkit_VERSION VERSION_GREATER_EQUAL 13.0) add_subdirectory(memory_pool_managed) endif() add_subdirectory(memory_resource) +add_subdirectory(optional) add_subdirectory(span) add_subdirectory(stream) add_subdirectory(tuple) diff --git a/libcudacxx/test/debugging/optional/CMakeLists.txt b/libcudacxx/test/debugging/optional/CMakeLists.txt new file mode 100644 index 000000000000..6530094fac08 --- /dev/null +++ b/libcudacxx/test/debugging/optional/CMakeLists.txt @@ -0,0 +1,17 @@ +libcudacxx_add_pretty_printer_test( + NAME optional + SOURCES source.cu + CASES + # gersemi: off + --case inspect_engaged optional.engaged values + --case inspect_disengaged optional.disengaged values + --case inspect_nested optional.nested values + --case inspect_nested_disengaged optional.nested_disengaged values + --case inspect_non_trivial optional.non_trivial values + --case inspect_alias optional.alias values + --case inspect_reference optional.reference values + --case inspect_reference_disengaged optional.reference_disengaged values + --case inspect_before_update optional.update.before values + --case inspect_after_update optional.update.after values + # gersemi: on +) diff --git a/libcudacxx/test/debugging/optional/gdb.expected b/libcudacxx/test/debugging/optional/gdb.expected new file mode 100644 index 000000000000..f9dc303e4254 --- /dev/null +++ b/libcudacxx/test/debugging/optional/gdb.expected @@ -0,0 +1,49 @@ +=============== optional.engaged begin =============== +cuda::std::optional = { + [contained value] = 42 +} +=============== optional.engaged end =============== +=============== optional.disengaged begin =============== +cuda::std::optional [no contained value] +=============== optional.disengaged end =============== +=============== optional.nested begin =============== +cuda::std::optional> = { + [contained value] = cuda::std::optional = { + [contained value] = 7 + } +} +=============== optional.nested end =============== +=============== optional.nested_disengaged begin =============== +cuda::std::optional> = { + [contained value] = cuda::std::optional [no contained value] +} +=============== optional.nested_disengaged end =============== +=============== optional.non_trivial begin =============== +cuda::std::optional = { + [contained value] = { + id = 3, + weight = 2.5 + } +} +=============== optional.non_trivial end =============== +=============== optional.alias begin =============== +cuda::std::optional = { + [contained value] = -8 +} +=============== optional.alias end =============== +=============== optional.reference begin =============== +cuda::std::optional = { + [contained value] = 11 +} +=============== optional.reference end =============== +=============== optional.reference_disengaged begin =============== +cuda::std::optional [no contained value] +=============== optional.reference_disengaged end =============== +=============== optional.update.before begin =============== +cuda::std::optional = { + [contained value] = 99 +} +=============== optional.update.before end =============== +=============== optional.update.after begin =============== +cuda::std::optional [no contained value] +=============== optional.update.after end =============== diff --git a/libcudacxx/test/debugging/optional/lldb.expected b/libcudacxx/test/debugging/optional/lldb.expected new file mode 100644 index 000000000000..d2efe90f543a --- /dev/null +++ b/libcudacxx/test/debugging/optional/lldb.expected @@ -0,0 +1,46 @@ +=============== optional.engaged begin =============== +(const cuda::std::optional &)
Has Value=true: { + Value = 42 +} +=============== optional.engaged end =============== +=============== optional.disengaged begin =============== +(const cuda::std::optional &)
Has Value=false +=============== optional.disengaged end =============== +=============== optional.nested begin =============== +(const cuda::std::optional> &)
Has Value=true: { + Value = Has Value=true { + Value = 7 + } +} +=============== optional.nested end =============== +=============== optional.nested_disengaged begin =============== +(const cuda::std::optional> &)
Has Value=true: { + Value = Has Value=false +} +=============== optional.nested_disengaged end =============== +=============== optional.non_trivial begin =============== +(const cuda::std::optional &)
Has Value=true: { + Value = (id = 3, weight = 2.5) +} +=============== optional.non_trivial end =============== +=============== optional.alias begin =============== +(const optional_alias &)
Has Value=true: { + Value = -8 +} +=============== optional.alias end =============== +=============== optional.reference begin =============== +(const cuda::std::optional &)
Has Value=true: { + Value = 11 +} +=============== optional.reference end =============== +=============== optional.reference_disengaged begin =============== +(const cuda::std::optional &)
Has Value=false +=============== optional.reference_disengaged end =============== +=============== optional.update.before begin =============== +(const cuda::std::optional &)
Has Value=true: { + Value = 99 +} +=============== optional.update.before end =============== +=============== optional.update.after begin =============== +(const cuda::std::optional &)
Has Value=false +=============== optional.update.after end =============== diff --git a/libcudacxx/test/debugging/optional/source.cu b/libcudacxx/test/debugging/optional/source.cu new file mode 100644 index 000000000000..efd7dd225ff3 --- /dev/null +++ b/libcudacxx/test/debugging/optional/source.cu @@ -0,0 +1,97 @@ +// Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +#include + +// Give the inspected parameter a stack location that survives optimization, so the +// debugger can read it in this frame. Without this the parameter stays in a +// caller-clobbered register and reads as unavailable at -O3. +#define KEEP_FOR_DEBUGGER(values) asm volatile("" : : "g"(&(values)) : "memory") + +using optional_alias = cuda::std::optional; + +// A payload with a user-provided destructor selects the non-trivial storage base. +struct non_trivial +{ + int id; + double weight; + ~non_trivial() {} +}; + +[[gnu::noinline]] void inspect_engaged(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_disengaged(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_nested(const cuda::std::optional>& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_nested_disengaged(const cuda::std::optional>& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_non_trivial(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_alias(const optional_alias& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_reference(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_reference_disengaged(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_before_update(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +[[gnu::noinline]] void inspect_after_update(const cuda::std::optional& values) +{ + KEEP_FOR_DEBUGGER(values); +} + +int main() +{ + const cuda::std::optional engaged = 42; + const cuda::std::optional disengaged = cuda::std::nullopt; + const cuda::std::optional> nested = cuda::std::optional{7}; + const cuda::std::optional> nested_disengaged = cuda::std::optional{}; + const cuda::std::optional non_trivial_value = non_trivial{3, 2.5}; + const optional_alias alias = -8; + int referenced = 11; + const cuda::std::optional reference(referenced); + const cuda::std::optional reference_disengaged{}; + cuda::std::optional updated = 99; + + inspect_engaged(engaged); + inspect_disengaged(disengaged); + inspect_nested(nested); + inspect_nested_disengaged(nested_disengaged); + inspect_non_trivial(non_trivial_value); + inspect_alias(alias); + inspect_reference(reference); + inspect_reference_disengaged(reference_disengaged); + inspect_before_update(updated); + // The payload storage keeps its old bytes after reset(); only the engaged flag changes. + updated.reset(); + inspect_after_update(updated); +} From 958dcd2b3539f67002bfdef5ee6277e1dca30cff Mon Sep 17 00:00:00 2001 From: theo-ai-lab <217547998+theo-ai-lab@users.noreply.github.com> Date: Mon, 17 Aug 2026 05:12:07 -0700 Subject: [PATCH 2/2] Make GDB optional detection robust to inline ABI namespaces Match the array/tuple detection pattern: check the ABI-stripped name prefix and the last template-name component instead of an exact match, so detection survives namespace forms the strip pattern doesn't know. --- libcudacxx/share/libcudacxx/gdb/optional.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/libcudacxx/share/libcudacxx/gdb/optional.py b/libcudacxx/share/libcudacxx/gdb/optional.py index 258e00184da0..592538969574 100644 --- a/libcudacxx/share/libcudacxx/gdb/optional.py +++ b/libcudacxx/share/libcudacxx/gdb/optional.py @@ -17,8 +17,12 @@ def _is_cuda_optional(value_type: gdb.Type) -> bool: value_type = cccl_common.canonical_type(value_type) - template_name = cccl_common.template_name(cccl_common.public_type_name(value_type)) - return template_name == "cuda::std::optional" + public_name = cccl_common.public_type_name(value_type) + template_name = cccl_common.template_name(value_type) + return ( + public_name.startswith("cuda::std::") + and template_name.rsplit("::", 1)[-1] == "optional" + ) def _has_field(value_type: gdb.Type, name: str) -> bool: