diff --git a/include/stdexec/__detail/__config.hpp b/include/stdexec/__detail/__config.hpp index 5aad8b496..b63b50b8d 100644 --- a/include/stdexec/__detail/__config.hpp +++ b/include/stdexec/__detail/__config.hpp @@ -252,6 +252,12 @@ STDEXEC_NAMESPACE_STD_END // clang-format on //////////////////////////////////////////////////////////////////////////////////////////////////// +#if defined(__cpp_impl_coroutine) && __cpp_impl_coroutine >= 202606L +# define STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() 0 +#else +# define STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() 1 +#endif + #if __cpp_impl_coroutine >= 201902L && __cpp_lib_coroutine >= 201902L # if !STDEXEC_USE_MODULES() // we've already imported std above diff --git a/include/stdexec/__detail/__task.hpp b/include/stdexec/__detail/__task.hpp index 92ce9bfdb..658860cb0 100644 --- a/include/stdexec/__detail/__task.hpp +++ b/include/stdexec/__detail/__task.hpp @@ -46,11 +46,28 @@ STDEXEC_PRAGMA_IGNORE_GNU("-Wmismatched-new-delete") namespace STDEXEC { # if !STDEXEC_NO_STDCPP_COROUTINES() + //////////////////////////////////////////////////////////////////////////////// + // STDEXEC::with_error + template + struct with_error + { + using type = __decay_t<_Error>; + type error; + }; + + template + STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE with_error(_Error) -> with_error<_Error>; + + //////////////////////////////////////////////////////////////////////////////// + // STDEXEC::with_stopped + struct with_stopped + {}; + namespace __task { //////////////////////////////////////////////////////////////////////////////// // A base class for task::promise_type so it can be specialized when _Ty is void: - template + template struct __promise_base { template @@ -59,6 +76,20 @@ namespace STDEXEC __result_.emplace(static_cast<_Value&&>(__value)); } + template + requires (!__std::convertible_to, _Ty>) + constexpr void return_value(with_error<_Error> __error) // + noexcept(noexcept(static_cast<_Promise&>(*this).__set_error(std::move(__error).error))) + { + static_cast<_Promise&>(*this).__set_error(std::move(__error).error); + } + + constexpr void return_value(with_stopped) noexcept + requires (!__std::convertible_to) + { + static_cast<_Promise&>(*this).__set_stopped(); + } + [[nodiscard]] constexpr auto __result() noexcept -> _Ty& { @@ -68,10 +99,25 @@ namespace STDEXEC __optional<_Ty> __result_{}; }; - template <> - struct __promise_base + template + struct __promise_base<_Promise, void> { constexpr void return_void() {} + +# if !STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() + template + constexpr void return_value(with_error<_Error> __error) // + noexcept(noexcept(static_cast<_Promise&>(*this).__set_error(std::move(__error).error))) + { + static_cast<_Promise&>(*this).__set_error(std::move(__error).error); + } + + constexpr void return_value(with_stopped) noexcept + { + static_cast<_Promise&>(*this).__set_stopped(); + } +# endif + constexpr void __result() {} }; @@ -261,18 +307,6 @@ namespace STDEXEC } __throw_error{}; } // namespace __task - //////////////////////////////////////////////////////////////////////////////// - // STDEXEC::with_error - template - struct with_error - { - using type = __decay_t<_Error>; - type error; - }; - - template - STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE with_error(_Error) -> with_error<_Error>; - //////////////////////////////////////////////////////////////////////////////// // STDEXEC::task template > @@ -471,6 +505,7 @@ namespace STDEXEC _TaskEnv __env_; task __task_; __error_variant_t __errors_{__no_init}; + bool __stopped_{}; }; template @@ -527,6 +562,10 @@ namespace STDEXEC [[nodiscard]] auto __completed() noexcept -> __std::coroutine_handle<> final { + if (this->__stopped_) + { + return STDEXEC::__coroutine_unhandled_stopped(this->__handle()); + } this->__reset_callback(); return this->__handle().promise().continuation().handle(); } @@ -535,7 +574,10 @@ namespace STDEXEC auto __canceled() noexcept -> __std::coroutine_handle<> final { this->__reset_callback(); - return this->__handle().promise().continuation().unhandled_stopped(); + auto const __continuation = this->__handle().promise().continuation(); + auto const __coro = std::exchange(this->__task_.__coro_, {}); + STDEXEC::__coroutine_destroy_nothrow(__coro); + return __continuation.unhandled_stopped(); } _ParentPromise& __parent_; @@ -578,7 +620,7 @@ namespace STDEXEC // task::promise_type template struct STDEXEC_ATTRIBUTE(empty_bases) task<_Ty, _TaskEnv>::__promise - : __task::__promise_base<_Ty> + : __task::__promise_base<__promise, _Ty> , with_awaitable_senders<__promise> { __promise() noexcept = default; @@ -620,17 +662,55 @@ namespace STDEXEC } template - constexpr auto yield_value(with_error<_Error> __error) // - noexcept(__nothrow_decay_copyable<_Error>) + static consteval bool __nothrow_error_conversion() { - if constexpr (__mapply<__mcontains<__decay_t<_Error>>, __error_variant_t>::value) + using __is_convertible_error = __mbind_front_q<__mconvertible_to, _Error>; + constexpr auto __count = + __mapply<__mcount_if<__is_convertible_error>, __error_variant_t>::value; + if constexpr (__count == 1) { - __state_->__errors_.template emplace<__decay_t<_Error>>(std::move(__error).error); + using __error_t = + __mapply<__mfind_if<__is_convertible_error, __q<__mfront>>, __error_variant_t>; + return __nothrow_constructible_from<__error_t, _Error>; } else { - static_assert(__mnever<_Error>, "Error type not in task's error_types"); + return false; } + } + + template + constexpr void __set_error(_Error&& __error) noexcept(__nothrow_error_conversion<_Error&&>()) + { + using __is_convertible_error = __mbind_front_q<__mconvertible_to, _Error&&>; + constexpr auto __count = + __mapply<__mcount_if<__is_convertible_error>, __error_variant_t>::value; + static_assert(__count == 1, + "The error must be convertible to exactly one of the task's error types"); + if constexpr (__count == 1) + { + using __error_t = + __mapply<__mfind_if<__is_convertible_error, __q<__mfront>>, __error_variant_t>; + __state_->__errors_.template emplace<__error_t>(static_cast<_Error&&>(__error)); + } + } + + template + constexpr auto yield_value(with_error<_Error> __error) // + noexcept(noexcept(__set_error(std::move(__error).error))) + { + __set_error(std::move(__error).error); + return __completed_awaiter{}; + } + + constexpr void __set_stopped() noexcept + { + __state_->__stopped_ = true; + } + + constexpr auto yield_value(with_stopped) noexcept + { + __set_stopped(); return __completed_awaiter{}; } diff --git a/test/stdexec/types/test_task.cpp b/test/stdexec/types/test_task.cpp index 5fb0b4e3d..f38d54bb2 100644 --- a/test/stdexec/types/test_task.cpp +++ b/test/stdexec/types/test_task.cpp @@ -26,6 +26,7 @@ # include # include +# include # include @@ -162,6 +163,234 @@ namespace CHECK(!res.has_value()); } + struct destruction_probe + { + explicit destruction_probe(bool &destroyed) noexcept + : destroyed_(&destroyed) + {} + + destruction_probe(destruction_probe &&other) noexcept + : destroyed_(std::exchange(other.destroyed_, nullptr)) + {} + + ~destruction_probe() + { + if (destroyed_ != nullptr) + { + *destroyed_ = true; + } + } + + destruction_probe(destruction_probe const &) = delete; + + bool *destroyed_; + }; + + auto test_task_destroys_frame_before_propagating_stopped( + [[maybe_unused]] destruction_probe probe) -> ex::task + { + co_await ex::just_stopped(); + FAIL("Expected co_awaiting just_stopped to stop the task"); + co_return 42; + } + + TEST_CASE("task destroys its coroutine frame before propagating stopped", "[types][task]") + { + bool destroyed = false; + auto t = test_task_destroys_frame_before_propagating_stopped(destruction_probe{destroyed}) + | ex::upon_stopped( + [&]() noexcept + { + CHECK(destroyed); + return 0; + }); + ex::sync_wait(std::move(t)); + CHECK(destroyed); + } + + auto test_task_yields_stopped([[maybe_unused]] destruction_probe probe) -> ex::task + { + co_yield ex::with_stopped(); + FAIL("Expected co_yielding with_stopped to stop the task"); + } + + TEST_CASE("task can co_yield with_stopped", "[types][task]") + { + bool destroyed = false; + auto t = test_task_yields_stopped(destruction_probe{destroyed}) + | ex::upon_stopped( + [&]() noexcept + { + CHECK(destroyed); + }); + ex::sync_wait(std::move(t)); + CHECK(destroyed); + } + + auto test_task_returns_stopped([[maybe_unused]] destruction_probe probe) -> ex::task + { + co_return ex::with_stopped(); + } + + TEST_CASE("non-void task can co_return with_stopped", "[types][task]") + { + bool destroyed = false; + auto t = test_task_returns_stopped(destruction_probe{destroyed}) + | ex::upon_stopped( + [&]() noexcept + { + CHECK(destroyed); + return 42; + }); + auto [value] = ex::sync_wait(std::move(t)).value(); + CHECK(value == 42); + CHECK(destroyed); + } + +# if !STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() + auto test_void_task_returns_stopped([[maybe_unused]] destruction_probe probe) -> ex::task + { + co_return ex::with_stopped(); + } + + TEST_CASE("void task can co_return with_stopped", "[types][task]") + { + bool destroyed = false; + auto t = test_void_task_returns_stopped(destruction_probe{destroyed}) + | ex::upon_stopped( + [&]() noexcept + { + CHECK(destroyed); + }); + ex::sync_wait(std::move(t)); + CHECK(destroyed); + } +# endif + + struct stopped_as_value + { + constexpr stopped_as_value(ex::with_stopped) noexcept {} + }; + + auto test_task_returns_with_stopped_as_value() -> ex::task + { + co_return ex::with_stopped(); + } + + TEST_CASE("task returns with_stopped as a value when it is convertible to the value type", + "[types][task]") + { + auto result = ex::sync_wait(test_task_returns_with_stopped_as_value()); + CHECK(result.has_value()); + } + +# if !STDEXEC_NO_STDCPP_EXCEPTIONS() + struct long_error_env + { + using error_types = ex::completion_signatures; + }; + + auto test_task_yields_convertible_error() -> ex::task + { + co_yield ex::with_error{42}; + FAIL("Expected co_yielding with_error to complete the task with an error"); + } + + auto test_task_catches_converted_yielded_error() -> ex::task + { + try + { + co_await test_task_yields_convertible_error(); + } + catch (long error) + { + co_return error; + } + FAIL("Expected co_awaiting the task to throw its declared error type"); + co_return 0; + } + + TEST_CASE("task converts a co_yielded error to its declared error type", "[types][task]") + { + auto [error] = ex::sync_wait(test_task_catches_converted_yielded_error()).value(); + CHECK(error == 42); + } + + auto test_task_returns_convertible_error() -> ex::task + { + co_return ex::with_error{42}; + } + + auto test_task_catches_converted_returned_error() -> ex::task + { + try + { + co_await test_task_returns_convertible_error(); + } + catch (long error) + { + co_return error; + } + FAIL("Expected co_awaiting the task to throw its declared error type"); + co_return 0; + } + + TEST_CASE("non-void task converts a co_returned error to its declared error type", + "[types][task]") + { + auto [error] = ex::sync_wait(test_task_catches_converted_returned_error()).value(); + CHECK(error == 42); + } + +# if !STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() + auto test_void_task_returns_convertible_error() -> ex::task + { + co_return ex::with_error{42}; + } + + auto test_task_catches_void_task_returned_error() -> ex::task + { + try + { + co_await test_void_task_returns_convertible_error(); + } + catch (long error) + { + co_return error; + } + FAIL("Expected co_awaiting the task to throw its declared error type"); + co_return 0; + } + + TEST_CASE("void task converts a co_returned error to its declared error type", "[types][task]") + { + auto [error] = ex::sync_wait(test_task_catches_void_task_returned_error()).value(); + CHECK(error == 42); + } +# endif +# endif + + struct error_as_value + { + constexpr error_as_value(ex::with_error error) noexcept + : value_(error.error) + {} + + int value_; + }; + + auto test_task_returns_with_error_as_value() -> ex::task + { + co_return ex::with_error{42}; + } + + TEST_CASE("task returns with_error as a value when it is convertible to the value type", + "[types][task]") + { + auto [value] = ex::sync_wait(test_task_returns_with_error_as_value()).value(); + CHECK(value.value_ == 42); + } + // A sender type that does not claim to complete inline: struct just_int : ex::__result_of {