From e89288e1f25f8a80dbbf3aa90e88a1b7e7c0ed4c Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 19 Jul 2025 13:56:45 +0000 Subject: [PATCH 01/10] exec::storage_for_completion_signatures Certain algorithms, for example when_all, must store the completions of child operations and then later examine or forward them. The typical way of doing this is via decay-copy into a tuple, but this erases the reference-ness of values and errors. The class template added by this commit, exec:: storage_for_completion_signatures, makes it convenient to store, examine, and forward the completions of some asynchronous operation, while also being reference-aware. --- .../storage_for_completion_signatures.hpp | 252 +++++++++++++++ test/exec/CMakeLists.txt | 1 + ...test_storage_for_completion_signatures.cpp | 296 ++++++++++++++++++ 3 files changed, 549 insertions(+) create mode 100644 include/exec/storage_for_completion_signatures.hpp create mode 100644 test/exec/test_storage_for_completion_signatures.cpp diff --git a/include/exec/storage_for_completion_signatures.hpp b/include/exec/storage_for_completion_signatures.hpp new file mode 100644 index 000000000..f6df423c6 --- /dev/null +++ b/include/exec/storage_for_completion_signatures.hpp @@ -0,0 +1,252 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include "../stdexec/execution.hpp" + +#include +#include +#include +#include +#include + +namespace exec { + +namespace detail::storage_for_completion_signatures { + +template +struct decay { + using type = std::decay_t; +}; +template +struct decay { + using type = T&; +}; +template +struct decay { + using type = T&&; +}; + +template +struct tuple_for_signature; + +template +struct tuple_for_signature { + using type = std::tuple::type...>; +}; + +template +struct overload; +template +struct overload { + typename tuple_for_signature::type operator()(Tag, Args...) + const; +}; + +template +struct overload_set; +template +struct overload_set<::STDEXEC::completion_signatures> + : overload... +{ + using overload::operator()...; +}; + +template +using tuple_for_arrival = decltype( + overload_set{}( + Tag{}, + std::declval()...)); + +template +struct variant_for_signatures; + +template +struct variant_for_signatures< + ::STDEXEC::completion_signatures> +{ + using type = std::variant< + std::monostate, + typename tuple_for_signature::type...>; +}; + +template +struct signature; + +template +struct signature { + using type = Tag(typename decay::type...); +}; + +template +struct nothrow_visitable; + +template +struct nothrow_visitable { + static constexpr bool value = std::is_nothrow_invocable_v< + Visitor, + Tag, + typename decay::type...>; +}; + +template +struct nothrow_storable; + +template +struct nothrow_storable { + static constexpr bool value = ( + std::is_nothrow_constructible_v< + typename decay::type, + Args> && ...); +}; + +} + +template +class storage_for_completion_signatures; + +template<> +class storage_for_completion_signatures<::STDEXEC::completion_signatures<>> { +public: + using completion_signatures = ::STDEXEC::completion_signatures<>; + template + constexpr bool visit(Visitor&&) && noexcept { + return false; + } + template<::STDEXEC::receiver Receiver> + [[noreturn]] + constexpr void complete(Receiver&&) && noexcept { + STDEXEC_UNREACHABLE(); + } +}; + +template +class storage_for_completion_signatures< + ::STDEXEC::completion_signatures> +{ + using base_signatures_ = ::STDEXEC::completion_signatures< + typename detail::storage_for_completion_signatures::signature< + Signatures>::type...>; + static constexpr auto noexcept_ = + (detail::storage_for_completion_signatures::nothrow_storable< + Signatures>::value && ...); + using maybe_throwing_signature_ = std::conditional_t< + noexcept_, + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>; +public: + using completion_signatures = ::STDEXEC::transform_completion_signatures< + base_signatures_, + maybe_throwing_signature_>; +private: + template + using tuple_type_ = + detail::storage_for_completion_signatures::tuple_for_arrival< + ::STDEXEC::completion_signatures, + Tag, + Args...>; + using storage_type_ = + typename detail::storage_for_completion_signatures::variant_for_signatures< + completion_signatures>::type; + storage_type_ storage_; + template + static constexpr bool nothrow_visitable_ = ( + detail::storage_for_completion_signatures::nothrow_visitable< + Visitor, + Signatures>::value && ...); +public: + template + requires std::is_constructible_v< + storage_type_, + std::in_place_type_t>, + Tag, + Args...> + constexpr void arrive(Tag t, Args&&... args) noexcept { + STDEXEC_ASSERT(std::holds_alternative(storage_)); + constexpr auto nothrow = std::is_nothrow_constructible_v< + tuple_type_, + Tag, + Args...>; + const auto impl = [&]() noexcept(nothrow) { + storage_.template emplace>( + (Tag&&)t, + (Args&&)args...); + }; + if constexpr (nothrow) { + impl(); + } else { + try { + impl(); + } catch (...) { + storage_.template emplace< + std::tuple< + ::STDEXEC::set_error_t, + std::exception_ptr>>( + ::STDEXEC::set_error, + std::current_exception()); + } + } + } + template + constexpr bool visit(Visitor&& visitor) && noexcept(nothrow_visitable_) + { + return std::visit( + [&](auto&& tuple_or_monostate) noexcept(nothrow_visitable_) { + if constexpr (std::is_same_v< + std::monostate, + std::remove_cvref_t>) + { + return false; + } else { + std::apply( + (Visitor&&)visitor, + (decltype(tuple_or_monostate)&&)tuple_or_monostate); + return true; + } + }, + (storage_type_&&)storage_); + } + template<::STDEXEC::receiver_of Receiver> + constexpr void complete(Receiver&& r) && noexcept { + std::visit( + [&](auto&& tuple_or_monostate) noexcept { + if constexpr (std::is_same_v< + std::monostate, + std::remove_cvref_t>) + { + STDEXEC_UNREACHABLE(); + } else { + std::apply( + [&](const auto tag, auto&&... args) noexcept { + tag((Receiver&&)r, (decltype(args)&&)args...); + }, + // Odds are this is inside an operation state, which means that + // sending the completion signal may end our lifetime, which means + // we shouldn't send references into ourselves, therefore we move + // all the non-references onto the stack + std::remove_cvref_t( + std::move(tuple_or_monostate))); + } + }, + (storage_type_&&)storage_); + } +}; + +} // namespace exec diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 895dc5017..54df251e1 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -46,6 +46,7 @@ set(exec_test_sources test_static_thread_pool.cpp test_just_from.cpp test_fork.cpp + test_storage_for_completion_signatures.cpp sequence/test_any_sequence_of.cpp sequence/test_empty_sequence.cpp sequence/test_ignore_all_values.cpp diff --git a/test/exec/test_storage_for_completion_signatures.cpp b/test/exec/test_storage_for_completion_signatures.cpp new file mode 100644 index 000000000..4cafe5632 --- /dev/null +++ b/test/exec/test_storage_for_completion_signatures.cpp @@ -0,0 +1,296 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +using namespace exec; + +namespace { + +TEST_CASE("Storing no completion signatures works", "[storage_for_completion_signatures]") { + storage_for_completion_signatures< + ::STDEXEC::completion_signatures<>> storage; + static_assert( + std::is_same_v< + decltype(storage)::completion_signatures, + ::STDEXEC::completion_signatures<>>); + CHECK(!std::move(storage).visit([&](auto&&...) { + FAIL("Unexpected invocation of visitor"); + })); +} + +TEST_CASE("Storing simple completion signatures and then visiting them works", "[storage_for_completion_signatures]") { + using completion_signatures = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::error_code)>; + using storage = storage_for_completion_signatures< + completion_signatures>; + static_assert( + set_equivalent< + completion_signatures, + storage::completion_signatures>); + CHECK(!storage{}.visit([&](auto&&...) { + FAIL("Unexpected invocation of visitor"); + })); + struct base { + void operator()(::STDEXEC::set_stopped_t&&) && { + FAIL("Unexpected stop"); + } + void operator()(::STDEXEC::set_value_t&&, int&&) && { + FAIL("Unexpected value"); + } + void operator()(::STDEXEC::set_error_t&&, std::error_code&&) && { + FAIL("Unexpected error"); + } + bool invoked{false}; + protected: + void invoke_() { + CHECK(!invoked); + invoked = true; + } + }; + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_stopped))); + s.arrive(::STDEXEC::set_stopped); + static_assert( + noexcept( + std::move(s).visit([](auto&&...) noexcept {}))); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_stopped_t&&) && { + invoke_(); + } + } visitor; + static_assert(!noexcept(std::move(s).visit(visitor))); + CHECK(std::move(s).visit(std::move(visitor))); + CHECK(visitor.invoked); + } + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_value, 5))); + s.arrive(::STDEXEC::set_value, 5); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_value_t&&, int&& i) && { + invoke_(); + CHECK(i == 5); + } + } visitor; + static_assert(!noexcept(std::move(s).visit(visitor))); + CHECK(std::move(s).visit(std::move(visitor))); + CHECK(visitor.invoked); + } + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_error, std::error_code{}))); + s.arrive( + ::STDEXEC::set_error, + make_error_code(std::errc::no_such_file_or_directory)); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_error_t&&, std::error_code&& ec) && { + invoke_(); + CHECK(ec == make_error_code(std::errc::no_such_file_or_directory)); + } + } visitor; + static_assert(!noexcept(std::move(s).visit(visitor))); + CHECK(std::move(s).visit(std::move(visitor))); + CHECK(visitor.invoked); + } +} + +TEST_CASE("Storing simple completion signatures and then completing a receiver therewith works", "[storage_for_completion_signatures]") { + using storage_type = + storage_for_completion_signatures< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(), + ::STDEXEC::set_stopped_t(), + ::STDEXEC::set_error_t(std::exception_ptr)>>; + { + storage_type storage; + storage.arrive(::STDEXEC::set_value); + std::move(storage).complete(expect_void_receiver{}); + } + { + std::optional storage(std::in_place); + storage->arrive( + ::STDEXEC::set_error, + std::make_exception_ptr(std::logic_error("TEST"))); + std::exception_ptr ex; + struct receiver { + using receiver_concept = ::STDEXEC::receiver_t; + void set_value() noexcept { + FAIL("Unexpected value invocation"); + } + void set_stopped() noexcept { + FAIL("Unexpected stopped invocation"); + } + void set_error(std::exception_ptr&& ex) noexcept { + // This ensures that the exception_ptr is moved onto the stack + CHECK(storage_); + storage_.reset(); + CHECK(!ex_); + ex_ = std::move(ex); + } + std::optional& storage_; + std::exception_ptr& ex_; + }; + std::move(*storage).complete(receiver{storage, ex}); + REQUIRE(ex); + bool threw = false; + try { + std::rethrow_exception(std::move(ex)); + } catch (const std::logic_error& ex) { + threw = true; + CHECK(ex.what() == std::string_view("TEST")); + } + CHECK(threw); + } +} + +TEST_CASE("When storing a completion signature would throw it is simply coalesced to std::exception_ptr", "[storage_for_completion_signatures]") { + struct maybe_throws_on_move { + maybe_throws_on_move() = default; + maybe_throws_on_move(maybe_throws_on_move&& other) { + if (other.throws) { + throw std::runtime_error("Throwing as requested"); + } + } + bool throws{false}; + }; + { + using storage = storage_for_completion_signatures< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move)>>; + static_assert( + set_equivalent< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move), + ::STDEXEC::set_error_t(std::exception_ptr)>, + storage::completion_signatures>); + struct base { + void operator()(::STDEXEC::set_value_t&&, maybe_throws_on_move&&) & { + FAIL("Unexpected value invocation"); + } + void operator()(::STDEXEC::set_error_t&&, std::exception_ptr&&) & { + FAIL("Unexpected error invocation"); + } + bool invoked{false}; + protected: + void invoke_() { + CHECK(!invoked); + invoked = true; + } + }; + { + storage s; + static_assert( + noexcept( + s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}))); + maybe_throws_on_move obj; + obj.throws = true; + s.arrive(::STDEXEC::set_value, std::move(obj)); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_error_t&&, std::exception_ptr&& ex) & { + invoke_(); + REQUIRE(ex); + // TODO? + } + } visitor; + CHECK(std::move(s).visit(visitor)); + CHECK(visitor.invoked); + } + { + storage s; + s.arrive(::STDEXEC::set_value, maybe_throws_on_move{}); + struct : base { + using base::operator(); + void operator()(::STDEXEC::set_value_t&&, maybe_throws_on_move&&) & { + invoke_(); + } + } visitor; + CHECK(std::move(s).visit(visitor)); + CHECK(visitor.invoked); + } + } + // Important that the below cases don't add the std::exception_ptr completion + // since propagating a reference can't throw + { + using signatures = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move&)>; + using storage = storage_for_completion_signatures; + static_assert( + std::is_same_v< + storage::completion_signatures, + signatures>); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, obj); + bool invoked = false; + CHECK(std::move(s).visit([&](::STDEXEC::set_value_t, maybe_throws_on_move& stored) { + CHECK(!invoked); + invoked = true; + CHECK(&obj == &stored); + })); + CHECK(invoked); + } + { + using signatures = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(maybe_throws_on_move&&)>; + using storage = storage_for_completion_signatures; + static_assert( + std::is_same_v< + storage::completion_signatures, + signatures>); + maybe_throws_on_move obj; + storage s; + s.arrive(::STDEXEC::set_value, std::move(obj)); + bool invoked = false; + CHECK(std::move(s).visit([&](::STDEXEC::set_value_t, maybe_throws_on_move&& stored) { + CHECK(!invoked); + invoked = true; + CHECK(&obj == &stored); + })); + CHECK(invoked); + } +} + +} // unnamed namespace From ff67ffa278fb0b7fca6de41026c02da70a1b0eec Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sun, 26 Oct 2025 14:34:55 -0400 Subject: [PATCH 02/10] exec::like_t Type alias for the type returned by std::forward_like. --- include/exec/like_t.hpp | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 include/exec/like_t.hpp diff --git a/include/exec/like_t.hpp b/include/exec/like_t.hpp new file mode 100644 index 000000000..e2dd04d84 --- /dev/null +++ b/include/exec/like_t.hpp @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include + +namespace exec { + +template +using like_t = decltype( + ::STDEXEC::__forward_like( + std::declval())); + +} // namespace exec From b39b6b610126d03fec2c3681cc52249117167f00 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Wed, 21 Jan 2026 23:06:13 -0500 Subject: [PATCH 03/10] exec::invoke stdexec::let_value performs two functions: - Persisting the values sent by the predecessor, and - Predicating the successor on those persisted values Which leaves space for a lower level primitive: One which simply predicates a successor sender on the values sent by a predecessor. That lower level primitive is exec::invoke. --- include/exec/invoke.hpp | 413 ++++++++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_invoke.cpp | 92 +++++++++ 3 files changed, 506 insertions(+) create mode 100644 include/exec/invoke.hpp create mode 100644 test/exec/test_invoke.cpp diff --git a/include/exec/invoke.hpp b/include/exec/invoke.hpp new file mode 100644 index 000000000..2a0474065 --- /dev/null +++ b/include/exec/invoke.hpp @@ -0,0 +1,413 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "elide.hpp" +#include "like_t.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::invoke { + +struct tag {}; + +struct t { + template<::STDEXEC::sender Sender, typename F> + constexpr ::STDEXEC::sender auto operator()(Sender&& sender, F&& f) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender> && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + F>) + { + return ::STDEXEC::__make_sexpr( + (F&&)f, + (Sender&&)sender); + } + template + constexpr auto operator()(F&& f) const noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + F>) + { + return ::STDEXEC::__closure(*this, (F&&)f); + } + template + static constexpr auto transform_sender( + ::STDEXEC::set_value_t, + Sender&& sender, + const Env&) noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + auto&& [_, f, predecessor] = (Sender&&)sender; + static_assert(::STDEXEC::sender); + return ::STDEXEC::__make_sexpr( + std::tuple((decltype(f)&&)f, (decltype(predecessor)&&)predecessor)); + } +}; + +template +struct sender_check; +template +struct sender_check { + static constexpr bool value = ::STDEXEC::sender; +}; +template +struct sender_check { + static constexpr bool value = ::STDEXEC::sender_in; +}; + +template +class transform_set_value { + template + class impl_ { + static_assert(std::is_invocable_v); + using sender_ = std::invoke_result_t; + static_assert(sender_check::value); + static constexpr bool nothrow_invoke_ = std::is_nothrow_invocable_v< + F, + Args...>; + static constexpr bool nothrow_connect_ = ::STDEXEC::__nothrow_connectable< + sender_, + ::STDEXEC::__receiver_archetype>; + public: + using type = ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t< + sender_, + Env...>, + std::conditional_t< + nothrow_invoke_ && nothrow_connect_, + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>>; + }; +public: + template + using fn = impl_::type; +}; + +template +using completions = ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures<>, + transform_set_value::template fn>; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template<::STDEXEC::receiver Receiver> +struct receiver_ref { + using receiver_concept = ::STDEXEC::receiver_t; + Receiver& r_; + constexpr ::STDEXEC::env_of_t get_env() const noexcept { + return ::STDEXEC::get_env(r_); + } + template + requires ::STDEXEC::receiver_of< + Receiver, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Args...)>> + constexpr void set_value(Args&&... args) && noexcept { + ::STDEXEC::set_value((Receiver&&)r_, (Args&&)args...); + } + template + requires ::STDEXEC::receiver_of< + Receiver, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(T)>> + constexpr void set_error(T&& t) && noexcept { + ::STDEXEC::set_error((Receiver&&)r_, (T&&)t); + } + constexpr void set_stopped() && noexcept requires ::STDEXEC::receiver_of< + Receiver, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_stopped_t()>> + { + ::STDEXEC::set_stopped((Receiver&&)r_); + } +}; + +template> +struct variant_for_operation_states; + +template +struct variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Args...), + Signatures...>, + std::tuple> +{ + using type = variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures, + std::tuple< + ::STDEXEC::connect_result_t< + std::invoke_result_t< + F, + Args...>, + receiver_ref>, + States...>>::type; +}; + +template +struct variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures< + Signature, + Signatures...>, + States> +{ + using type = variant_for_operation_states< + F, + Receiver, + Additional, + ::STDEXEC::completion_signatures, + States>::type; +}; + +template +struct variant_for_operation_states< + F, + Receiver, + std::tuple, + ::STDEXEC::completion_signatures<>, + std::tuple> +{ + using type = ::STDEXEC::__munique< + ::STDEXEC::__qq>::__f; +}; + +template +class state { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_type_ { + using receiver_concept = ::STDEXEC::receiver_t; + state& self_; + constexpr env_type_ get_env() const noexcept; + template + constexpr void set_value(Args&&...) && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + using operation_state_type_ = ::STDEXEC::connect_result_t< + Sender, + receiver_type_>; + using operation_states_type_ = variant_for_operation_states< + F, + Receiver, + std::tuple, + ::STDEXEC::completion_signatures_of_t>::type; + Receiver r_; + F f_; + operation_states_type_ ops_; + static constexpr bool nothrow_connect_ = ::STDEXEC::__nothrow_connectable< + Sender, + receiver_type_>; +public: + template + explicit constexpr state(Sender&& s, T&& t, Receiver r) noexcept( + std::is_nothrow_constructible_v && nothrow_connect_) + : r_((Receiver&&)r), + f_((T&&)t), + ops_( + std::in_place_type, + ::exec::elide([&]() noexcept(nothrow_connect_) { + return ::STDEXEC::connect( + (Sender&&)s, + receiver_type_{*this}); + })) + {} + constexpr void start() & noexcept { + const auto ptr = std::get_if(&ops_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(*ptr); + } +}; + +template +constexpr auto state::receiver_type_::get_env() const + noexcept -> env_type_ +{ + return ::STDEXEC::get_env(self_.r_); +} + +template +template +constexpr void state::receiver_type_::set_value( + Args&&... args) && noexcept +{ + constexpr auto nothrow_connect = ::STDEXEC::__nothrow_connectable< + std::invoke_result_t, + receiver_ref>; + // We store this locally because we're going to destroy the operation state + // and therefore, transitively, *this + auto&& self = self_; + try { + // It's important we use auto&& here not auto because the invocable might + // return a reference to a sender + auto&& sender = std::invoke((F&&)self.f_, (Args&&)args...); + // receiver_ref is important here, imagine we didn't use receiver_ref and + // instead directly moved the final receiver into connect, then if connect + // threw we'd already potentially have "consumed" the receiver and wouldn't + // be able to send the error completion anywhere + using op_state_type = ::STDEXEC::connect_result_t< + decltype(sender), + receiver_ref>; + auto&& op = self.ops_.template emplace( + ::exec::elide([&]() noexcept(nothrow_connect) { + return ::STDEXEC::connect( + (decltype(sender)&&)sender, + receiver_ref{self.r_}); + })); + ::STDEXEC::start(op); + } catch (...) { + if constexpr (std::is_nothrow_invocable_v && nothrow_connect) { + STDEXEC_UNREACHABLE(); + } else { + ::STDEXEC::set_error((Receiver&&)self.r_, std::current_exception()); + } + } +} + +template +template +constexpr void state::receiver_type_::set_error( + Args&&... args) && noexcept +{ + ::STDEXEC::set_error((Receiver&&)self_.r_, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_stopped( + Args&&... args) && noexcept +{ + ::STDEXEC::set_stopped((Receiver&&)self_.r_, (Args&&)args...); +} + +template +class nothrow_get_state { + using tuple_type_ = std::remove_cvref_t<::STDEXEC::__data_of>; + using sender_type_ = ::exec::like_t< + Sender, + std::tuple_element_t<1, tuple_type_>>; + using invocable_type_ = std::tuple_element_t<0, tuple_type_>; +public: + static constexpr bool value = std::is_nothrow_constructible_v< + state, + sender_type_, + ::exec::like_t, + Receiver>; +}; + +template> +struct get_state_result; +template +struct get_state_result> { + using type = state< + ::exec::like_t, + F, + Receiver>; +}; + +struct impl : public ::STDEXEC::__sexpr_defaults { + template + static consteval auto get_completion_signatures() { + using tuple = std::remove_cvref_t< + ::STDEXEC::__data_of>; + using f = std::tuple_element_t<0, tuple>; + using sender = ::exec::like_t>; + static_assert(::STDEXEC::sender); + if constexpr (sizeof...(Env)) { + static_assert(::STDEXEC::sender_in); + } + if constexpr (::STDEXEC::__mvalid) { + return completions{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto get_state = []( + Sender&& sender, Receiver r) noexcept( + nothrow_get_state::value) + -> get_state_result<::STDEXEC::__data_of, Receiver>::type + { + auto&& [_, tuple] = (Sender&&)sender; + auto&& [f, inner] = (decltype(tuple)&&)tuple; + return + typename get_state_result<::STDEXEC::__data_of, Receiver>::type( + (decltype(inner)&&)inner, + (decltype(f)&&)f, + (Receiver&&)r); + }; + static constexpr auto start = [](auto& state) noexcept { + state.start(); + }; +}; + +} + +using invoke_t = detail::invoke::t; +inline constexpr invoke_t invoke; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::detail::invoke::tag> + : ::exec::detail::invoke::impl {}; + +template<> +struct __sexpr_impl<::exec::invoke_t> : ::STDEXEC::__sexpr_defaults { + template + static consteval auto get_completion_signatures() { + using type = decltype( + ::STDEXEC::transform_sender( + std::declval(), + std::declval()...)); + static_assert(!std::is_same_v< + std::remove_cvref_t, + std::remove_cvref_t>); + return ::STDEXEC::get_completion_signatures(); + } +}; + +} diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 54df251e1..d2507c26b 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -62,6 +62,7 @@ set(exec_test_sources test_system_context.cpp $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp + test_invoke.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_invoke.cpp b/test/exec/test_invoke.cpp new file mode 100644 index 000000000..7c4464218 --- /dev/null +++ b/test/exec/test_invoke.cpp @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +namespace { + +TEST_CASE("Values from a predecessor are used to predicate the successor", "[invoke]") { + auto f = [](int&& i) noexcept { + return ::STDEXEC::just(i * 2); + }; + auto predecessor = ::STDEXEC::just(5); + auto sender = predecessor | ::exec::invoke(f); + static_assert( + std::is_same_v< + ::exec::detail::invoke::completions< + decltype(predecessor), + decltype(f), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int)>>); + static_assert( + set_equivalent< + ::exec::detail::invoke::variant_for_operation_states< + decltype(f), + expect_value_receiver<::STDEXEC::env<>, int>, + std::tuple<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int)>>::type, + std::variant< + ::STDEXEC::connect_result_t< + decltype(predecessor), + ::exec::detail::invoke::receiver_ref< + expect_value_receiver<::STDEXEC::env<>, int>>>>>); + static_assert( + std::is_same_v< + ::STDEXEC::completion_signatures_of_t< + decltype(sender), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(int)>>); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_value_receiver(10)); + ::STDEXEC::start(op); +} + +TEST_CASE("If the predecessor never completes successfully then the invocable is never invoked", "[invoke]") { + auto op = ::STDEXEC::connect( + // Notably 5 is not invocable + ::STDEXEC::just_stopped() | ::exec::invoke(5), + expect_stopped_receiver{}); + ::STDEXEC::start(op); +} + +TEST_CASE("When the invocable throws that exception is passed on", "[invoke]") { + auto op = ::STDEXEC::connect( + ::STDEXEC::just() | ::exec::invoke([]() -> decltype(::STDEXEC::just()) { + throw std::logic_error("TEST"); + }), + expect_error_receiver{}); + ::STDEXEC::start(op); +} + +} // unnamed namespace From 6dcb3f02e2dbd0ded331ff5ca8c20614992b6d58 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Mon, 19 Jan 2026 09:22:32 -0500 Subject: [PATCH 04/10] exec::exit_scope_sender Adds concepts which deal with exit scope senders. An exit scope sender performs the clean up/rollback operations necessary to leave a "scope." As such an exit scope sender must (in addition to being a sender): - Send exactly stdexec::set_value_t() - Be nothrow connectable - Be nothrow movable - Be nothrow decay-copyable Note that in some sense an exit scope sender is analogous to a destructor. --- include/exec/exit_scope_sender.hpp | 50 ++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_exit_scope_sender.cpp | 35 +++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 include/exec/exit_scope_sender.hpp create mode 100644 test/exec/test_exit_scope_sender.cpp diff --git a/include/exec/exit_scope_sender.hpp b/include/exec/exit_scope_sender.hpp new file mode 100644 index 000000000..6a710f72d --- /dev/null +++ b/include/exec/exit_scope_sender.hpp @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include + +#include "../stdexec/execution.hpp" + +namespace exec { + +template +concept exit_scope_sender = + ::STDEXEC::sender && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender> && + std::is_nothrow_move_constructible_v< + std::remove_cvref_t>; + +template +concept exit_scope_sender_in = + exit_scope_sender && + ::STDEXEC::sender_in && + ::STDEXEC::__nothrow_connectable< + Sender, + ::STDEXEC::__receiver_archetype> && + std::is_same_v< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t()>, + ::STDEXEC::completion_signatures_of_t< + Sender, + Env>>; + +} // namespace exec diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index d2507c26b..335706b90 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -63,6 +63,7 @@ set(exec_test_sources $<$:test_libdispatch.cpp> test_unless_stop_requested.cpp test_invoke.cpp + test_exit_scope_sender.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_exit_scope_sender.cpp b/test/exec/test_exit_scope_sender.cpp new file mode 100644 index 000000000..0929a5e67 --- /dev/null +++ b/test/exec/test_exit_scope_sender.cpp @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include + +namespace { + +static_assert( + ::exec::exit_scope_sender_in< + decltype(::STDEXEC::just()), + ::STDEXEC::env<>>); +static_assert( + !::exec::exit_scope_sender_in< + decltype(::STDEXEC::just(5)), + ::STDEXEC::env<>>); + +} // unnamed namespace From 35b6d63e23c8057276e7bf92a8216120ef978a5f Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Mon, 19 Jan 2026 09:26:13 -0500 Subject: [PATCH 05/10] exec::enter_scope_sender et al. Adds concepts which deal with enter scope senders. Whereas an exit scope sender performs the operations necessary to leave a scope, an enter scope sender performs the operations necessary to enter a scope. Just entering a scope is insufficient, however, as if a scope is entered it must be exited. Therefore enter scope senders not only perform the actions which must be taken to enter a scope, but also complete with a sender which undoes those actions, i.e. all enter scope senders are a higher-order sender. --- include/exec/enter_scope_sender.hpp | 81 +++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_enter_scope_sender.cpp | 51 +++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 include/exec/enter_scope_sender.hpp create mode 100644 test/exec/test_enter_scope_sender.cpp diff --git a/include/exec/enter_scope_sender.hpp b/include/exec/enter_scope_sender.hpp new file mode 100644 index 000000000..af8746171 --- /dev/null +++ b/include/exec/enter_scope_sender.hpp @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include + +#include "exit_scope_sender.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +template +concept enter_scope_sender = ::STDEXEC::sender; + +namespace detail::exit_scope_sender_of { + +template +struct transform_set_value_impl; +template Sender> +struct transform_set_value_impl { + using type = ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Sender)>; +}; + +template +struct transform_set_value { + template + using fn = transform_set_value_impl::type; +}; + +template +using transform_set_error = ::STDEXEC::completion_signatures<>; + +template +struct impl; +template +struct impl< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t(Sender)>> +{ + using type = Sender; +}; + +} + +template +using exit_scope_sender_of_t = detail::exit_scope_sender_of::impl< + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures<>, + detail::exit_scope_sender_of::transform_set_value::template fn, + detail::exit_scope_sender_of::transform_set_error, + ::STDEXEC::completion_signatures<>>>::type; + +template +concept enter_scope_sender_in = + enter_scope_sender && + ::STDEXEC::sender_in && + requires { + typename exit_scope_sender_of_t; + }; + +} // namespace exec diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 335706b90..77e1d9084 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -64,6 +64,7 @@ set(exec_test_sources test_unless_stop_requested.cpp test_invoke.cpp test_exit_scope_sender.cpp + test_enter_scope_sender.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_enter_scope_sender.cpp b/test/exec/test_enter_scope_sender.cpp new file mode 100644 index 000000000..97b7948ab --- /dev/null +++ b/test/exec/test_enter_scope_sender.cpp @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include +#include + +namespace { + +static_assert( + !::exec::enter_scope_sender_in< + decltype(::STDEXEC::just()), + ::STDEXEC::env<>>); +static_assert( + ::exec::enter_scope_sender_in< + decltype(::STDEXEC::just(::STDEXEC::just())), + ::STDEXEC::env<>>); + +static_assert( + std::is_same_v< + decltype(::STDEXEC::just()), + ::exec::exit_scope_sender_of_t< + decltype(::STDEXEC::just(::STDEXEC::just())), + ::STDEXEC::env<>>>); +static_assert( + std::is_same_v< + decltype(::STDEXEC::just()), + ::exec::exit_scope_sender_of_t< + ::exec::variant_sender< + decltype(::STDEXEC::just(::STDEXEC::just())), + decltype(::STDEXEC::just_stopped())>, + ::STDEXEC::env<>>>); + +} // unnamed namespace From 1e6002e57ed4c12242709f26addd22298c90d1a2 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Tue, 20 Jan 2026 11:08:21 -0500 Subject: [PATCH 06/10] exec::enter_scopes An algorithm which combines N enter scope senders into a single enter scope sender which enters the scopes represented by each of its children in parallel, i.e. the scopes are entered in no particular order, as if by stdexec::when_all. --- include/exec/enter_scopes.hpp | 448 ++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_enter_scopes.cpp | 180 +++++++++++++ 3 files changed, 629 insertions(+) create mode 100644 include/exec/enter_scopes.hpp create mode 100644 test/exec/test_enter_scopes.cpp diff --git a/include/exec/enter_scopes.hpp b/include/exec/enter_scopes.hpp new file mode 100644 index 000000000..d69024762 --- /dev/null +++ b/include/exec/enter_scopes.hpp @@ -0,0 +1,448 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "like_t.hpp" +#include "storage_for_completion_signatures.hpp" +#include "../stdexec/execution.hpp" + +#include + +namespace exec { + +namespace detail::enter_scopes { + +struct tag {}; + +struct t { + template<::exec::enter_scope_sender... Senders> + constexpr ::exec::enter_scope_sender auto operator()(Senders&&... senders) + const noexcept( + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Senders> && ...)) + { + return ::STDEXEC::__make_sexpr( + ::STDEXEC::__(), + (Senders&&)senders...); + } + template<::exec::enter_scope_sender Sender> + constexpr ::exec::enter_scope_sender auto operator()(Sender&& sender) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + return (Sender&&)sender; + } + constexpr ::exec::enter_scope_sender auto operator()() const noexcept { + return ::STDEXEC::just(::STDEXEC::just()); + } + // This moves the senders from the children section to the data section so we + // can connect/start/etc. them ourselves + template + static constexpr auto transform_sender( + ::STDEXEC::set_value_t, + Sender&& sender, + const Env&) noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + return ::STDEXEC::__apply( + [](::STDEXEC::__ignore, ::STDEXEC::__ignore, auto&&... senders) + noexcept( + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + decltype(senders)> && ...)) + { + return ::STDEXEC::__make_sexpr( + // TODO: Transform all the children? + std::tuple((decltype(senders)&&)senders...)); + }, + (Sender&&)sender); + } +}; + +template< + ::exec::enter_scope_sender Sender, + typename Receiver, + typename RollbackReceiver> +class enter_scope_sender_state { + using env_type_ = ::STDEXEC::env_of_t; + using exit_scope_sender_type_ = ::exec::exit_scope_sender_of_t< + Sender, + env_type_>; + struct receiver_type_ : Receiver { + // Note the parameter is by value which means that we can't possibly have a + // reference back into the operation state, this is exception safe because + // exit senders must be nothrow decay-copyable + constexpr void set_value(exit_scope_sender_type_ sender) && noexcept { + auto base = (Receiver&&)*this; + auto&& self = self_; + // This destroys *this + self.storage_.template emplace( + std::move(sender)); + ::STDEXEC::set_value(std::move(base)); + } + enter_scope_sender_state& self_; + }; + using operation_state_type_ = ::STDEXEC::connect_result_t< + Sender, + receiver_type_>; + using rollback_operation_state_type_ = ::STDEXEC::connect_result_t< + exit_scope_sender_type_, + RollbackReceiver>; + std::variant< + operation_state_type_, + exit_scope_sender_type_, + rollback_operation_state_type_> storage_; + static constexpr bool nothrow_constructible_ = + ::STDEXEC::__nothrow_connectable; +public: + constexpr explicit enter_scope_sender_state(Sender&& sender, Receiver r) + noexcept(nothrow_constructible_) + : storage_( + std::in_place_type, + ::exec::elide([&]() noexcept(nothrow_constructible_) { + return ::STDEXEC::connect( + (Sender&&)sender, + receiver_type_{ + {(Receiver&&)r}, + *this}); + })) + {} + constexpr void start() & noexcept { + const auto ptr = std::get_if(&storage_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(*ptr); + } + [[nodiscard]] + constexpr bool connect_rollback(RollbackReceiver r) & noexcept { + const auto ptr = std::get_if(&storage_); + if (!ptr) { + return false; + } + auto sender = std::move(*ptr); + storage_.template emplace( + ::exec::elide([&]() noexcept { + return ::STDEXEC::connect( + std::move(sender), + std::move(r)); + })); + return true; + } + constexpr void start_rollback() & noexcept { + if ( + const auto ptr = std::get_if(&storage_); + ptr) + { + ::STDEXEC::start(*ptr); + } + } + constexpr exit_scope_sender_type_&& complete() && noexcept { + const auto ptr = std::get_if(&storage_); + STDEXEC_ASSERT(ptr); + return std::move(*ptr); + } +}; + +template +using remove_set_value_t = ::STDEXEC::completion_signatures<>; + +template +using exit_sender_t = decltype( + ::STDEXEC::when_all( + std::declval()...)); + +// TODO: Rewrite this +template +struct storage_for_completion_signatures { + template + using __f = ::exec::storage_for_completion_signatures< + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::__concat_completion_signatures_t< + ::STDEXEC::completion_signatures_of_t< + Senders, + Env...>...>, + ::STDEXEC::completion_signatures<>, + remove_set_value_t>>; +}; + +template +class state { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_base_ { + using receiver_concept = ::STDEXEC::receiver_t; + constexpr env_type_ get_env() const noexcept; + state& self_; + }; + struct receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + struct rollback_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept; + }; + template + using state_type_ = enter_scope_sender_state< + S, + receiver_type_, + rollback_receiver_type_>; + Receiver r_; + std::tuple...> states_; + // TODO: There's no need for these two members if no child can fail or stop + typename storage_for_completion_signatures< + ::STDEXEC::env_of_t>::template __f storage_; + std::atomic stored_{false}; + std::atomic outstanding_{sizeof...(Senders)}; + constexpr bool is_complete_() noexcept { + return outstanding_.fetch_sub(1, std::memory_order_acq_rel) == 1; + } + constexpr void complete_() noexcept { + if (!is_complete_()) { + return; + } + std::apply( + [&](auto&... states) noexcept { + if (stored_.load(std::memory_order_relaxed)) { + // Starting with one prevents the child operations from finalizing + // the operation out from under us, thereby preventing UB (it does + // require that we check to see if we have to finalize as we + // complete, see below) + outstanding_.store(1, std::memory_order_relaxed); + const auto impl = [&](auto& state) noexcept { + if (state.connect_rollback(rollback_receiver_type_{{*this}})) { + outstanding_.fetch_add(1, std::memory_order_relaxed); + } + }; + (impl(states), ...); + (states.start_rollback(), ...); + rollback_complete_(); + } else { + ::STDEXEC::set_value( + std::move(r_), + ::STDEXEC::when_all(std::move(states).complete()...)); + } + }, + states_); + } + template + constexpr void fail_(Args&&... args) noexcept { + if (!stored_.exchange(true, std::memory_order_relaxed)) { + storage_.arrive((Args&&)args...); + } + complete_(); + } + constexpr void rollback_complete_() noexcept { + if (is_complete_()) { + std::move(storage_).complete(std::move(r_)); + } + } +public: + explicit constexpr state( + Receiver r, + Senders&&... senders) noexcept( + (std::is_nothrow_constructible_v< + state_type_, + Senders, + receiver_type_> && ...)) + : r_((Receiver&&)r), + states_( + ::exec::elide([&]() noexcept( + std::is_nothrow_constructible_v< + state_type_, + Senders, + receiver_type_>) + { + return state_type_( + (Senders&&)senders, + receiver_type_{{*this}}); + })...) + {} + constexpr void start() & noexcept { + std::apply( + [](auto&&... states) noexcept { + (states.start(), ...); + }, + states_); + } +}; + +template +constexpr auto state::receiver_base_::get_env() const + noexcept -> env_type_ +{ + return ::STDEXEC::get_env(self_.r_); +} + +template +constexpr void state::receiver_type_::set_value() && + noexcept +{ + self_.complete_(); +} + +template +template +constexpr void state::receiver_type_::set_error( + Args&&... args) && noexcept +{ + self_.fail_(::STDEXEC::set_error, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_stopped( + Args&&... args) && noexcept +{ + self_.fail_(::STDEXEC::set_stopped, (Args&&)args...); +} + +template +constexpr void state::rollback_receiver_type_::set_value() + && noexcept +{ + self_.rollback_complete_(); +} + +template +struct completions; +template +struct completions, Env> { + using type = ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t( + exit_sender_t< + ::exec::exit_scope_sender_of_t< + ::exec::like_t, + Env>...>)>, + typename storage_for_completion_signatures:: + template __f<::exec::like_t...>::completion_signatures>; +}; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template> +struct get_state_result; +template +struct get_state_result> { + using type = state< + Receiver, + ::exec::like_t...>; +}; + +template> +struct nothrow_get_state; +template +struct nothrow_get_state> { + static constexpr bool value = std::is_nothrow_constructible_v< + typename get_state_result::type, + Receiver, + Senders...>; +}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = completions::type; +public: + template + static consteval auto get_completion_signatures() { + using tuple = std::remove_cvref_t< + ::STDEXEC::__data_of>; + if constexpr (sizeof...(Env) == 0) { + return ::STDEXEC::__dependent_sender_error_t(); + } else if constexpr (::STDEXEC::__mvalid) + { + return completions_{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto get_state = []( + Sender&& sender, Receiver r) noexcept( + nothrow_get_state<::STDEXEC::__data_of, Receiver>::value) + -> get_state_result<::STDEXEC::__data_of, Receiver>::type + { + auto&& [_, tuple] = (Sender&&)sender; + using state_type = + get_state_result<::STDEXEC::__data_of, Receiver>::type; + return std::apply( + [&](auto&&... senders) noexcept( + nothrow_get_state<::STDEXEC::__data_of, Receiver>::value) + { + return typename get_state_result< + ::STDEXEC::__data_of, + Receiver>::type( + (Receiver&&)r, + (decltype(senders)&&)senders...); + }, + (decltype(tuple)&&)tuple); + }; + static constexpr auto start = [](auto& state) noexcept { + state.start(); + }; +}; + +} + +using enter_scopes_t = detail::enter_scopes::t; +inline constexpr enter_scopes_t enter_scopes; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::detail::enter_scopes::tag> + : ::exec::detail::enter_scopes::impl {}; + +template<> +struct __sexpr_impl<::exec::enter_scopes_t> : ::STDEXEC::__sexpr_defaults { + template + static consteval auto get_completion_signatures() { + using type = decltype( + ::STDEXEC::transform_sender( + std::declval(), + std::declval()...)); + static_assert(!std::is_same_v< + std::remove_cvref_t, + std::remove_cvref_t>); + return ::STDEXEC::get_completion_signatures(); + } +}; + +} diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 77e1d9084..6651e6136 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -65,6 +65,7 @@ set(exec_test_sources test_invoke.cpp test_exit_scope_sender.cpp test_enter_scope_sender.cpp + test_enter_scopes.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_enter_scopes.cpp b/test/exec/test_enter_scopes.cpp new file mode 100644 index 000000000..6200cb25a --- /dev/null +++ b/test/exec/test_enter_scopes.cpp @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" +#include "../test_common/type_helpers.hpp" + +namespace { + +static_assert( + ::exec::enter_scope_sender_in< + decltype(::exec::enter_scopes()), + ::STDEXEC::env<>>); +static_assert( + ::exec::enter_scope_sender_in< + decltype(::exec::enter_scopes(::STDEXEC::just(::STDEXEC::just()))), + ::STDEXEC::env<>>); + +TEST_CASE("No scopes may be entered", "[enter_scopes]") { + bool invoked = false; + auto sender = ::exec::enter_scopes(); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + auto op = ::STDEXEC::connect( + std::move(sender), + make_fun_receiver([&](auto&& sender) noexcept { + static_assert(::exec::exit_scope_sender); + CHECK(!invoked); + invoked = true; + auto op = ::STDEXEC::connect( + std::forward(sender), + expect_void_receiver{}); + ::STDEXEC::start(op); + })); + CHECK(!invoked); + ::STDEXEC::start(op); + CHECK(invoked); +} + +TEST_CASE("One scope may be entered", "[enter_scopes]") { + std::size_t done = 0; + std::size_t undone = 0; + auto undo = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++undone; + }); + auto succeed = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++done; + return undo; + }); + auto sender = ::exec::enter_scopes(succeed); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + auto op = ::STDEXEC::connect( + std::move(sender), + make_fun_receiver([&](auto&& sender) noexcept { + static_assert(::exec::exit_scope_sender); + CHECK(done == 1); + CHECK(!undone); + auto op = ::STDEXEC::connect( + std::forward(sender), + expect_void_receiver{}); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(undone == 1); + })); + CHECK(!done); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(done == 1); + CHECK(undone == 1); +} + +TEST_CASE("Multiple scopes may be entered", "[enter_scopes]") { + std::size_t done = 0; + std::size_t undone = 0; + auto undo = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++undone; + }); + auto succeed = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + ++done; + return undo; + }); + auto sender = ::exec::enter_scopes(succeed, succeed); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + auto op = ::STDEXEC::connect( + std::move(sender), + make_fun_receiver([&](auto&& sender) noexcept { + static_assert(::exec::exit_scope_sender); + CHECK(done == 2); + CHECK(!undone); + auto op = ::STDEXEC::connect( + std::forward(sender), + expect_void_receiver{}); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(undone == 2); + })); + CHECK(!done); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(done == 2); + CHECK(undone == 2); +} + +TEST_CASE("When an attempt is made to enter multiple scopes and entering one of them fails the effects of entering the other are undone", "[construct]") { + bool undone = false; + auto undo = ::STDEXEC::just() | ::STDEXEC::then([&]() noexcept { + CHECK(!undone); + undone = true; + }); + auto succeed = ::STDEXEC::just(undo); + auto fail = ::STDEXEC::just_error( + std::make_exception_ptr( + std::logic_error("TEST"))); + using enter_scope_sender_type = ::exec::variant_sender< + decltype(succeed), + decltype(fail)>; + static_assert( + ::exec::enter_scope_sender_in< + enter_scope_sender_type, + ::STDEXEC::env<>>); + auto sender = ::exec::enter_scopes( + enter_scope_sender_type(succeed), + enter_scope_sender_type(fail)); + static_assert( + ::exec::enter_scope_sender_in< + decltype(sender), + ::STDEXEC::env<>>); + static_assert( + set_equivalent< + ::STDEXEC::completion_signatures_of_t< + decltype(sender), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t( + ::exec::exit_scope_sender_of_t< + decltype(sender), + ::STDEXEC::env<>>), + ::STDEXEC::set_error_t(std::exception_ptr)>>); + auto op = ::STDEXEC::connect(std::move(sender), expect_error_receiver{}); + CHECK(!undone); + ::STDEXEC::start(op); + CHECK(undone); +} + +} // unnamed namespace From 036d6a48f0ad053ae8c0c871fdf107387d017d68 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 24 Jan 2026 15:59:42 -0500 Subject: [PATCH 07/10] exec::within Algorithm which accepts an enter scope sender, and a sender to run within the scope represented by the enter scope sender, respectively. The resulting operation: 1. Enters the scope represented by the enter scope sender 2. Runs the operation represented by the other sender 3. Stores the completion of the above 4. Exits the scope using the exit scope sender yielded by the operation in step 1 5. Yields the completion stored in step 3 --- include/exec/within.hpp | 400 ++++++++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_within.cpp | 118 +++++++++++ 3 files changed, 519 insertions(+) create mode 100644 include/exec/within.hpp create mode 100644 test/exec/test_within.cpp diff --git a/include/exec/within.hpp b/include/exec/within.hpp new file mode 100644 index 000000000..851b4b4bd --- /dev/null +++ b/include/exec/within.hpp @@ -0,0 +1,400 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "like_t.hpp" +#include "storage_for_completion_signatures.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::within { + +struct tag {}; + +struct t { + template<::exec::enter_scope_sender Scope, ::STDEXEC::sender Sender> + constexpr ::STDEXEC::sender auto operator()(Scope&& scope, Sender&& sender) + const noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Scope> && + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + return ::STDEXEC::__make_sexpr( + ::STDEXEC::__(), + (Scope&&)scope, + (Sender&&)sender); + } + template + static constexpr auto transform_sender( + Tag, + Sender&& sender, + const Env&) noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + Sender>) + { + auto&& [_, data, scope, inner] = (Sender&&)sender; + (void)data; + return ::STDEXEC::__make_sexpr( + // TODO: Transform the children? + std::tuple((decltype(scope)&&)scope, (decltype(inner)&&)inner)); + } +}; + +template +using remove_set_value_t = ::STDEXEC::completion_signatures<>; + +template +inline constexpr bool nothrow_connect = + ::STDEXEC::__nothrow_connectable< + Sender, + ::STDEXEC::__receiver_archetype> && + // Because we move the sender out of storage so we can reuse it + std::is_nothrow_move_constructible_v; + +template +using completion_signatures_of_sender_t = + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + std::conditional_t< + nothrow_connect, + ::STDEXEC::completion_signatures<>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_error_t(std::exception_ptr)>>>; + +template +using completion_signatures_of_scope_t = + ::STDEXEC::transform_completion_signatures< + ::STDEXEC::completion_signatures_of_t, + ::STDEXEC::completion_signatures<>, + remove_set_value_t>; + +template +using storage_for_completion_signatures_t = + ::exec::storage_for_completion_signatures< + completion_signatures_of_sender_t>; + +template +struct completions; +template +struct completions, Env> { + using type = ::STDEXEC::transform_completion_signatures< + typename storage_for_completion_signatures_t< + Sender, + Env>::completion_signatures, + completion_signatures_of_scope_t< + // Note the use of like_t for the scope but not the inner sender, this is + // because we're going to decay-copy the inner sender whereas we connect + // the scope with the value category of the incoming sender + ::exec::like_t, + Env>>; +}; + +template +class state { + using env_type_ = ::STDEXEC::env_of_t; + struct receiver_base_ { + using receiver_concept = ::STDEXEC::receiver_t; + constexpr env_type_ get_env() const noexcept; + state& self_; + }; + using exit_scope_sender_type_ = + ::exec::exit_scope_sender_of_t; + struct enter_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value(exit_scope_sender_type_) && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + using enter_operation_state_type_ = ::STDEXEC::connect_result_t< + Scope, + enter_receiver_type_>; + struct exit_receiver_type_ : receiver_base_ { + using receiver_base_::self_; + constexpr void set_value() && noexcept; + }; + using exit_operation_state_type_ = ::STDEXEC::connect_result_t< + exit_scope_sender_type_, + exit_receiver_type_>; + struct receiver_type_ : receiver_base_ { + using receiver_base_::self_; + template + constexpr void set_value(Args&&...) && noexcept; + template + constexpr void set_error(Args&&...) && noexcept; + template + constexpr void set_stopped(Args&&...) && noexcept; + }; + using operation_state_type_ = ::STDEXEC::connect_result_t< + Sender, + receiver_type_>; + Receiver r_; + storage_for_completion_signatures_t storage_; + struct enter_state_type_ { + enter_operation_state_type_ op; + Sender s; + }; + struct state_type_ { + operation_state_type_ op; + exit_scope_sender_type_ s; + }; + std::variant< + enter_state_type_, + state_type_, + exit_operation_state_type_> state_; + constexpr void exit_() noexcept { + const auto ptr = std::get_if(&state_); + auto sender = std::move(ptr->s); + auto&& op = state_.template emplace( + ::exec::elide([&]() noexcept { + return ::STDEXEC::connect( + std::move(sender), + exit_receiver_type_{{*this}}); + })); + ::STDEXEC::start(op); + } + template + constexpr void complete_(Args&&... args) noexcept { + storage_.arrive((Args&&)args...); + exit_(); + } +public: + template + explicit constexpr state( + Scope&& scope, + S&& sender, + Receiver r) // noexcept + : r_((Receiver&&)r), + state_( + std::in_place_type, + ::exec::elide([&]() /* noexcept */ { + return enter_state_type_{ + ::STDEXEC::connect( + (Scope&&)scope, + enter_receiver_type_{{*this}}), + (S&&)sender}; + })) + { + } + constexpr void start() & noexcept { + const auto ptr = std::get_if(&state_); + STDEXEC_ASSERT(ptr); + ::STDEXEC::start(ptr->op); + } +}; + +template +constexpr auto state::receiver_base_::get_env() const + noexcept -> env_type_ +{ + return ::STDEXEC::get_env(self_.r_); +} + +template +constexpr void state::enter_receiver_type_::set_value( + exit_scope_sender_type_ exit) && noexcept +{ + // Because we're going to destroy the operation state and therefore + // transitively *this + auto&& self = self_; + constexpr auto nothrow = nothrow_connect; + const auto ptr = std::get_if(&self.state_); + STDEXEC_ASSERT(ptr); + try { + auto sender = (Sender&&)ptr->s; + auto&& state = self.state_.template emplace( + ::exec::elide([&]() noexcept(nothrow) { + return state_type_{ + ::STDEXEC::connect( + (Sender&&)sender, + receiver_type_{{self}}), + std::move(exit)}; + })); + ::STDEXEC::start(state.op); + } catch (...) { + if constexpr (nothrow) { + STDEXEC_UNREACHABLE(); + } else { + ::STDEXEC::set_error((Receiver&&)self.r_, std::current_exception()); + } + } +} + +template +template +constexpr void state::enter_receiver_type_::set_error( + Args&&... args) && noexcept +{ + ::STDEXEC::set_error((Receiver&&)self_.r_, (Args&&)args...); +} + +template +template +constexpr void state::enter_receiver_type_:: + set_stopped(Args&&... args) && noexcept +{ + ::STDEXEC::set_stopped((Receiver&&)self_.r_, (Args&&)args...); +} + +template +constexpr void state::exit_receiver_type_::set_value() + && noexcept +{ + std::move(self_.storage_).complete((Receiver&&)self_.r_); +} + +template +template +constexpr void state::receiver_type_::set_value( + Args&&... args) && noexcept +{ + self_.complete_(::STDEXEC::set_value, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_error( + Args&&... args) && noexcept +{ + self_.complete_(::STDEXEC::set_error, (Args&&)args...); +} + +template +template +constexpr void state::receiver_type_::set_stopped( + Args&&... args) && noexcept +{ + self_.complete_(::STDEXEC::set_stopped, (Args&&)args...); +} + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +template> +struct get_state_result; +template +struct get_state_result> { + using type = state< + ::exec::like_t, + Sender, + Receiver>; +}; + +template> +struct nothrow_get_state; +template +struct nothrow_get_state> { + static constexpr bool value = std::is_nothrow_constructible_v< + typename get_state_result::type, + ::exec::like_t, + ::exec::like_t, + Receiver>; +}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = completions::type; +public: + template + static consteval auto get_completion_signatures() { + using tuple = std::remove_cvref_t< + ::STDEXEC::__data_of>; + if constexpr (sizeof...(Env) == 0) { + return ::STDEXEC::__dependent_sender_error_t(); + } else if constexpr (::STDEXEC::__mvalid) + { + return completions_{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto get_state = []( + Sender&& sender, Receiver r) noexcept( + nothrow_get_state<::STDEXEC::__data_of, Receiver>::value) + -> get_state_result<::STDEXEC::__data_of, Receiver>::type + { + auto&& [_, tuple] = (Sender&&)sender; + auto&& [scope, inner] = (decltype(tuple)&&)tuple; + // See note on completion signature computation for why we remove_cvref_t + // here + using inner_type = std::remove_cvref_t; + static_assert( + ::exec::enter_scope_sender_in< + decltype(scope), + ::STDEXEC::env_of_t>); + static_assert( + std::is_constructible_v< + inner_type, + decltype(inner)>); + return + typename get_state_result<::STDEXEC::__data_of, Receiver>::type( + (decltype(scope)&&)scope, + (decltype(inner)&&)inner, + (Receiver&&)r); + }; + static constexpr auto start = [](auto& state) noexcept { + state.start(); + }; +}; + +} + +using within_t = detail::within::t; +inline constexpr within_t within; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::detail::within::tag> + : ::exec::detail::within::impl {}; + +template<> +struct __sexpr_impl<::exec::within_t> : ::STDEXEC::__sexpr_defaults { + template + static consteval auto get_completion_signatures() { + static_assert(::STDEXEC::sender_expr_for); + using type = decltype( + ::STDEXEC::transform_sender( + std::declval(), + std::declval()...)); + static_assert(!std::is_same_v< + std::remove_cvref_t, + std::remove_cvref_t>); + return ::STDEXEC::get_completion_signatures(); + } +}; + +} diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 6651e6136..c74ca30f8 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -66,6 +66,7 @@ set(exec_test_sources test_exit_scope_sender.cpp test_enter_scope_sender.cpp test_enter_scopes.cpp + test_within.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_within.cpp b/test/exec/test_within.cpp new file mode 100644 index 000000000..36ca4917e --- /dev/null +++ b/test/exec/test_within.cpp @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" + +namespace { + +TEST_CASE("The scope is entered, the wrapped sender is run, and then the scope is exited", "[within]") { + std::size_t n{}; + std::optional entered; + std::optional executed; + std::optional exited; + auto enter = + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!entered); + entered = n; + ++n; + return + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!exited); + exited = n; + ++n; + }); + }); + auto sender = + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!executed); + executed = n; + ++n; + }); + auto within = ::exec::within(enter, sender); + static_assert( + std::is_same_v< + ::STDEXEC::completion_signatures_of_t< + decltype(within), + ::STDEXEC::env<>>, + ::STDEXEC::completion_signatures< + ::STDEXEC::set_value_t()>>); + auto op = ::STDEXEC::connect( + std::move(within), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(entered == 0); + CHECK(executed == 1); + CHECK(exited == 2); +} + +TEST_CASE("If the work throws the scope is still exited", "[within]") { + std::size_t n{}; + std::optional entered; + std::optional executed; + std::optional exited; + auto enter = + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!entered); + entered = n; + ++n; + return + ::STDEXEC::just() | + ::STDEXEC::then([&]() noexcept { + CHECK(!exited); + exited = n; + ++n; + }); + }); + auto sender = + ::STDEXEC::just() | + ::STDEXEC::then([&]() { + CHECK(!executed); + executed = n; + ++n; + throw std::logic_error("TEST"); + }); + auto op = ::STDEXEC::connect( + ::exec::within( + enter, + sender), + expect_error_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(entered == 0); + CHECK(executed == 1); + CHECK(exited == 2); +} + +} // unnamed namespace From 726e4a0b9dd34ac1623099df570d03213bc9017a Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 24 Jan 2026 15:55:57 -0500 Subject: [PATCH 08/10] exec::object et al. Adds the concepts which deal with asynchronous objects. An asynchronous object is an object whose constructor and destructor are asynchronous operations (as opposed to regular, synchronous object which have constructors and destructors which are regular, synchronous functions). Whereas senders are a fully-curried asynchronous function the concept of an asynchronous object embodied by the concepts in this commit is a fully-curried asynchronous constructor. The particular form of the aforementioned is a unary invocable whose: - Sole argument is a pointer to storage whereat the asynchronous object shall be placed - Returned value is an enter scope sender which constructs the object (asynchronously) when the scope is entered, and destroys the object (asynchronously) when the scope is exited --- include/exec/object.hpp | 65 +++++++++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_object.cpp | 30 ++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 include/exec/object.hpp create mode 100644 test/exec/test_object.cpp diff --git a/include/exec/object.hpp b/include/exec/object.hpp new file mode 100644 index 000000000..1e306ab0a --- /dev/null +++ b/include/exec/object.hpp @@ -0,0 +1,65 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include + +#include "enter_scope_sender.hpp" + +namespace exec { + +template +concept object = + std::constructible_from< + std::remove_cvref_t, + Object> && + std::move_constructible< + std::remove_cvref_t> && + std::is_object_v< + typename std::remove_cvref_t::type> && + requires(Object o) { + { + std::invoke( + (Object&&)o, + (typename std::remove_cvref_t::type*)nullptr) } + -> enter_scope_sender; + }; + +template +concept object_in = + object && + requires(Object o) { + { + std::invoke( + (Object&&)o, + (typename std::remove_cvref_t::type*)nullptr) } + -> enter_scope_sender_in; + }; + +template +using type_of_object_t = std::remove_cvref_t::type; + +template +using enter_scope_sender_of_object_t = std::invoke_result_t< + Object, + type_of_object_t*>; + +} // namespace exec diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index c74ca30f8..8e7469774 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -46,6 +46,7 @@ set(exec_test_sources test_static_thread_pool.cpp test_just_from.cpp test_fork.cpp + test_object.cpp test_storage_for_completion_signatures.cpp sequence/test_any_sequence_of.cpp sequence/test_empty_sequence.cpp diff --git a/test/exec/test_object.cpp b/test/exec/test_object.cpp new file mode 100644 index 000000000..84ab3a953 --- /dev/null +++ b/test/exec/test_object.cpp @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include +#include + +namespace { + +static_assert(::exec::object<::exec::sync_object>); +static_assert(::exec::object_in<::exec::sync_object, ::STDEXEC::env<>>); + +} // unnamed namespace From 25e6f510221ab7a9ccee9bf8bc3d3a446d5ff172 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 24 Jan 2026 16:01:58 -0500 Subject: [PATCH 09/10] exec::lifetime Enables consumption of asynchronous objects. Accepts an invocable, and N asynchronous objects. Forms an operation which, when connected: 1. Provides the asynchronous objects with storage in its operation state 2. Passes all enter scope senders obtained in step 1 to exec:: enter_scopes to combine them 3. Connects the resulting enter scope sender And when started: 1. Starts the enter scope sender which was connected in step 3 above 2. If the operation started in step 1 sends error or stopped, completes immediately with that completion, otherwise passes a reference to all of the newly-constructed objects to the invocable, obtaining a sender 3. Connects and starts the sender obtained in step 2 4. Upon the completion of that operation, stores the completion thereof 5. Connects and starts the exit scope sender which destroys the objects constructed in step 1 6. Upon the completion of that operation, ends the overall operation with the completion stored in step 4 --- include/exec/lifetime.hpp | 218 ++++++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_lifetime.cpp | 145 ++++++++++++++++++++++++ 3 files changed, 364 insertions(+) create mode 100644 include/exec/lifetime.hpp create mode 100644 test/exec/test_lifetime.cpp diff --git a/include/exec/lifetime.hpp b/include/exec/lifetime.hpp new file mode 100644 index 000000000..8a0725f6e --- /dev/null +++ b/include/exec/lifetime.hpp @@ -0,0 +1,218 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "enter_scopes.hpp" +#include "invoke.hpp" +#include "like_t.hpp" +#include "object.hpp" +#include "within.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +namespace detail::lifetime { + +struct t { + template + requires ::STDEXEC::sender< + std::invoke_result_t< + F, + ::exec::type_of_object_t&...>> + constexpr ::STDEXEC::sender auto operator()(F&& f, Objects&&... objects) const + noexcept( + std::is_nothrow_constructible_v< + std::remove_cvref_t, + F> && + (std::is_nothrow_constructible_v< + std::remove_cvref_t, + Objects> && ...)) + { + return ::STDEXEC::__make_sexpr( + std::tuple((F&&)f, (Objects&&)objects...)); + } +}; + +template +class storage_for_object { + alignas(T) std::byte buffer_[sizeof(T)]; +public: + constexpr T* get_storage() noexcept { + return reinterpret_cast(buffer_); + } + constexpr T& get_object() noexcept { + return *std::launder(get_storage()); + } +}; + +template +using storage_for_object_t = storage_for_object< + ::exec::type_of_object_t>; + +template +using storage_for_objects_t = std::tuple< + storage_for_object_t...>; + +template> +class make_sender_impl; + +template +class make_sender_impl> { + static constexpr auto impl_( + ::exec::like_t&& f, + storage_for_object_t&... storage) noexcept( + std::is_nothrow_constructible_v< + F, + ::exec::like_t>) + { + return [f = (::exec::like_t&&)f, &storage...]() mutable noexcept( + std::is_nothrow_invocable_v< + F, + ::exec::type_of_object_t&...>) + { + return std::invoke( + std::move(f), + storage.get_object()...); + }; + } +public: + static constexpr bool nothrow = noexcept( + ::exec::within( + ::exec::enter_scopes( + std::invoke( + std::declval<::exec::like_t>(), + std::declval<::exec::type_of_object_t*>())...), + ::STDEXEC::just() | ::exec::invoke( + impl_( + std::declval<::exec::like_t>(), + std::declval&>()...)))); + using storage_type = storage_for_objects_t; + static constexpr ::STDEXEC::sender auto impl( + Tuple&& t, + storage_type& storage) noexcept(nothrow) + { + return std::apply( + [&](auto&& f, auto&&... objects) noexcept(nothrow) { + return std::apply( + [&](auto&... storage_for_object) noexcept(nothrow) { + return ::exec::within( + ::exec::enter_scopes( + std::invoke( + (decltype(objects)&&)objects, + storage_for_object.get_storage())...), + ::STDEXEC::just() | ::exec::invoke( + impl_((decltype(f)&&)f, storage_for_object...))); + }, + storage); + }, + (Tuple&&)t); + } + using sender_type = decltype( + impl( + std::declval(), + std::declval&>())); +}; + +template +constexpr ::STDEXEC::sender auto make_sender( + Tuple&& tuple, + std::tuple...>& storage) noexcept( + noexcept( + make_sender_impl::impl( + (Tuple&&)tuple, + storage))) +{ + return make_sender_impl::impl((Tuple&&)tuple, storage); +} + +template +class state { + using impl_ = make_sender_impl; + typename impl_::storage_type storage_; + ::STDEXEC::connect_result_t< + typename impl_::sender_type, + Receiver> op_; +public: + explicit constexpr state(Tuple&& t, Receiver r) noexcept(impl_::nothrow) + : op_( + ::STDEXEC::connect( + impl_::impl((Tuple&&)t, storage_), + (Receiver&&)r)) + {} + constexpr void start() & noexcept { + ::STDEXEC::start(op_); + } +}; + +// TODO: We should be able to get a better "message" than this +struct FAILED_TO_FORM_COMPLETION_SIGNATURES {}; + +class impl : public ::STDEXEC::__sexpr_defaults { + template + using completions_ = ::STDEXEC::completion_signatures_of_t< + typename make_sender_impl<::STDEXEC::__data_of>::sender_type, + Env...>; +public: + template + static consteval auto get_completion_signatures() { + if constexpr (::STDEXEC::__mvalid) { + return completions_{}; + } else { + return ::STDEXEC::__throw_compile_time_error< + FAILED_TO_FORM_COMPLETION_SIGNATURES, + ::STDEXEC::_WITH_PRETTY_SENDER_>(); + } + } + static constexpr auto get_state = []( + Sender&& sender, Receiver r) noexcept( + std::is_nothrow_constructible_v< + state<::STDEXEC::__data_of, Receiver>, + ::STDEXEC::__data_of, + Receiver>) -> state<::STDEXEC::__data_of, Receiver> + { + auto&& [_, tuple] = (Sender&&)sender; + return state( + (decltype(tuple)&&)tuple, + (Receiver&&)r); + }; + static constexpr auto start = [](auto& state) noexcept { + state.start(); + }; +}; + +} + +using lifetime_t = detail::lifetime::t; +inline constexpr lifetime_t lifetime; + +} // namespace exec + +namespace STDEXEC { + +template<> +struct __sexpr_impl<::exec::lifetime_t> : ::exec::detail::lifetime::impl {}; + +} diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 8e7469774..2ca7fdd0d 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -68,6 +68,7 @@ set(exec_test_sources test_enter_scope_sender.cpp test_enter_scopes.cpp test_within.cpp + test_lifetime.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_lifetime.cpp b/test/exec/test_lifetime.cpp new file mode 100644 index 000000000..e418b20e0 --- /dev/null +++ b/test/exec/test_lifetime.cpp @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include +#include + +#include +#include +#include +#include +#include + +#include "../test_common/receivers.hpp" + +namespace { + +struct state { + std::size_t& n; + std::optional constructed; + std::optional destroyed; +}; + +struct object { + state& s; + using type = int; + ::exec::enter_scope_sender auto operator()(int* storage) const noexcept { + return + ::STDEXEC::just() | + ::STDEXEC::then([&s = s, storage]() noexcept { + new(storage) int(5); + CHECK(!s.constructed); + s.constructed = s.n; + ++s.n; + return + ::STDEXEC::just() | + ::STDEXEC::then([&s]() noexcept { + CHECK(!s.destroyed); + s.destroyed = s.n; + ++s.n; + }); + }); + } +}; + +TEST_CASE("Zero async objects work", "[lifetime]") { + bool invoked = false; + auto sender = ::exec::lifetime( + [&]() { + CHECK(!invoked); + invoked = true; + return ::STDEXEC::just(); + }); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!invoked); + ::STDEXEC::start(op); + CHECK(invoked); +} + +TEST_CASE("Implementation detail sender factory works for a single async object", "[lifetime]") { + std::size_t n{}; + state s{n}; + std::tuple<::exec::detail::lifetime::storage_for_object> storage; + auto sender = ::exec::detail::lifetime::make_sender( + std::tuple( + [&](int& i) { + CHECK(i == 5); + return ::STDEXEC::just(); + }, + object{s}), + storage); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(n == 2); + CHECK(s.constructed == 0); + CHECK(s.destroyed == 1); +} + +TEST_CASE("Single async object works", "[lifetime]") { + std::size_t n{}; + state s{n}; + auto sender = ::exec::lifetime( + [&](int& i) { + CHECK(i == 5); + return ::STDEXEC::just(); + }, + object{s}); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(n == 2); + CHECK(s.constructed == 0); + CHECK(s.destroyed == 1); +} + +TEST_CASE("Multiple async objects work", "[lifetime]") { + std::size_t n{}; + state a{n}; + state b{n}; + auto sender = ::exec::lifetime( + [&](int& a, int& b) { + CHECK(a == 5); + CHECK(b == 5); + CHECK(&a != &b); + return ::STDEXEC::just(); + }, + object{a}, + object{b}); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_void_receiver{}); + CHECK(!n); + ::STDEXEC::start(op); + CHECK(n == 4); + CHECK(a.constructed == 0); + CHECK(b.constructed == 1); + CHECK(a.destroyed == 2); + CHECK(b.destroyed == 3); +} + +} // unnamed namespace From 98869bdea6810ac590617ee60a909cfee0bb8495 Mon Sep 17 00:00:00 2001 From: Robert Leahy Date: Sat, 24 Jan 2026 15:59:40 -0500 Subject: [PATCH 10/10] exec::sync_object Adaptor which transforms a regular, synchronous object into an asynchronous object. --- include/exec/lifetime.hpp | 10 +++- include/exec/sync_object.hpp | 101 ++++++++++++++++++++++++++++++++ test/exec/CMakeLists.txt | 1 + test/exec/test_sync_object.cpp | 103 +++++++++++++++++++++++++++++++++ 4 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 include/exec/sync_object.hpp create mode 100644 test/exec/test_sync_object.cpp diff --git a/include/exec/lifetime.hpp b/include/exec/lifetime.hpp index 8a0725f6e..1fdbd435a 100644 --- a/include/exec/lifetime.hpp +++ b/include/exec/lifetime.hpp @@ -58,10 +58,16 @@ struct t { template class storage_for_object { - alignas(T) std::byte buffer_[sizeof(T)]; + union type_ { + char c; + T t; + constexpr type_() noexcept : c() {} + constexpr ~type_() noexcept {} + }; + type_ storage_; public: constexpr T* get_storage() noexcept { - return reinterpret_cast(buffer_); + return std::addressof(storage_.t); } constexpr T& get_object() noexcept { return *std::launder(get_storage()); diff --git a/include/exec/sync_object.hpp b/include/exec/sync_object.hpp new file mode 100644 index 000000000..29967f1ad --- /dev/null +++ b/include/exec/sync_object.hpp @@ -0,0 +1,101 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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. + */ + +#pragma once + +#include +#include +#include +#include + +#include "elide.hpp" +#include "enter_scope_sender.hpp" +#include "like_t.hpp" +#include "../stdexec/execution.hpp" + +namespace exec { + +template + requires + std::is_constructible_v && + std::is_destructible_v +struct sync_object { + using type = T; + template + requires (std::is_constructible_v && ...) + constexpr explicit sync_object(Ts&&... ts) noexcept( + (std::is_nothrow_constructible_v && ...)) + : args_((Ts&&)ts...) + {} + constexpr enter_scope_sender auto operator()(type* storage) & + noexcept(noexcept(make_sender(*this, storage))) + { + return make_sender(*this, storage); + } + constexpr enter_scope_sender auto operator()(type* storage) const & + noexcept(noexcept(make_sender(*this, storage))) + { + return make_sender(*this, storage); + } + constexpr enter_scope_sender auto operator()(type* storage) && + noexcept(noexcept(make_sender(std::move(*this), storage))) + { + return make_sender(std::move(*this), storage); + } + constexpr enter_scope_sender auto operator()(type* storage) const && + noexcept(noexcept(make_sender(std::move(*this), storage))) + { + return make_sender(std::move(*this), storage); + } +private: + template + static constexpr enter_scope_sender auto make_sender(Self&& self, type* storage) + noexcept( + std::is_nothrow_constructible_v< + std::tuple, + like_t>>) + { + constexpr auto nothrow = std::is_nothrow_constructible_v; + return + ::STDEXEC::just(std::forward(self).args_) | + ::STDEXEC::then([storage](std::tuple&& tuple) noexcept(nothrow) { + const auto ptr = std::construct_at( + storage, + ::exec::elide([&]() noexcept(nothrow) { + return std::make_from_tuple(std::move(tuple)); + })); + return + ::STDEXEC::just() | + // It's important we capture ptr not storage because storage just + // points to storage where ptr actually points to an object + ::STDEXEC::then([ptr]() noexcept { + ptr->~T(); + }); + }); + } + std::tuple args_; +}; + +template +constexpr sync_object...> make_sync_object(Args&&... args) + noexcept((std::is_nothrow_constructible_v, Args> && ...)) +{ + return sync_object...>((Args&&)args...); +} + +} // namespace exec diff --git a/test/exec/CMakeLists.txt b/test/exec/CMakeLists.txt index 2ca7fdd0d..40c6f9ba5 100644 --- a/test/exec/CMakeLists.txt +++ b/test/exec/CMakeLists.txt @@ -69,6 +69,7 @@ set(exec_test_sources test_enter_scopes.cpp test_within.cpp test_lifetime.cpp + test_sync_object.cpp ) add_executable(test.exec ${exec_test_sources}) diff --git a/test/exec/test_sync_object.cpp b/test/exec/test_sync_object.cpp new file mode 100644 index 000000000..71215cfb6 --- /dev/null +++ b/test/exec/test_sync_object.cpp @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * Copyright (c) 2025 Robert Leahy. 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 +#include +#include + +#include + +#include +#include + +#include "../test_common/receivers.hpp" + +namespace { + +struct state { + std::size_t constructed{0}; + std::size_t destroyed{0}; +}; + +class object { + state& s_; +public: + int i; + explicit constexpr object(state& s, int i) noexcept : s_(s), i(i) { + ++s_.constructed; + } + object(const object&) = delete; + object& operator=(const object&) = delete; + constexpr ~object() noexcept { + ++s_.destroyed; + } +}; + +// GCC 14 complains about an object being used outside its lifetime trying to +// build this, but doesn't really give any clues about which object so it's +// difficult to address +#ifdef __clang__ +static_assert([]() { + state s; + struct receiver { + using receiver_concept = ::STDEXEC::receiver_t; + bool& b_; + constexpr void set_value(const int i) && noexcept { + b_ = i == 5; + } + }; + auto sender = ::exec::lifetime( + [&](object& o) noexcept { + return ::STDEXEC::just(o.i); + }, + ::exec::make_sync_object( + std::ref(s), + 5)); + bool success = false; + auto op = ::STDEXEC::connect( + std::move(sender), + receiver{success}); + ::STDEXEC::start(op); + return success; +}()); +#endif + +TEST_CASE("Synchronous object may be adapted into asynchronous objects", "[sync_object]") { + state s; + auto sender = ::exec::lifetime( + [&](object& o) { + CHECK(s.constructed == 1); + CHECK(s.destroyed == 0); + return ::STDEXEC::just(o.i); + }, + ::exec::make_sync_object( + std::ref(s), + 5)); + auto op = ::STDEXEC::connect( + std::move(sender), + expect_value_receiver(5)); + CHECK(s.constructed == 0); + CHECK(s.destroyed == 0); + ::STDEXEC::start(op); + CHECK(s.constructed == 1); + CHECK(s.destroyed == 1); +} + +} // unnamed namespace