From 2509fb842f4d19df24560894aa7cf7849e7a6a3a Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Tue, 11 Aug 2026 23:37:55 -0400 Subject: [PATCH] get_completion_signatures_type get_completion_signatures attempts to leverage compile-time exceptions to report errors. It does this by: - Throwing an exception when it cannot synthesize completion signatures (definitionally this happens at compile time because it's consteval), and - Reporting that it returns completion_signatures<> in situations wherein it would throw an exception The latter is reflective of a tension: get_completion_signatures "knows" it will throw an exception when the return type is computed, but that exception is not surfaced until and unless the function is invoked. This creates the opportunity for very interesting bugs, for example decltype( get_completion_signatures<...>()) is, in a vacuum, likely to be a bug because this doesn't actually evaluate the expression and therefore any exception is not thrown. The solution is complete surrender to the invocation of functions at compile time (rather than exposing information via channels which can be interrogated via an unevaluated context): Reflection. Added get_completion_signatures_type which returns std::meta::info and provides a reflection-first way to access completion signatures computation. In addition to ameliorating the above-described tension in the design of get_completion_signatures this new function allows for better ergonomics in reflection-first uses. When attempting to use reflection to aid in the computation of a certain asynchronous operation's completion signatures one needs to, in general: - Obtain the completion signatures of one or more child operations - Examine and manipulate those completion signatures - Yield the final completion signatures With get_completion_signatures this becomes awkward. The way to "lift" the completion signatures into the reflection domain is via ^^decltype( get_completion_signatures<...>()), which is pathological as discussed above. In order to expose the needed interface (i.e. a static, consteval member function get_completion_signatures which returns an instance of an instantiation of the completion_signatures class template) one needs to splice a reflection, but one can't splice a reflection unless that reflection is constexpr, but initialization of constexpr variables needs to be done by a constant expression which can't propagate exceptions thereby ruining the entire error reporting mechanism of get_completion_signatures. To make the above concrete consider: template static consteval auto get_completion_signatures() { auto sigs = get_completion_signatures<...>(); auto meta = ^^decltype(sigs); auto final_sigs = /* depends on meta */; return typename [:final_sigs:]{}; } This doesn't compile because final_sigs isn't a constant expression. If we try to adapt: template static consteval auto get_completion_signatures() { auto sigs = get_completion_signatures<...>(); constexpr auto meta = ^^decltype(sigs); constexpr auto final_sigs = /* depends on meta */; return typename [:final_sigs:]{}; } Then any operation in the synthesis of final_sigs which throws an exception fails compilation rather than being propagated. Intuitively we can understand why this is the case: The return type of the function depends thereupon, a problem get_completion_signatures works around by coalescing to the dummy type completion_signatures<>. Also note again that in both of the above examples: auto sigs = get_completion_signatures<...>(); constexpr auto meta = ^^decltype(sigs); Is fragile. It seems as though it could be rewritten as: constexpr auto meta = ^^decltype(get_completion_signatures<...>()); But that would introduce a subtle bug: get_completion_signatures would not actually be evaluated. get_completion_signatures_type ameliorates the above. The problematic member function can be written as: template static consteval std::meta::info get_completion_signatures_type() { auto meta = get_completion_signatures_type<...>(); auto final_sigs = /* depends on meta */; return final_sigs; } --- include/stdexec/__detail/__config.hpp | 6 + .../__detail/__get_completion_signatures.hpp | 119 ++++++++- test/CMakeLists.txt | 1 + .../detail/test_get_completion_signatures.cpp | 248 ++++++++++++++++++ 4 files changed, 371 insertions(+), 3 deletions(-) create mode 100644 test/stdexec/detail/test_get_completion_signatures.cpp diff --git a/include/stdexec/__detail/__config.hpp b/include/stdexec/__detail/__config.hpp index 8539d45ca..1e91584ff 100644 --- a/include/stdexec/__detail/__config.hpp +++ b/include/stdexec/__detail/__config.hpp @@ -617,6 +617,12 @@ namespace STDEXEC # define STDEXEC_NO_STDCPP_PACK_INDEXING() 1 #endif // no pack indexing +#if __cpp_impl_reflection >= 202506L && __cpp_lib_reflection >= 202506L +# define STDEXEC_NO_STDCPP_REFLECTION() 0 +#else +# define STDEXEC_NO_STDCPP_REFLECTION() 1 +#endif + #if STDEXEC_HAS_FEATURE(thread_sanitizer) || defined(__SANITIZE_THREAD__) # define STDEXEC_TSAN() 1 #else diff --git a/include/stdexec/__detail/__get_completion_signatures.hpp b/include/stdexec/__detail/__get_completion_signatures.hpp index b9f55229e..1b3f0f2b0 100644 --- a/include/stdexec/__detail/__get_completion_signatures.hpp +++ b/include/stdexec/__detail/__get_completion_signatures.hpp @@ -35,6 +35,10 @@ import stdexec; # include "__tag_invoke.hpp" # include "__tuple.hpp" // IWYU pragma: keep for __tuple +# if !STDEXEC_NO_STDCPP_REFLECTION() +# include +# endif + # include "__prologue.hpp" namespace STDEXEC @@ -62,6 +66,16 @@ namespace STDEXEC struct _A_GET_COMPLETION_SIGNATURES_CUSTOMIZATION_RETURNED_A_TYPE_THAT_IS_NOT_A_COMPLETION_SIGNATURES_SPECIALIZATION; +# if !STDEXEC_NO_STDCPP_REFLECTION() + STDEXEC_MODULE_EXPORT + template + consteval std::meta::info get_completion_signatures_type(); + + STDEXEC_MODULE_EXPORT + template + consteval std::meta::info get_completion_signatures_type(); +# endif + namespace __cmplsigs { # define STDEXEC_GET_COMPLSIGS(...) \ @@ -162,6 +176,28 @@ namespace STDEXEC template concept __with_co_await = __awaitable<_Sender, __detail::__promise<_Env>...>; +# if !STDEXEC_NO_STDCPP_REFLECTION() + template + concept __has_get_completion_signatures_type = requires { + { + STDEXEC_REMOVE_REFERENCE(_Sender) + ::template get_completion_signatures_type<_Sender, _Env...>() + } -> __std::same_as; + }; + + template + consteval bool __nothrow_get_completion_signatures_type() noexcept + try + { + (void) STDEXEC::get_completion_signatures_type<_Sender, _Env...>(); + return true; + } + catch (...) + { + return false; + } +# endif + template concept __with = __with_legacy_static_member<_Sender, _Env> // || __with_legacy_member<_Sender, _Env> // @@ -170,7 +206,11 @@ namespace STDEXEC || __with_consteval_static_member<_Sender> // || __with_legacy_tag_invoke<_Sender, _Env> // || __with_legacy_non_dependent_tag_invoke<_Sender, _Env> // - || __with_co_await<_Sender, _Env>; + || __with_co_await<_Sender, _Env> +# if !STDEXEC_NO_STDCPP_REFLECTION() + || __has_get_completion_signatures_type<_Sender, _Env> +# endif + ; } // namespace __cmplsigs template @@ -354,7 +394,26 @@ namespace STDEXEC template consteval auto get_completion_signatures() { - return __cmplsigs::__get_completion_signatures_helper<_Sender>(); +# if !STDEXEC_NO_STDCPP_REFLECTION() + if constexpr (__cmplsigs::__has_get_completion_signatures_type<_Sender>) + { + if constexpr (__cmplsigs::__nothrow_get_completion_signatures_type<_Sender>()) + { + constexpr std::meta::info __completions = + STDEXEC::get_completion_signatures_type<_Sender>(); + return typename[:__completions:]{}; + } + else + { + (void) STDEXEC::get_completion_signatures_type<_Sender>(); + return completion_signatures<>{}; + } + } + else +# endif + { + return __cmplsigs::__get_completion_signatures_helper<_Sender>(); + } } //! @brief Overload of @ref get_completion_signatures that takes an @@ -377,9 +436,63 @@ namespace STDEXEC { using __new_sndr_t = transform_sender_result_t<_Sender, _Env>; static_assert(!__merror<__new_sndr_t>); - return __cmplsigs::__get_completion_signatures_helper<__new_sndr_t, _Env>(); +# if !STDEXEC_NO_STDCPP_REFLECTION() + if constexpr (__cmplsigs::__has_get_completion_signatures_type<__new_sndr_t, _Env>) + { + if constexpr (__cmplsigs::__nothrow_get_completion_signatures_type<_Sender, _Env>()) + { + constexpr std::meta::info __completions = + STDEXEC::get_completion_signatures_type<_Sender, _Env>(); + return typename[:__completions:]{}; + } + else + { + (void) STDEXEC::get_completion_signatures_type<_Sender, _Env>(); + return completion_signatures<>{}; + } + } + else +# endif + { + return __cmplsigs::__get_completion_signatures_helper<__new_sndr_t, _Env>(); + } } +# if !STDEXEC_NO_STDCPP_REFLECTION() + STDEXEC_MODULE_EXPORT + template + consteval std::meta::info get_completion_signatures_type() + { + if constexpr (__cmplsigs::__has_get_completion_signatures_type<_Sender>) + { + return STDEXEC_REMOVE_REFERENCE(_Sender)::template get_completion_signatures_type<_Sender>(); + } + else + { + auto __completions = STDEXEC::get_completion_signatures<_Sender>(); + return ^^decltype(__completions); + } + } + + STDEXEC_MODULE_EXPORT + template + consteval std::meta::info get_completion_signatures_type() + { + using __new_sndr_t = transform_sender_result_t<_Sender, _Env>; + static_assert(!__merror<__new_sndr_t>); + if constexpr (__cmplsigs::__has_get_completion_signatures_type<__new_sndr_t, _Env>) + { + return STDEXEC_REMOVE_REFERENCE( + __new_sndr_t)::template get_completion_signatures_type<__new_sndr_t, _Env>(); + } + else + { + auto __completions = STDEXEC::get_completion_signatures<_Sender, _Env>(); + return ^^decltype(__completions); + } + } +# endif + // Legacy interface: STDEXEC_MODULE_EXPORT_AUTHORING template diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2725a8fba..ba57f7a8b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -68,6 +68,7 @@ set(stdexec_test_sources stdexec/detail/test_common_domain.cpp stdexec/detail/test_completion_signatures.cpp stdexec/detail/test_demangle.cpp + stdexec/detail/test_get_completion_signatures.cpp stdexec/detail/test_intrusive_mpsc_queue.cpp stdexec/detail/test_utility.cpp stdexec/queries/test_env.cpp diff --git a/test/stdexec/detail/test_get_completion_signatures.cpp b/test/stdexec/detail/test_get_completion_signatures.cpp new file mode 100644 index 000000000..0d17ea2ca --- /dev/null +++ b/test/stdexec/detail/test_get_completion_signatures.cpp @@ -0,0 +1,248 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + * + * Licensed under the Apache License, Version 2.0 with LLVM Exceptions (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://llvm.org/LICENSE.txt + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include + +#if !STDEXEC_NO_STDCPP_REFLECTION() +# include +#endif + +namespace ex = STDEXEC; + +namespace +{ + using independent_completions = ex::completion_signatures; + using dependent_completions = ex::completion_signatures; + + struct test_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() noexcept + { + if constexpr (sizeof...(Env) == 0) + { + return independent_completions{}; + } + else + { + return dependent_completions{}; + } + } + }; + +#if !STDEXEC_NO_STDCPP_REFLECTION() + template + consteval bool completion_signature_protocols_agree() + { + return std::meta::is_same_type(ex::get_completion_signatures_type(), + ^^decltype(ex::get_completion_signatures())); + } + + template + consteval bool get_completion_signatures_type_throws() noexcept + try + { + (void) ex::get_completion_signatures_type(); + return false; + } + catch (...) + { + return true; + } + + struct throwing_legacy_test_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval auto get_completion_signatures() + { + throw std::meta::exception("Unable to compute completion signatures.", + ^^throwing_legacy_test_sender); + return ex::completion_signatures<>{}; + } + }; + + struct reflection_test_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval std::meta::info get_completion_signatures_type() noexcept + { + if constexpr (sizeof...(Env) == 0) + { + return ^^independent_completions; + } + else + { + return ^^dependent_completions; + } + } + }; + + struct transformed_reflection_test_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval std::meta::info get_completion_signatures_type() noexcept + { + return ^^dependent_completions; + } + }; + + struct throwing_reflection_test_sender + { + using sender_concept = ex::sender_tag; + + template + static consteval std::meta::info get_completion_signatures_type() + { + throw std::meta::exception("Unable to compute completion signatures.", + ^^throwing_reflection_test_sender); + } + }; + + struct reflection_test_domain + { + template + auto transform_sender(ex::start_t, reflection_test_sender, Env const &) const + -> transformed_reflection_test_sender + { + return {}; + } + }; + + using reflection_test_env = ex::prop; + + struct throwing_reflection_test_domain + { + template + auto transform_sender(ex::start_t, reflection_test_sender, Env const &) const + -> throwing_reflection_test_sender + { + return {}; + } + }; + + using throwing_reflection_test_env = ex::prop; +#endif + + TEST_CASE("get_completion_signatures queries a sender without an environment", + "[detail][get_completion_signatures]") + { + STATIC_REQUIRE(std::same_as()), + independent_completions>); +#if !STDEXEC_NO_STDCPP_REFLECTION() + STATIC_REQUIRE(completion_signature_protocols_agree()); +#endif + } + + TEST_CASE("get_completion_signatures queries a sender in an environment", + "[detail][get_completion_signatures]") + { + STATIC_REQUIRE(std::same_as>()), + dependent_completions>); +#if !STDEXEC_NO_STDCPP_REFLECTION() + STATIC_REQUIRE(completion_signature_protocols_agree>()); +#endif + } + + TEST_CASE("get_completion_signatures supports the legacy function-call interface", + "[detail][get_completion_signatures]") + { + STATIC_REQUIRE(std::same_as{})), + dependent_completions>); +#if !STDEXEC_NO_STDCPP_REFLECTION() + STATIC_REQUIRE(completion_signature_protocols_agree>()); +#endif + } + +#if !STDEXEC_NO_STDCPP_REFLECTION() + TEST_CASE("get_completion_signatures_type reflects the legacy query result", + "[detail][get_completion_signatures_type][reflection]") + { + STATIC_REQUIRE(std::meta::is_same_type(ex::get_completion_signatures_type(), + ^^independent_completions)); + STATIC_REQUIRE( + std::meta::is_same_type(ex::get_completion_signatures_type>(), + ^^dependent_completions)); + STATIC_REQUIRE(completion_signature_protocols_agree()); + STATIC_REQUIRE(completion_signature_protocols_agree>()); + } + + TEST_CASE("get_completion_signatures_type invokes the legacy query", + "[detail][get_completion_signatures_type][reflection]") + { + STATIC_REQUIRE(get_completion_signatures_type_throws()); + STATIC_REQUIRE(get_completion_signatures_type_throws>()); + } + + TEST_CASE("detect whether get_completion_signatures_type throws", + "[detail][get_completion_signatures_type][reflection]") + { + STATIC_REQUIRE( + ex::__cmplsigs::__nothrow_get_completion_signatures_type()); + STATIC_REQUIRE_FALSE( + ex::__cmplsigs::__nothrow_get_completion_signatures_type()); + STATIC_REQUIRE_FALSE( + ex::__cmplsigs::__nothrow_get_completion_signatures_type()); + STATIC_REQUIRE( + std::same_as()), + ex::completion_signatures<>>); + STATIC_REQUIRE( + std::same_as()), + ex::completion_signatures<>>); + } + + TEST_CASE("get_completion_signatures_type queries a reflection-native sender without an " + "environment", + "[detail][get_completion_signatures_type][reflection]") + { + STATIC_REQUIRE( + std::meta::is_same_type(ex::get_completion_signatures_type(), + ^^independent_completions)); + STATIC_REQUIRE(completion_signature_protocols_agree()); + } + + TEST_CASE("get_completion_signatures_type queries a reflection-native sender in an environment", + "[detail][get_completion_signatures_type][reflection]") + { + STATIC_REQUIRE(std::meta::is_same_type( + ex::get_completion_signatures_type>(), + ^^dependent_completions)); + STATIC_REQUIRE(completion_signature_protocols_agree>()); + } + + TEST_CASE("get_completion_signatures_type queries the transformed sender", + "[detail][get_completion_signatures_type][reflection]") + { + STATIC_REQUIRE(std::meta::is_same_type( + ex::get_completion_signatures_type(), + ^^dependent_completions)); + STATIC_REQUIRE( + completion_signature_protocols_agree()); + } +#endif +} // namespace