From 5a1de51297a14f5510c86311421381b0b36ffa1f Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Tue, 5 May 2026 22:47:56 +0530 Subject: [PATCH 01/12] take run_loop& directly, removing the __loop_ access entirely Signed-off-by: guptapratykshh --- .../algorithms/detail/inject_scheduler.hpp | 40 +++++++++++ .../hpx/execution/algorithms/make_future.hpp | 68 +++++++------------ .../tests/unit/algorithm_run_loop.cpp | 8 +-- 3 files changed, 70 insertions(+), 46 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp index 71be3690ff7b..fc29de11e626 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp @@ -58,4 +58,44 @@ namespace hpx::execution::experimental::detail { return HPX_MOVE(p).invoke(HPX_MOVE(p.scheduler), HPX_FORWARD(U, u)); } }; + + // Partial s/r algorithm sibling of `inject_scheduler` that holds a + // reference to a `run_loop` instead of a scheduler. Used so that + // `make_future(loop)` yields a pipe-able adapter which, when piped with + // a sender, dispatches `tag_invoke(Tag, run_loop&, sender, ...)`. + HPX_CXX_CORE_EXPORT template + struct inject_run_loop + : partial_algorithm_base, + Ts...> + { + private: + // Stored as a pointer because the partial may be moved and references + // can't be re-bound; the `run_loop&` lifetime is the caller's + // responsibility (the same as the `scheduler` case). + hpx::execution::experimental::run_loop* loop_; + + using base_type = partial_algorithm_base, Ts...>; + + public: + template + explicit constexpr inject_run_loop( + hpx::execution::experimental::run_loop& loop, Ts_&&... ts) + : base_type(HPX_FORWARD(Ts_, ts)...) + , loop_(&loop) + { + } + + // clang-format off + template + )> + // clang-format on + friend constexpr HPX_FORCEINLINE auto operator|( + U&& u, inject_run_loop p) + { + return HPX_MOVE(p).invoke(*p.loop_, HPX_FORWARD(U, u)); + } + }; } // namespace hpx::execution::experimental::detail diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index da41952550de..f6320441d561 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -29,36 +29,6 @@ namespace hpx::execution::experimental { // enforce proper formatting namespace detail { - // Recover the parent `run_loop&` from a `run_loop::scheduler`. - // - // P2300 deliberately does not provide a public API to obtain the - // owning `run_loop&` from one of its schedulers. The only way to do - // this against the current stdexec implementation is to read the - // private `__loop_` member exposed by the scheduler's environment. - // - // This helper is the single point in HPX that touches that internal - // detail. If upstream stdexec ever renames/removes `__loop_` (as - // happened with `stdexec::tags`), only this function needs to change. - // Post-cleanup follow-up of #7123. - // - // The parameter is constrained to the concrete `run_loop::scheduler` - // type (rather than a generic template) because the implementation - // depends on `.__loop_` being the specific stdexec env layout. - // `run_loop::scheduler::schedule()` is `noexcept`, so the - // unconditional `noexcept` here is sound. The function is not marked - // `constexpr` because the `stdexec::schedule` CPO wrapper is not - // declared `constexpr` (GCC strict mode rejects calling it from a - // constexpr context, even though the underlying member is constexpr). - inline hpx::execution::experimental::run_loop& - get_run_loop_from_scheduler( - decltype(std::declval() - .get_scheduler()) const& sched) noexcept - { - return static_cast( - *hpx::execution::experimental::get_env(schedule(sched)) - .__loop_); - } - template void start_operation_state(OperationState& op_state) noexcept { @@ -200,11 +170,9 @@ namespace hpx::execution::experimental { template future_data_with_run_loop(init_no_addref no_addref, other_allocator const& alloc, - decltype(std::declval() - .get_scheduler()) const& sched, - Sender&& sender) + hpx::execution::experimental::run_loop& loop_, Sender&& sender) : base_type(no_addref, alloc, HPX_FORWARD(Sender, sender)) - , loop(get_run_loop_from_scheduler(sched)) + , loop(loop_) { this->set_on_completed([this]() { loop.finish(); }); } @@ -264,9 +232,8 @@ namespace hpx::execution::experimental { /////////////////////////////////////////////////////////////////////// HPX_CXX_CORE_EXPORT template auto make_future_with_run_loop( - decltype(std::declval() - .get_scheduler()) const& sched, - Sender&& sender, Allocator const& allocator) + hpx::execution::experimental::run_loop& loop, Sender&& sender, + Allocator const& allocator) { using allocator_type = Allocator; @@ -294,7 +261,7 @@ namespace hpx::execution::experimental { hpx::util::allocator_deleter{alloc}); allocator_traits::construct(alloc, p.get(), init_no_addref{}, alloc, - sched, HPX_FORWARD(Sender, sender)); + loop, HPX_FORWARD(Sender, sender)); return hpx::traits::future_access>::create( p.release(), false); @@ -383,12 +350,11 @@ namespace hpx::execution::experimental { )> // clang-format on friend auto tag_invoke(make_future_t, - decltype(std::declval() - .get_scheduler()) const& sched, - Sender&& sender, Allocator const& allocator = Allocator{}) + hpx::execution::experimental::run_loop& loop, Sender&& sender, + Allocator const& allocator = Allocator{}) { return detail::make_future_with_run_loop( - sched, HPX_FORWARD(Sender, sender), allocator); + loop, HPX_FORWARD(Sender, sender), allocator); } // clang-format off @@ -421,6 +387,24 @@ namespace hpx::execution::experimental { HPX_FORWARD(Scheduler, scheduler), allocator}; } + // Partial-algorithm overload for the `run_loop&` form so that + // `make_future(loop)` can be piped with a sender. Returns an + // `inject_run_loop` adapter which, when piped with a sender, calls + // `tag_invoke(make_future_t, loop, sender, allocator)` above. + // clang-format off + template , + HPX_CONCEPT_REQUIRES_( + hpx::traits::is_allocator_v + )> + // clang-format on + friend constexpr HPX_FORCEINLINE auto tag_fallback_invoke(make_future_t, + hpx::execution::experimental::run_loop& loop, + Allocator const& allocator = Allocator{}) + { + return hpx::execution::experimental::detail::inject_run_loop< + make_future_t, Allocator>{loop, allocator}; + } + // clang-format off template , HPX_CONCEPT_REQUIRES_( diff --git a/libs/core/execution/tests/unit/algorithm_run_loop.cpp b/libs/core/execution/tests/unit/algorithm_run_loop.cpp index bd76b873d496..b4f829582060 100644 --- a/libs/core/execution/tests/unit/algorithm_run_loop.cpp +++ b/libs/core/execution/tests/unit/algorithm_run_loop.cpp @@ -629,7 +629,7 @@ void test_future_sender() ex::run_loop loop; [[maybe_unused]] auto sched = loop.get_scheduler(); - auto f = ex::just(3) | ex::make_future(sched); + auto f = ex::just(3) | ex::make_future(loop); HPX_TEST_EQ(f.get(), 3); } @@ -653,7 +653,7 @@ void test_future_sender() auto s1 = ex::transfer_just(sched, std::size_t(42)); auto s2 = ex::transfer_just(sched, 3.14); auto s3 = ex::transfer_just(sched, std::string("hello")); - auto f = ex::make_future(sched, + auto f = ex::make_future(loop, ex::then(ex::when_all(std::move(s1), std::move(s2), std::move(s3)), [](std::size_t x, double, std::string z) { return z.size() + x; @@ -695,7 +695,7 @@ void test_future_sender() auto s1 = ex::transfer_just(sched, std::size_t(42)); auto s2 = ex::transfer_just(sched, 3.14); auto s3 = ex::transfer_just(sched, std::string("hello")); - auto f = ex::make_future(sched, + auto f = ex::make_future(loop, ex::then(ex::when_all(std::move(s1), std::move(s2), std::move(s3)), [](std::size_t x, double, std::string z) { return z.size() + x; @@ -705,7 +705,7 @@ void test_future_sender() auto t2 = sf.then([](auto&& sf) { return sf.get() + 2; }); auto t1s = ex::then( ex::as_sender(std::move(t1)), [](std::size_t x) { return x + 1; }); - auto t1f = ex::make_future(sched, std::move(t1s)); + auto t1f = ex::make_future(loop, std::move(t1s)); auto last = hpx::dataflow( hpx::unwrapping([](std::size_t x, std::size_t y) { return x + y; }), t1f, t2); From fa6b723790e59aaadd718453ac8e863eb1fc35d4 Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Tue, 5 May 2026 23:07:03 +0530 Subject: [PATCH 02/12] address copilot review: drop dead inject_scheduler partial, pass loop explicitly in implicit-extraction tests, strengthen pipe coverage --- .../algorithms/detail/inject_scheduler.hpp | 50 +++---------------- .../hpx/execution/algorithms/make_future.hpp | 24 +++------ .../tests/unit/algorithm_run_loop.cpp | 21 +++++--- 3 files changed, 28 insertions(+), 67 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp index fc29de11e626..56311ca2b8cc 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp @@ -18,51 +18,15 @@ namespace hpx::execution::experimental::detail { - // This is a partial s/r algorithm that injects a given scheduler as the - // first argument while tag-invoking the bound algorithm. - HPX_CXX_CORE_EXPORT template - struct inject_scheduler - : partial_algorithm_base, - Ts...> - { - private: - std::decay_t scheduler; - - using base_type = partial_algorithm_base, Ts...>; - - public: - // clang-format off - template - )> - // clang-format on - explicit constexpr inject_scheduler(Scheduler_&& scheduler, Ts_&&... ts) - : base_type(HPX_FORWARD(Ts_, ts)...) - , scheduler(HPX_FORWARD(Scheduler_, scheduler)) - { - } - - // clang-format off - template - )> - // clang-format on - friend constexpr HPX_FORCEINLINE auto operator|( - U&& u, inject_scheduler p) - { - // NOLINTNEXTLINE(bugprone-use-after-move) - return HPX_MOVE(p).invoke(HPX_MOVE(p.scheduler), HPX_FORWARD(U, u)); - } - }; - - // Partial s/r algorithm sibling of `inject_scheduler` that holds a - // reference to a `run_loop` instead of a scheduler. Used so that + // Partial s/r algorithm that holds a reference to a `run_loop` so that // `make_future(loop)` yields a pipe-able adapter which, when piped with // a sender, dispatches `tag_invoke(Tag, run_loop&, sender, ...)`. + // + // (A previous `inject_scheduler` sibling that held a scheduler instead + // was removed when the `make_future` API moved to taking `run_loop&` + // explicitly: there was no longer any binary + // `tag_invoke(Tag, scheduler, sender, alloc)` overload for the partial + // to dispatch to.) HPX_CXX_CORE_EXPORT template struct inject_run_loop : partial_algorithm_base, diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index f6320441d561..406dcaf176e0 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -371,26 +371,18 @@ namespace hpx::execution::experimental { return detail::make_future(HPX_FORWARD(Sender, sender), allocator); } - // clang-format off - template , - HPX_CONCEPT_REQUIRES_( - hpx::execution::experimental::is_scheduler_v && - hpx::traits::is_allocator_v - )> - // clang-format on - friend constexpr HPX_FORCEINLINE auto tag_fallback_invoke(make_future_t, - Scheduler&& scheduler, Allocator const& allocator = Allocator{}) - { - return hpx::execution::experimental::detail::inject_scheduler< - make_future_t, Scheduler, Allocator>{ - HPX_FORWARD(Scheduler, scheduler), allocator}; - } - // Partial-algorithm overload for the `run_loop&` form so that // `make_future(loop)` can be piped with a sender. Returns an // `inject_run_loop` adapter which, when piped with a sender, calls // `tag_invoke(make_future_t, loop, sender, allocator)` above. + // + // Note: there is intentionally no `make_future(scheduler)` partial + // adapter. The previous `inject_scheduler` partial only worked for + // `run_loop::scheduler` (no other scheduler had a matching binary + // `tag_invoke(make_future_t, scheduler, sender, alloc)` overload), + // and that scheduler-based path has been replaced by the + // `make_future(loop, sender)` form above. Other schedulers were + // never supported through this partial. // clang-format off template , HPX_CONCEPT_REQUIRES_( diff --git a/libs/core/execution/tests/unit/algorithm_run_loop.cpp b/libs/core/execution/tests/unit/algorithm_run_loop.cpp index b4f829582060..2c49f4f96e86 100644 --- a/libs/core/execution/tests/unit/algorithm_run_loop.cpp +++ b/libs/core/execution/tests/unit/algorithm_run_loop.cpp @@ -611,7 +611,7 @@ void test_future_sender() [[maybe_unused]] auto sched = loop.get_scheduler(); auto s = ex::transfer_just(sched, 3); - auto f = ex::make_future(std::move(s)); + auto f = ex::make_future(loop, std::move(s)); HPX_TEST_EQ(f.get(), 3); } @@ -620,7 +620,7 @@ void test_future_sender() ex::run_loop loop; [[maybe_unused]] auto sched = loop.get_scheduler(); - auto f = ex::transfer_just(sched, 3) | ex::make_future(); + auto f = ex::transfer_just(sched, 3) | ex::make_future(loop); HPX_TEST_EQ(f.get(), 3); } @@ -629,7 +629,12 @@ void test_future_sender() ex::run_loop loop; [[maybe_unused]] auto sched = loop.get_scheduler(); - auto f = ex::just(3) | ex::make_future(loop); + // Use transfer_just(sched, ...) so the partial-pipe path actually + // schedules work onto `loop` and depends on `loop.run()` being + // driven by the resulting future's `get()` (a bug in the + // `inject_run_loop` partial would hang here, not silently pass + // as it would with `just(3)`). + auto f = ex::transfer_just(sched, 3) | ex::make_future(loop); HPX_TEST_EQ(f.get(), 3); } @@ -640,7 +645,7 @@ void test_future_sender() std::atomic called{false}; auto s = ex::schedule(sched) | ex::then([&] { called = true; }); - auto f = ex::make_future(std::move(s)); + auto f = ex::make_future(loop, std::move(s)); f.get(); HPX_TEST(called); } @@ -667,7 +672,7 @@ void test_future_sender() ex::run_loop loop; [[maybe_unused]] auto sched = loop.get_scheduler(); auto result = tt::sync_wait( - ex::as_sender(ex::make_future(ex::transfer_just(sched, 42)))); + ex::as_sender(ex::make_future(loop, ex::transfer_just(sched, 42)))); HPX_TEST_EQ(hpx::get<0>(*result), 42); } @@ -681,9 +686,9 @@ void test_future_sender() return 42; }); - HPX_TEST_EQ( - ex::make_future(ex::transfer(ex::as_sender(std::move(f)), sched)) - .get(), + HPX_TEST_EQ(ex::make_future( + loop, ex::transfer(ex::as_sender(std::move(f)), sched)) + .get(), 42); } From 036416f3ba1639124c650d841584904c1a0714cd Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 01:31:18 +0530 Subject: [PATCH 03/12] keep inject_scheduler in place, add inject_run_loop alongside --- .../algorithms/detail/inject_scheduler.hpp | 54 +++++++++++++++---- .../hpx/execution/algorithms/make_future.hpp | 24 ++++++--- 2 files changed, 61 insertions(+), 17 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp index 56311ca2b8cc..f90c4f60496a 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp @@ -18,15 +18,51 @@ namespace hpx::execution::experimental::detail { - // Partial s/r algorithm that holds a reference to a `run_loop` so that - // `make_future(loop)` yields a pipe-able adapter which, when piped with - // a sender, dispatches `tag_invoke(Tag, run_loop&, sender, ...)`. - // - // (A previous `inject_scheduler` sibling that held a scheduler instead - // was removed when the `make_future` API moved to taking `run_loop&` - // explicitly: there was no longer any binary - // `tag_invoke(Tag, scheduler, sender, alloc)` overload for the partial - // to dispatch to.) + // This is a partial s/r algorithm that injects a given scheduler as the + // first argument while tag-invoking the bound algorithm. + HPX_CXX_CORE_EXPORT template + struct inject_scheduler + : partial_algorithm_base, + Ts...> + { + private: + std::decay_t scheduler; + + using base_type = partial_algorithm_base, Ts...>; + + public: + // clang-format off + template + )> + // clang-format on + explicit constexpr inject_scheduler(Scheduler_&& scheduler, Ts_&&... ts) + : base_type(HPX_FORWARD(Ts_, ts)...) + , scheduler(HPX_FORWARD(Scheduler_, scheduler)) + { + } + + // clang-format off + template + )> + // clang-format on + friend constexpr HPX_FORCEINLINE auto operator|( + U&& u, inject_scheduler p) + { + // NOLINTNEXTLINE(bugprone-use-after-move) + return HPX_MOVE(p).invoke(HPX_MOVE(p.scheduler), HPX_FORWARD(U, u)); + } + }; + + // Sibling of `inject_scheduler` that holds a reference to a `run_loop` + // instead of a scheduler. Used so that `make_future(loop)` yields a + // pipe-able adapter which, when piped with a sender, dispatches + // `tag_invoke(Tag, run_loop&, sender, ...)`. HPX_CXX_CORE_EXPORT template struct inject_run_loop : partial_algorithm_base, diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index 406dcaf176e0..f6320441d561 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -371,18 +371,26 @@ namespace hpx::execution::experimental { return detail::make_future(HPX_FORWARD(Sender, sender), allocator); } + // clang-format off + template , + HPX_CONCEPT_REQUIRES_( + hpx::execution::experimental::is_scheduler_v && + hpx::traits::is_allocator_v + )> + // clang-format on + friend constexpr HPX_FORCEINLINE auto tag_fallback_invoke(make_future_t, + Scheduler&& scheduler, Allocator const& allocator = Allocator{}) + { + return hpx::execution::experimental::detail::inject_scheduler< + make_future_t, Scheduler, Allocator>{ + HPX_FORWARD(Scheduler, scheduler), allocator}; + } + // Partial-algorithm overload for the `run_loop&` form so that // `make_future(loop)` can be piped with a sender. Returns an // `inject_run_loop` adapter which, when piped with a sender, calls // `tag_invoke(make_future_t, loop, sender, allocator)` above. - // - // Note: there is intentionally no `make_future(scheduler)` partial - // adapter. The previous `inject_scheduler` partial only worked for - // `run_loop::scheduler` (no other scheduler had a matching binary - // `tag_invoke(make_future_t, scheduler, sender, alloc)` overload), - // and that scheduler-based path has been replaced by the - // `make_future(loop, sender)` form above. Other schedulers were - // never supported through this partial. // clang-format off template , HPX_CONCEPT_REQUIRES_( From 1fc14012753b3965b55280a89077bd1cdcc4a95e Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 01:41:35 +0530 Subject: [PATCH 04/12] cover make_future(sender) and sender|make_future() public paths via separate driver thread --- .../tests/unit/algorithm_run_loop.cpp | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/libs/core/execution/tests/unit/algorithm_run_loop.cpp b/libs/core/execution/tests/unit/algorithm_run_loop.cpp index 2c49f4f96e86..a67297aabf01 100644 --- a/libs/core/execution/tests/unit/algorithm_run_loop.cpp +++ b/libs/core/execution/tests/unit/algorithm_run_loop.cpp @@ -717,6 +717,38 @@ void test_future_sender() HPX_TEST_EQ(last.get(), std::size_t(18)); } + + // The following two cases cover the public `make_future(sender)` and + // `sender | make_future()` paths for run-loop-backed senders. After + // this PR these no longer drive the loop themselves (only the + // explicit `make_future(loop, sender)` form does), but the overloads + // are still public — exercising them here with a separate driver + // thread keeps that contract test-covered. + std::cout << "9 (sender_only_make_future_with_driver)\n"; + { + ex::run_loop loop; + auto driver = hpx::thread([&] { loop.run(); }); + [[maybe_unused]] auto sched = loop.get_scheduler(); + + auto f = ex::make_future(ex::transfer_just(sched, 3)); + HPX_TEST_EQ(f.get(), 3); + + loop.finish(); + driver.join(); + } + + std::cout << "10 (sender_pipe_make_future_with_driver)\n"; + { + ex::run_loop loop; + auto driver = hpx::thread([&] { loop.run(); }); + [[maybe_unused]] auto sched = loop.get_scheduler(); + + auto f = ex::transfer_just(sched, 3) | ex::make_future(); + HPX_TEST_EQ(f.get(), 3); + + loop.finish(); + driver.join(); + } } void test_ensure_started() From c1d87e1c7f150b129c6e406479bfb92203c1f608 Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 14:57:13 +0530 Subject: [PATCH 05/12] address copilot review: reject make_future(run_loop::scheduler) at call site, add explicit run_loop include in inject_scheduler.hpp --- .../algorithms/detail/inject_scheduler.hpp | 1 + .../hpx/execution/algorithms/make_future.hpp | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp index f90c4f60496a..28b76382d3d0 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include #include diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index f6320441d561..842e88d99c84 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -387,6 +387,25 @@ namespace hpx::execution::experimental { HPX_FORWARD(Scheduler, scheduler), allocator}; } + // `make_future(loop.get_scheduler())` is intentionally rejected at + // the call site: the run-loop-backed binary + // `tag_invoke(make_future_t, run_loop::scheduler, sender, alloc)` + // overload is gone (replaced by the `run_loop&` overload above). + // Without this delete, the generic scheduler partial above would + // accept the call, return an `inject_scheduler`, and fail with a + // confusing substitution error only when the partial is later piped + // with a sender. Use `make_future(loop, sender)` / + // `sender | make_future(loop)` instead. + // clang-format off + template , + HPX_CONCEPT_REQUIRES_( + hpx::traits::is_allocator_v + )> + // clang-format on + friend constexpr auto tag_fallback_invoke(make_future_t, + typename hpx::execution::experimental::run_loop::scheduler const&, + Allocator const& = Allocator{}) = delete; + // Partial-algorithm overload for the `run_loop&` form so that // `make_future(loop)` can be piped with a sender. Returns an // `inject_run_loop` adapter which, when piped with a sender, calls From a3ec4883b870a6f1c6eb2548a0118b969576af71 Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 15:03:38 +0530 Subject: [PATCH 06/12] fix GCC: split deleted make_future(run_loop::scheduler) overload (no defaulted template arg on friend) --- .../hpx/execution/algorithms/make_future.hpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index 842e88d99c84..c4b1831de632 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -391,20 +391,28 @@ namespace hpx::execution::experimental { // the call site: the run-loop-backed binary // `tag_invoke(make_future_t, run_loop::scheduler, sender, alloc)` // overload is gone (replaced by the `run_loop&` overload above). - // Without this delete, the generic scheduler partial above would + // Without these deletes, the generic scheduler partial above would // accept the call, return an `inject_scheduler`, and fail with a // confusing substitution error only when the partial is later piped // with a sender. Use `make_future(loop, sender)` / // `sender | make_future(loop)` instead. + // + // Two deleted overloads (one per arity) rather than one with a + // defaulted Allocator: GCC rejects defaulted template arguments on + // template friend declarations, even when the friend is `= delete`d. + friend constexpr auto tag_fallback_invoke(make_future_t, + typename hpx::execution::experimental::run_loop::scheduler const&) = + delete; + // clang-format off - template , + template )> // clang-format on friend constexpr auto tag_fallback_invoke(make_future_t, typename hpx::execution::experimental::run_loop::scheduler const&, - Allocator const& = Allocator{}) = delete; + Allocator const&) = delete; // Partial-algorithm overload for the `run_loop&` form so that // `make_future(loop)` can be piped with a sender. Returns an From 67fd954b7fb51967081d85dc30a1c78ae295bdce Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 15:10:49 +0530 Subject: [PATCH 07/12] fix GCC: drop HPX_CONCEPT_REQUIRES from deleted friend overloads (defaulted template args reject = delete on friends) --- .../hpx/execution/algorithms/make_future.hpp | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index c4b1831de632..5a1628133a7f 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -397,22 +397,21 @@ namespace hpx::execution::experimental { // with a sender. Use `make_future(loop, sender)` / // `sender | make_future(loop)` instead. // - // Two deleted overloads (one per arity) rather than one with a - // defaulted Allocator: GCC rejects defaulted template arguments on - // template friend declarations, even when the friend is `= delete`d. + // Two deleted overloads (one per arity), and neither uses + // `HPX_CONCEPT_REQUIRES_`: that macro expands to a defaulted + // template parameter, and GCC rejects defaulted template arguments + // on friend *declarations* (which a `= delete`d friend is). The + // 2-argument overload therefore takes any second arg (allocator or + // not); that's intentional — `make_future(run_loop::scheduler, X)` + // shouldn't compile regardless of `X`. friend constexpr auto tag_fallback_invoke(make_future_t, typename hpx::execution::experimental::run_loop::scheduler const&) = delete; - // clang-format off - template - )> - // clang-format on + template friend constexpr auto tag_fallback_invoke(make_future_t, typename hpx::execution::experimental::run_loop::scheduler const&, - Allocator const&) = delete; + T const&) = delete; // Partial-algorithm overload for the `run_loop&` form so that // `make_future(loop)` can be piped with a sender. Returns an From 644f8b251bc9835702639b39424aef081f514ecd Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 16:16:09 +0530 Subject: [PATCH 08/12] address copilot review: drop redundant typename on non-dependent name, add NOLINT for use-after-move in inject_run_loop --- .../include/hpx/execution/algorithms/detail/inject_scheduler.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp index 28b76382d3d0..d4c062f40c24 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/detail/inject_scheduler.hpp @@ -96,6 +96,7 @@ namespace hpx::execution::experimental::detail { friend constexpr HPX_FORCEINLINE auto operator|( U&& u, inject_run_loop p) { + // NOLINTNEXTLINE(bugprone-use-after-move) return HPX_MOVE(p).invoke(*p.loop_, HPX_FORWARD(U, u)); } }; From e3a968e7076c3eaf4487ec781394df3243d5b90b Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 16:18:01 +0530 Subject: [PATCH 09/12] drop redundant typename on non-dependent run_loop::scheduler in deleted overloads --- .../include/hpx/execution/algorithms/make_future.hpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index 5a1628133a7f..08c43355e594 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -405,12 +405,11 @@ namespace hpx::execution::experimental { // not); that's intentional — `make_future(run_loop::scheduler, X)` // shouldn't compile regardless of `X`. friend constexpr auto tag_fallback_invoke(make_future_t, - typename hpx::execution::experimental::run_loop::scheduler const&) = - delete; + hpx::execution::experimental::run_loop::scheduler const&) = delete; template friend constexpr auto tag_fallback_invoke(make_future_t, - typename hpx::execution::experimental::run_loop::scheduler const&, + hpx::execution::experimental::run_loop::scheduler const&, T const&) = delete; // Partial-algorithm overload for the `run_loop&` form so that From ef3c514dc455d954e1cc9ecd1ff66062d13df4ee Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Wed, 6 May 2026 22:08:22 +0530 Subject: [PATCH 10/12] fail loudly: static_assert make_future(sender) when completion_scheduler is run_loop::scheduler --- .../hpx/execution/algorithms/make_future.hpp | 43 +++++++++++++++++++ .../tests/unit/algorithm_run_loop.cpp | 39 ++++------------- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index 08c43355e594..0ab549d2869f 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -190,10 +190,53 @@ namespace hpx::execution::experimental { } }; + // Detects whether a sender's `set_value` completion scheduler is + // `run_loop::scheduler`. Used by `detail::make_future` to reject — + // at compile time — calls of the form `make_future(transfer_just( + // run_loop_sched, ...))` etc., which previously relied on HPX + // recovering the parent `run_loop&` from the scheduler via stdexec + // internals; that path was removed in this refactor and the new + // `make_future(loop, sender)` form must be used instead. + template + struct sender_completion_is_run_loop_scheduler : std::false_type + { + }; + + template + struct sender_completion_is_run_loop_scheduler(hpx::execution:: + experimental::get_env(std::declval())))>> + : std::is_same( + hpx::execution::experimental::get_env( + std::declval())))>, + hpx::execution::experimental::run_loop::scheduler> + { + }; + /////////////////////////////////////////////////////////////////////// HPX_CXX_CORE_EXPORT template auto make_future(Sender&& sender, Allocator const& allocator) { + // The previous implementation recovered the parent `run_loop&` + // from the sender's completion scheduler (a `run_loop::scheduler`) + // via stdexec internals so it could call `loop.run()` from the + // future's `get()`. That path is gone; without an explicit + // `run_loop&`, this `detail::make_future` would silently produce + // a future that hangs because nothing drives the loop. Reject + // such calls at compile time and direct the user to + // `make_future(loop, sender)`. + static_assert(!sender_completion_is_run_loop_scheduler< + std::decay_t>::value, + "make_future(sender) cannot drive the parent run_loop when " + "the sender's completion scheduler is a run_loop::scheduler. " + "Use make_future(loop, sender) — pass the run_loop reference " + "explicitly so the resulting future can drive the loop in " + "its get(). See docs/migration/stdexec-mandatory.md."); + using allocator_type = Allocator; using value_types = hpx::execution::experimental::value_types_of_t< diff --git a/libs/core/execution/tests/unit/algorithm_run_loop.cpp b/libs/core/execution/tests/unit/algorithm_run_loop.cpp index a67297aabf01..8a178a18e294 100644 --- a/libs/core/execution/tests/unit/algorithm_run_loop.cpp +++ b/libs/core/execution/tests/unit/algorithm_run_loop.cpp @@ -718,37 +718,14 @@ void test_future_sender() HPX_TEST_EQ(last.get(), std::size_t(18)); } - // The following two cases cover the public `make_future(sender)` and - // `sender | make_future()` paths for run-loop-backed senders. After - // this PR these no longer drive the loop themselves (only the - // explicit `make_future(loop, sender)` form does), but the overloads - // are still public — exercising them here with a separate driver - // thread keeps that contract test-covered. - std::cout << "9 (sender_only_make_future_with_driver)\n"; - { - ex::run_loop loop; - auto driver = hpx::thread([&] { loop.run(); }); - [[maybe_unused]] auto sched = loop.get_scheduler(); - - auto f = ex::make_future(ex::transfer_just(sched, 3)); - HPX_TEST_EQ(f.get(), 3); - - loop.finish(); - driver.join(); - } - - std::cout << "10 (sender_pipe_make_future_with_driver)\n"; - { - ex::run_loop loop; - auto driver = hpx::thread([&] { loop.run(); }); - [[maybe_unused]] auto sched = loop.get_scheduler(); - - auto f = ex::transfer_just(sched, 3) | ex::make_future(); - HPX_TEST_EQ(f.get(), 3); - - loop.finish(); - driver.join(); - } + // Note: `make_future(sender)` / `sender | make_future()` for senders + // whose completion scheduler is a `run_loop::scheduler` is rejected at + // compile time by a `static_assert` in `detail::make_future`. Those + // forms previously relied on HPX recovering the parent `run_loop&` from + // the scheduler via stdexec internals; with that path removed, the only + // way to drive the loop from the future is to pass `run_loop&` + // explicitly via `make_future(loop, sender)` (covered by tests 1-8 and + // the run-loop-driven cases above). } void test_ensure_started() From fc838308e3439ec8149df59924d6fc24d0468990 Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Thu, 7 May 2026 10:55:11 +0530 Subject: [PATCH 11/12] fix HPX inspect: replace non-ASCII em-dashes with -- in comments --- .../include/hpx/execution/algorithms/make_future.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index 0ab549d2869f..728bbeb9c170 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -191,8 +191,8 @@ namespace hpx::execution::experimental { }; // Detects whether a sender's `set_value` completion scheduler is - // `run_loop::scheduler`. Used by `detail::make_future` to reject — - // at compile time — calls of the form `make_future(transfer_just( + // `run_loop::scheduler`. Used by `detail::make_future` to reject -- + // at compile time -- calls of the form `make_future(transfer_just( // run_loop_sched, ...))` etc., which previously relied on HPX // recovering the parent `run_loop&` from the scheduler via stdexec // internals; that path was removed in this refactor and the new @@ -233,7 +233,7 @@ namespace hpx::execution::experimental { std::decay_t>::value, "make_future(sender) cannot drive the parent run_loop when " "the sender's completion scheduler is a run_loop::scheduler. " - "Use make_future(loop, sender) — pass the run_loop reference " + "Use make_future(loop, sender) -- pass the run_loop reference " "explicitly so the resulting future can drive the loop in " "its get(). See docs/migration/stdexec-mandatory.md."); @@ -445,7 +445,7 @@ namespace hpx::execution::experimental { // template parameter, and GCC rejects defaulted template arguments // on friend *declarations* (which a `= delete`d friend is). The // 2-argument overload therefore takes any second arg (allocator or - // not); that's intentional — `make_future(run_loop::scheduler, X)` + // not); that's intentional -- `make_future(run_loop::scheduler, X)` // shouldn't compile regardless of `X`. friend constexpr auto tag_fallback_invoke(make_future_t, hpx::execution::experimental::run_loop::scheduler const&) = delete; From a0c1c12caaa9db0bd4f4a22e093bca933eeb29ab Mon Sep 17 00:00:00 2001 From: guptapratykshh Date: Fri, 8 May 2026 09:27:25 +0530 Subject: [PATCH 12/12] drop dangling docs/migration/ reference from static_assert message Signed-off-by: guptapratykshh --- .../execution/include/hpx/execution/algorithms/make_future.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp index 728bbeb9c170..0b99939dae51 100644 --- a/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp +++ b/libs/core/execution/include/hpx/execution/algorithms/make_future.hpp @@ -235,7 +235,7 @@ namespace hpx::execution::experimental { "the sender's completion scheduler is a run_loop::scheduler. " "Use make_future(loop, sender) -- pass the run_loop reference " "explicitly so the resulting future can drive the loop in " - "its get(). See docs/migration/stdexec-mandatory.md."); + "its get()."); using allocator_type = Allocator;