diff --git a/clang/docs/tools/clang-formatted-files.txt b/clang/docs/tools/clang-formatted-files.txt --- a/clang/docs/tools/clang-formatted-files.txt +++ b/clang/docs/tools/clang-formatted-files.txt @@ -4180,6 +4180,8 @@ lldb/source/Plugins/Language/ClangCommon/ClangHighlighter.h lldb/source/Plugins/Language/CPlusPlus/BlockPointer.cpp lldb/source/Plugins/Language/CPlusPlus/BlockPointer.h +lldb/source/Plugins/Language/CPlusPlus/Coroutines.cpp +lldb/source/Plugins/Language/CPlusPlus/Coroutines.h lldb/source/Plugins/Language/CPlusPlus/CPlusPlusLanguage.h lldb/source/Plugins/Language/CPlusPlus/CPlusPlusNameParser.h lldb/source/Plugins/Language/CPlusPlus/CxxStringTypes.h diff --git a/lldb/packages/Python/lldbsuite/test/lldbtest.py b/lldb/packages/Python/lldbsuite/test/lldbtest.py --- a/lldb/packages/Python/lldbsuite/test/lldbtest.py +++ b/lldb/packages/Python/lldbsuite/test/lldbtest.py @@ -292,8 +292,12 @@ test_base.assertEqual(self.expect_type, val.GetDisplayTypeName(), this_error_msg) if self.expect_summary: - test_base.assertEqual(self.expect_summary, val.GetSummary(), - this_error_msg) + if isinstance(self.expect_summary, re.Pattern): + test_base.assertRegex(val.GetSummary(), self.expect_summary, + this_error_msg) + else: + test_base.assertEqual(self.expect_summary, val.GetSummary(), + this_error_msg) if self.children is not None: self.check_value_children(test_base, val, error_msg) diff --git a/lldb/packages/Python/lldbsuite/test/lldbutil.py b/lldb/packages/Python/lldbsuite/test/lldbutil.py --- a/lldb/packages/Python/lldbsuite/test/lldbutil.py +++ b/lldb/packages/Python/lldbsuite/test/lldbutil.py @@ -988,6 +988,21 @@ return get_threads_stopped_at_breakpoint(process, bkpt) +def continue_to_source_breakpoint(test, process, bkpt_pattern, source_spec): + """ + Sets a breakpoint set by source regex bkpt_pattern, continues the process, and deletes the breakpoint again. + Otherwise the same as `continue_to_breakpoint` + """ + breakpoint = process.target.BreakpointCreateBySourceRegex( + bkpt_pattern, source_spec, None) + test.assertTrue(breakpoint.GetNumLocations() > 0, + 'No locations found for source breakpoint: "%s", file: "%s", dir: "%s"' + %(bkpt_pattern, source_spec.GetFilename(), source_spec.GetDirectory())) + stopped_threads = continue_to_breakpoint(process, breakpoint) + process.target.BreakpointDelete(breakpoint.GetID()) + return stopped_threads + + def get_caller_symbol(thread): """ Returns the symbol name for the call site of the leaf function. diff --git a/lldb/source/Plugins/Language/CPlusPlus/CMakeLists.txt b/lldb/source/Plugins/Language/CPlusPlus/CMakeLists.txt --- a/lldb/source/Plugins/Language/CPlusPlus/CMakeLists.txt +++ b/lldb/source/Plugins/Language/CPlusPlus/CMakeLists.txt @@ -1,5 +1,6 @@ add_lldb_library(lldbPluginCPlusPlusLanguage PLUGIN BlockPointer.cpp + Coroutines.cpp CPlusPlusLanguage.cpp CPlusPlusNameParser.cpp CxxStringTypes.cpp diff --git a/lldb/source/Plugins/Language/CPlusPlus/CPlusPlusLanguage.cpp b/lldb/source/Plugins/Language/CPlusPlus/CPlusPlusLanguage.cpp --- a/lldb/source/Plugins/Language/CPlusPlus/CPlusPlusLanguage.cpp +++ b/lldb/source/Plugins/Language/CPlusPlus/CPlusPlusLanguage.cpp @@ -35,6 +35,7 @@ #include "BlockPointer.h" #include "CPlusPlusNameParser.h" +#include "Coroutines.h" #include "CxxStringTypes.h" #include "Generic.h" #include "LibCxx.h" @@ -796,6 +797,14 @@ ConstString("^std::__[[:alnum:]]+::function<.+>$"), stl_summary_flags, true); + ConstString libcxx_std_coroutine_handle_regex( + "^std::__[[:alnum:]]+::coroutine_handle<.+>(( )?&)?$"); + AddCXXSynthetic( + cpp_category_sp, + lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEndCreator, + "coroutine_handle synthetic children", libcxx_std_coroutine_handle_regex, + stl_deref_flags, true); + stl_summary_flags.SetDontShowChildren(false); stl_summary_flags.SetSkipPointers(false); AddCXXSummary(cpp_category_sp, @@ -898,6 +907,11 @@ "libc++ std::unique_ptr summary provider", libcxx_std_unique_ptr_regex, stl_summary_flags, true); + AddCXXSummary(cpp_category_sp, + lldb_private::formatters::StdlibCoroutineHandleSummaryProvider, + "libc++ std::coroutine_handle summary provider", + libcxx_std_coroutine_handle_regex, stl_summary_flags, true); + AddCXXSynthetic( cpp_category_sp, lldb_private::formatters::LibCxxVectorIteratorSyntheticFrontEndCreator, @@ -1122,6 +1136,14 @@ "std::tuple synthetic children", ConstString("^std::tuple<.+>(( )?&)?$"), stl_synth_flags, true); + ConstString libstdcpp_std_coroutine_handle_regex( + "^std::coroutine_handle<.+>(( )?&)?$"); + AddCXXSynthetic( + cpp_category_sp, + lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEndCreator, + "std::coroutine_handle synthetic children", + libstdcpp_std_coroutine_handle_regex, stl_deref_flags, true); + AddCXXSynthetic( cpp_category_sp, lldb_private::formatters::LibStdcppBitsetSyntheticFrontEndCreator, @@ -1149,6 +1171,10 @@ "libstdc++ std::weak_ptr summary provider", ConstString("^std::weak_ptr<.+>(( )?&)?$"), stl_summary_flags, true); + AddCXXSummary(cpp_category_sp, + lldb_private::formatters::StdlibCoroutineHandleSummaryProvider, + "libstdc++ std::coroutine_handle summary provider", + libstdcpp_std_coroutine_handle_regex, stl_summary_flags, true); AddCXXSummary( cpp_category_sp, lldb_private::formatters::GenericOptionalSummaryProvider, "libstd++ std::optional summary provider", diff --git a/lldb/source/Plugins/Language/CPlusPlus/Coroutines.h b/lldb/source/Plugins/Language/CPlusPlus/Coroutines.h new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Language/CPlusPlus/Coroutines.h @@ -0,0 +1,57 @@ +//===-- Coroutines.h --------------------------------------------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_PLUGINS_LANGUAGE_CPLUSPLUS_COROUTINES_H +#define LLDB_SOURCE_PLUGINS_LANGUAGE_CPLUSPLUS_COROUTINES_H + +#include "lldb/Core/ValueObject.h" +#include "lldb/DataFormatters/TypeSummary.h" +#include "lldb/DataFormatters/TypeSynthetic.h" +#include "lldb/Utility/Stream.h" + +namespace lldb_private { +namespace formatters { + +/// Summary provider for `std::coroutine_handle` from libc++, libstdc++ and +/// MSVC STL. +bool StdlibCoroutineHandleSummaryProvider(ValueObject &valobj, Stream &stream, + const TypeSummaryOptions &options); + +/// Synthetic children frontend for `std::coroutine_handle` from +/// libc++, libstdc++ and MSVC STL. Shows the compiler-generated `resume` and +/// `destroy` function pointers as well as the `promise`, if the promise type +/// is `promise_type != void`. +class StdlibCoroutineHandleSyntheticFrontEnd + : public SyntheticChildrenFrontEnd { +public: + StdlibCoroutineHandleSyntheticFrontEnd(lldb::ValueObjectSP valobj_sp); + + ~StdlibCoroutineHandleSyntheticFrontEnd() override; + + size_t CalculateNumChildren() override; + + lldb::ValueObjectSP GetChildAtIndex(size_t idx) override; + + bool Update() override; + + bool MightHaveChildren() override; + + size_t GetIndexOfChildWithName(ConstString name) override; + +private: + lldb::ValueObjectSP m_frame_ptr_sp; +}; + +SyntheticChildrenFrontEnd * +StdlibCoroutineHandleSyntheticFrontEndCreator(CXXSyntheticChildren *, + lldb::ValueObjectSP); + +} // namespace formatters +} // namespace lldb_private + +#endif // LLDB_SOURCE_PLUGINS_LANGUAGE_CPLUSPLUS_COROUTINES_H diff --git a/lldb/source/Plugins/Language/CPlusPlus/Coroutines.cpp b/lldb/source/Plugins/Language/CPlusPlus/Coroutines.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Plugins/Language/CPlusPlus/Coroutines.cpp @@ -0,0 +1,137 @@ +//===-- Coroutines.cpp ----------------------------------------------------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "Coroutines.h" + +#include "Plugins/TypeSystem/Clang/TypeSystemClang.h" + +using namespace lldb; +using namespace lldb_private; +using namespace lldb_private::formatters; + +static ValueObjectSP GetCoroFramePtrFromHandle(ValueObject &valobj) { + ValueObjectSP valobj_sp(valobj.GetNonSyntheticValue()); + if (!valobj_sp) + return nullptr; + + // We expect a single pointer in the `coroutine_handle` class. + // We don't care about its name. + if (valobj_sp->GetNumChildren() != 1) + return nullptr; + ValueObjectSP ptr_sp(valobj_sp->GetChildAtIndex(0, true)); + if (!ptr_sp) + return nullptr; + if (!ptr_sp->GetCompilerType().IsPointerType()) + return nullptr; + + return ptr_sp; +} + +static CompilerType GetCoroutineFrameType(TypeSystemClang &ast_ctx, + CompilerType promise_type) { + CompilerType void_type = ast_ctx.GetBasicType(lldb::eBasicTypeVoid); + CompilerType coro_func_type = ast_ctx.CreateFunctionType( + /*result_type=*/void_type, /*args=*/&void_type, /*num_args=*/1, + /*is_variadic=*/false, /*qualifiers=*/0); + CompilerType coro_abi_type; + if (promise_type.IsVoidType()) { + coro_abi_type = ast_ctx.CreateStructForIdentifier( + ConstString(), {{"resume", coro_func_type.GetPointerType()}, + {"destroy", coro_func_type.GetPointerType()}}); + } else { + coro_abi_type = ast_ctx.CreateStructForIdentifier( + ConstString(), {{"resume", coro_func_type.GetPointerType()}, + {"destroy", coro_func_type.GetPointerType()}, + {"promise", promise_type}}); + } + return coro_abi_type; +} + +bool lldb_private::formatters::StdlibCoroutineHandleSummaryProvider( + ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) { + ValueObjectSP ptr_sp(GetCoroFramePtrFromHandle(valobj)); + if (!ptr_sp) + return false; + + stream.Printf("coro frame = 0x%" PRIx64, ptr_sp->GetValueAsUnsigned(0)); + return true; +} + +lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd:: + StdlibCoroutineHandleSyntheticFrontEnd(lldb::ValueObjectSP valobj_sp) + : SyntheticChildrenFrontEnd(*valobj_sp) { + if (valobj_sp) + Update(); +} + +lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd:: + ~StdlibCoroutineHandleSyntheticFrontEnd() = default; + +size_t lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd:: + CalculateNumChildren() { + if (!m_frame_ptr_sp) + return 0; + + return m_frame_ptr_sp->GetNumChildren(); +} + +lldb::ValueObjectSP lldb_private::formatters:: + StdlibCoroutineHandleSyntheticFrontEnd::GetChildAtIndex(size_t idx) { + if (!m_frame_ptr_sp) + return lldb::ValueObjectSP(); + + return m_frame_ptr_sp->GetChildAtIndex(idx, true); +} + +bool lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd:: + Update() { + m_frame_ptr_sp.reset(); + + ValueObjectSP valobj_sp = m_backend.GetSP(); + if (!valobj_sp) + return false; + + ValueObjectSP ptr_sp(GetCoroFramePtrFromHandle(m_backend)); + if (!ptr_sp) + return false; + + TypeSystemClang *ast_ctx = llvm::dyn_cast_or_null( + valobj_sp->GetCompilerType().GetTypeSystem()); + if (!ast_ctx) + return false; + + CompilerType promise_type( + valobj_sp->GetCompilerType().GetTypeTemplateArgument(0)); + if (!promise_type) + return false; + CompilerType coro_frame_type = GetCoroutineFrameType(*ast_ctx, promise_type); + + m_frame_ptr_sp = ptr_sp->Cast(coro_frame_type.GetPointerType()); + + return false; +} + +bool lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd:: + MightHaveChildren() { + return true; +} + +size_t StdlibCoroutineHandleSyntheticFrontEnd::GetIndexOfChildWithName( + ConstString name) { + if (!m_frame_ptr_sp) + return UINT32_MAX; + + return m_frame_ptr_sp->GetIndexOfChildWithName(name); +} + +SyntheticChildrenFrontEnd * +lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEndCreator( + CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) { + return (valobj_sp ? new StdlibCoroutineHandleSyntheticFrontEnd(valobj_sp) + : nullptr); +} diff --git a/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/Makefile b/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/Makefile new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/Makefile @@ -0,0 +1,4 @@ +CXX_SOURCES := main.cpp +CFLAGS_EXTRAS := -std=c++20 + +include Makefile.rules diff --git a/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/TestCoroutineHandle.py b/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/TestCoroutineHandle.py new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/TestCoroutineHandle.py @@ -0,0 +1,79 @@ +""" +Test lldb data formatter subsystem. +""" + + + +import lldb +from lldbsuite.test.decorators import * +from lldbsuite.test.lldbtest import * +from lldbsuite.test import lldbutil + +USE_LIBSTDCPP = "USE_LIBSTDCPP" +USE_LIBCPP = "USE_LIBCPP" + +class TestCoroutineHandle(TestBase): + def do_test(self, stdlib_type): + """Test std::coroutine_handle is displayed correctly.""" + self.build(dictionary={stdlib_type: "1"}) + + test_generator_func_ptr_re = re.compile( + r"^\(a.out`my_generator_func\(\) at main.cpp:[0-9]*\)$") + + # Run until the initial suspension point + lldbutil.run_to_source_breakpoint(self, '// Break at initial_suspend', + lldb.SBFileSpec("main.cpp", False)) + # Check that we show the correct function pointers and the `promise`. + self.expect_expr("gen.hdl", + result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"), + result_children=[ + ValueCheck(name="resume", summary = test_generator_func_ptr_re), + ValueCheck(name="destroy", summary = test_generator_func_ptr_re), + ValueCheck(name="promise", children=[ + ValueCheck(name="current_value", value = "-1"), + ]) + ]) + # For type-erased `coroutine_handle<>` we are missing the `promise` + # but still show `resume` and `destroy`. + self.expect_expr("type_erased_hdl", + result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"), + result_children=[ + ValueCheck(name="resume", summary = test_generator_func_ptr_re), + ValueCheck(name="destroy", summary = test_generator_func_ptr_re), + ]) + + # Run until after the `co_yield` + process = self.process() + lldbutil.continue_to_source_breakpoint(self, process, + '// Break after co_yield', lldb.SBFileSpec("main.cpp", False)) + # We correctly show the updated value inside `prommise.current_value`. + self.expect_expr("gen.hdl", + result_children=[ + ValueCheck(name="resume", summary = test_generator_func_ptr_re), + ValueCheck(name="destroy", summary = test_generator_func_ptr_re), + ValueCheck(name="promise", children=[ + ValueCheck(name="current_value", value = "42"), + ]) + ]) + + # Run until the `final_suspend` + lldbutil.continue_to_source_breakpoint(self, process, + '// Break at final_suspend', lldb.SBFileSpec("main.cpp", False)) + # At the final suspension point, `resume` is set to a nullptr. + # Check that we still show the remaining data correctly. + self.expect_expr("gen.hdl", + result_children=[ + ValueCheck(name="resume", value = "0x0000000000000000"), + ValueCheck(name="destroy", summary = test_generator_func_ptr_re), + ValueCheck(name="promise", children=[ + ValueCheck(name="current_value", value = "42"), + ]) + ]) + + @add_test_categories(["libstdcxx"]) + def test_libstdcpp(self): + self.do_test(USE_LIBSTDCPP) + + @add_test_categories(["libc++"]) + def test_libcpp(self): + self.do_test(USE_LIBCPP) diff --git a/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/main.cpp b/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/main.cpp new file mode 100644 --- /dev/null +++ b/lldb/test/API/functionalities/data-formatter/data-formatter-stl/generic/coroutine_handle/main.cpp @@ -0,0 +1,40 @@ +#include + +// `int_generator` is a stripped down, minimal coroutine generator +// type. +struct int_generator { + struct promise_type { + int current_value = -1; + + auto get_return_object() { + return std::coroutine_handle::from_promise(*this); + } + auto initial_suspend() { return std::suspend_always(); } + auto final_suspend() noexcept { return std::suspend_always(); } + auto return_void() { return std::suspend_always(); } + void unhandled_exception() { __builtin_unreachable(); } + auto yield_value(int v) { + current_value = v; + return std::suspend_always(); + } + }; + + std::coroutine_handle hdl; + + int_generator(std::coroutine_handle h) : hdl(h) {} + ~int_generator() { hdl.destroy(); } +}; + +int_generator my_generator_func() { co_yield 42; } + +// This is an empty function which we call just so the debugger has +// a place to reliably set a breakpoint on. +void empty_function_so_we_can_set_a_breakpoint() {} + +int main() { + int_generator gen = my_generator_func(); + std::coroutine_handle<> type_erased_hdl = gen.hdl; + gen.hdl.resume(); // Break at initial_suspend + gen.hdl.resume(); // Break after co_yield + empty_function_so_we_can_set_a_breakpoint(); // Break at final_suspend +}