diff --git a/example/awaitable-sender/CMakeLists.txt b/example/awaitable-sender/CMakeLists.txt index 40f0ff8fe..94e60f3fe 100644 --- a/example/awaitable-sender/CMakeLists.txt +++ b/example/awaitable-sender/CMakeLists.txt @@ -10,12 +10,14 @@ include(FetchContent) FetchContent_Declare( - execution - GIT_REPOSITORY https://github.com/bemanproject/execution.git - GIT_TAG main + stdexec + GIT_REPOSITORY https://github.com/NVIDIA/stdexec + GIT_TAG 307b83c5689ea7c2e5b31561cdc428697705333e SYSTEM + FIND_PACKAGE_ARGS + NAMES stdexec ) -FetchContent_MakeAvailable(execution) +FetchContent_MakeAvailable(stdexec) add_executable(capy_example_awaitable_sender awaitable_sender.cpp @@ -36,7 +38,7 @@ foreach(tgt capy_example_awaitable_sender capy_example_awaitable_sender_test) target_compile_features(${tgt} PRIVATE cxx_std_23) target_link_libraries(${tgt} Boost::capy - beman::execution_headers) + STDEXEC::stdexec) endforeach() add_test(NAME capy_example_awaitable_sender_test diff --git a/example/awaitable-sender/awaitable_sender.cpp b/example/awaitable-sender/awaitable_sender.cpp index 7a92cf6a3..58c18f907 100644 --- a/example/awaitable-sender/awaitable_sender.cpp +++ b/example/awaitable-sender/awaitable_sender.cpp @@ -14,9 +14,10 @@ #include -#include +#include #include +#include #include #include #include @@ -24,7 +25,7 @@ #include namespace capy = boost::capy; -namespace ex = beman::execution; +namespace ex = stdexec; // A receiver whose environment carries a Capy executor. // Completion signals a latch so main() can wait. @@ -187,14 +188,13 @@ int main() std::cout << "\n--- native awaitable-sender test ---\n"; std::latch done5(1); - auto rop = capy::read_op::result({}, 42); + auto rop = capy::read_op::result({}); auto op5 = ex::connect( ex::then( std::move(rop), - [](std::size_t n) + [] { - std::cout - << " read " << n << " bytes\n"; + std::cout << " read completed\n"; }), demo_receiver{ {pool_ex, std::stop_token{}}, @@ -204,6 +204,29 @@ int main() done5.wait(); std::cout << " native awaitable-sender test done\n"; + // A byte-producing op: the count lives in caller-owned state + // (like the buffer it would describe) and is already valid + // when the continuation runs; the completion signal carries + // only the disposition. + std::cout << "\n--- counted read test ---\n"; + std::latch done6(1); + + std::size_t n = 0; + auto op6 = ex::connect( + ex::then( + capy::counted_read_op::result({}, 42, &n), + [&n] + { + std::cout << " read " << n << " bytes\n"; + }), + demo_receiver{ + {pool_ex, std::stop_token{}}, + &done6}); + + ex::start(op6); + done6.wait(); + std::cout << " counted read test done\n"; + // All demos have drained; safe to join the waker thread now. waker_thread.join(); } diff --git a/example/awaitable-sender/awaitable_sender.hpp b/example/awaitable-sender/awaitable_sender.hpp index 0ae61617c..1fc9cadbb 100644 --- a/example/awaitable-sender/awaitable_sender.hpp +++ b/example/awaitable-sender/awaitable_sender.hpp @@ -16,7 +16,7 @@ #include -#include +#include #include #include @@ -51,14 +51,6 @@ struct awaitable_sender return decltype(detail::make_sigs()){}; } - /// beman-compat form: DROP AT GRADUATION (pre-P3164 protocol). - template - constexpr auto get_completion_signatures( - Env const&) const noexcept - { - return decltype(detail::make_sigs()){}; - } - /// Connect this sender with a receiver to form an operation state. template auto connect(Receiver rcvr) && @@ -87,16 +79,19 @@ struct awaitable_sender - `error_code` or an empty `io_result` - calls `set_value()` when the code is zero, `set_error(ec)` otherwise. - - `io_result` with payload elements - calls - `set_value(ts...)` when `ec` is zero, `set_error(ec)` - otherwise. Any partial payload accompanying a truthy - `ec` is dropped, since sender completion channels are - exclusive. - Any other single value `T` - calls `set_value(T)`, including generic tuple-likes that happen to lead with an `error_code`: only `io_result` declares the element-0-is-outcome intent, so only it is split. + An `io_result` with payload elements is rejected at compile + time. Completion channels are exclusive, so a partial + success (an `error_code` arriving alongside bytes already + transferred) cannot be delivered on any single channel + without dropping data. Wrap such an operation in a + `task` that inspects the full result, moves the + payload out through a side channel, and returns the code. + For the `error_code`-carrying result types the channel is chosen by the operation's own disposition: an `ec` that compares equal to `errc::operation_canceled` completes with @@ -118,6 +113,15 @@ struct awaitable_sender template auto as_sender(IoAw&& aw) { + using R = awaitable_result_t>; + static_assert( + !detail::is_compound_ec_result_v, + "as_sender does not accept awaitables whose result is an " + "io_result with payload elements: completion channels " + "are exclusive, so a partial success (error_code plus " + "payload) would be silently dropped. Wrap the operation " + "in a task that inspects the full result " + "and returns the error code."); return awaitable_sender>{ std::forward(aw)}; } @@ -163,21 +167,13 @@ struct split_ec_sender Sender sndr_; - // C++26 static-template form plus the beman-compat instance - // form (DROP the latter at graduation; pre-P3164 protocol). + // C++26 static-template form ([exec.getcomplsigs]). template static consteval auto get_completion_signatures() noexcept { return sigs_type{}; } - template - constexpr auto get_completion_signatures( - Env const&) const noexcept - { - return sigs_type{}; - } - template struct ec_receiver { diff --git a/example/awaitable-sender/awaitable_sender_base.hpp b/example/awaitable-sender/awaitable_sender_base.hpp index e0491e6bc..4a8fe34ce 100644 --- a/example/awaitable-sender/awaitable_sender_base.hpp +++ b/example/awaitable-sender/awaitable_sender_base.hpp @@ -12,7 +12,6 @@ #include "awaitable_sender_detail.hpp" -#include #include namespace boost::capy { @@ -44,28 +43,6 @@ concept AwaitableSender = IoAwaitable && ex::sender; -namespace detail { - -// Shared by both connect() overloads so const& connect gets the -// same check as the rvalue overload. Scaffolding-era check, DROP -// AT GRADUATION with the beman-compat query forms: the bundled -// implementation probes decomposable senders by aggregate -// brace-init and hard-errors on arity mismatches. Under the -// adopted wording the durable opt-out is private data members -// (see the class note); aggregates merely take a guarded -// fallback path there. -template -constexpr void check_awaitable_sender() -{ - static_assert( - !std::is_aggregate_v, - "Derived must not be an aggregate; keep its data " - "members private (or declare a constructor) so " - "sender decomposition cannot claim it"); -} - -} // namespace detail - /** CRTP mixin that makes an IoAwaitable a sender. Deriving from this base adds the C++26 sender interface to @@ -94,10 +71,7 @@ constexpr void check_awaitable_sender() first member as a sender tag; private members make the binding ill-formed, so the op is categorically non-decomposable and independent of the dispatch - machinery's guarded fallbacks. (`connect` additionally - rejects aggregates; that check accommodates the bundled - pre-standard implementation and is dropped at - graduation.) + machinery's guarded fallbacks. @par Example @code @@ -129,27 +103,11 @@ struct awaitable_sender_base return decltype(detail::make_sigs()){}; } - /** Return the completion signatures deduced from `Derived`. - - beman-compat form: DROP AT GRADUATION. beman::execution - still implements the pre-P3164 protocol and probes for an - instance member called with the environment as a function - argument; the adopted C++26 wording recognizes only the - static template form above. - */ - template - constexpr auto get_completion_signatures( - Env const&) const noexcept - { - return decltype(detail::make_sigs()){}; - } - /// Connect the op to a receiver, consuming it. template auto connect(Receiver rcvr) && -> detail::awaitable_op_state { - detail::check_awaitable_sender(); return detail::awaitable_op_state( static_cast(*this), std::move(rcvr)); } @@ -159,7 +117,6 @@ struct awaitable_sender_base auto connect(Receiver rcvr) const& -> detail::awaitable_op_state { - detail::check_awaitable_sender(); return detail::awaitable_op_state( static_cast(*this), std::move(rcvr)); diff --git a/example/awaitable-sender/awaitable_sender_detail.hpp b/example/awaitable-sender/awaitable_sender_detail.hpp index df29d66c0..48405ccf5 100644 --- a/example/awaitable-sender/awaitable_sender_detail.hpp +++ b/example/awaitable-sender/awaitable_sender_detail.hpp @@ -17,7 +17,7 @@ #include #include -#include +#include #include #include @@ -32,7 +32,7 @@ namespace boost::capy { // On graduation this alias flips to std::execution, gated on // __cpp_lib_senders. -namespace ex = beman::execution; +namespace ex = stdexec; // ------------------------------------------------------- // CPO: query a receiver environment for a Capy executor @@ -85,9 +85,12 @@ namespace detail { // declares "element 0 is an error_code" as intent. A generic // tuple-like that merely happens to lead with an error_code is a // value, not an outcome to split — shape alone cannot tell a -// result protocol from a payload. The arity trait avoids naming -// std::tuple_size on foreign types, which would hard-error for -// non-tuples (&& does not short-circuit instantiation). +// result protocol from a payload. An io_result with payload +// elements is rejected in make_sigs: exclusive completion +// channels cannot carry a partial success without dropping data. +// The arity trait avoids naming std::tuple_size on foreign +// types, which would hard-error for non-tuples (&& does not +// short-circuit instantiation). template struct io_result_arity : std::integral_constant {}; @@ -128,18 +131,22 @@ auto concat_sigs( ex::completion_signatures) -> ex::completion_signatures; -template -auto compound_value_sig(std::index_sequence) - -> ex::completion_signatures< - ex::set_value_t(std::tuple_element_t...), - ex::set_error_t(std::error_code)>; - // Deduce completion signatures from an awaitable's result type. template auto make_sigs() { using A = std::decay_t; using R = awaitable_result_t; + + static_assert( + !is_compound_ec_result_v, + "IoAwaitables whose result is an io_result with payload " + "elements cannot be senders: completion channels are " + "exclusive, so a partial success (error_code plus " + "payload) would be silently dropped. Wrap the operation " + "in a task that inspects the full result " + "and returns the error code."); + constexpr bool nothrow_resume = noexcept(std::declval().await_resume()); @@ -151,10 +158,6 @@ auto make_sigs() return ex::completion_signatures< ex::set_value_t(), ex::set_error_t(std::error_code)>{}; - else if constexpr (is_compound_ec_result_v) - return decltype(compound_value_sig( - std::make_index_sequence< - std::tuple_size_v - 1>{})){}; else return ex::completion_signatures< ex::set_value_t(R)>{}; @@ -173,7 +176,7 @@ auto make_sigs() return decltype(concat_sigs(base, tail)){}; } -// beman's get_stop_token CPO requires the C++26 stoppable_token +// stdexec's get_stop_token CPO requires the C++26 stoppable_token // concept, which today's std::stop_token fails (no callback_type); // query the environment directly so std::stop_token environments // keep cancellation until stdlibs catch up. @@ -481,30 +484,6 @@ struct awaitable_op_state ex::set_error( std::move(rcvr_), ec); } - else if constexpr (is_compound_ec_result_v) - { - auto result = aw_.await_resume(); - auto ec = get<0>(result); - if(ec) - { - if(ec == std::errc::operation_canceled) - ex::set_stopped( - std::move(rcvr_)); - else - ex::set_error( - std::move(rcvr_), ec); - } - else - { - [&](std::index_sequence) - { - ex::set_value( - std::move(rcvr_), - get(std::move(result))...); - }(std::make_index_sequence< - std::tuple_size_v - 1>{}); - } - } else { auto result = aw_.await_resume(); diff --git a/example/awaitable-sender/read_op.hpp b/example/awaitable-sender/read_op.hpp index 738d79d22..741488d39 100644 --- a/example/awaitable-sender/read_op.hpp +++ b/example/awaitable-sender/read_op.hpp @@ -32,18 +32,17 @@ namespace boost::capy { @ref awaitable_sender_base. Its stop-wait mode composes `async_waker` rather than hand-rolling stop-callback arming: the waker's arbiter already resolves the races between - arming, stop requests, and completion. + arming, stop requests, and completion. Completes with a bare + `io_result<>`: a result carrying payload alongside the + `error_code` cannot be a sender (see @ref as_sender). */ class read_op : public awaitable_sender_base { // Data members are private so the op is invisible to // std::execution sender decomposition: tag_of_t claims any // type whose members admit a structured binding, and - // inaccessible members make that binding ill-formed (it also - // keeps the type a non-aggregate, which is the condition - // today's implementations actually probe). + // inaccessible members make that binding ill-formed. std::error_code ec_{}; - std::size_t n_ = 0; bool immediate_ = false; io_env const* env_ = nullptr; @@ -62,29 +61,21 @@ class read_op : public awaitable_sender_base } public: - /// Construct an op that completes successfully with zero bytes. + /// Construct an op that completes successfully. read_op() = default; - // Scripted construction goes through factories, not multi-arg - // constructors — a scaffolding accommodation, unnecessary at - // graduation: the bundled pre-standard implementation probes - // decomposable senders by brace-initializing with 2..6 - // placeholder arguments, and any constructor of that arity - // matches the probe and re-enters its decomposition machinery. - - /// Create an op with a scripted `(ec, n)` completion. - static read_op result(std::error_code ec, std::size_t n) + /// Create an op with a scripted `error_code` completion. + static read_op result(std::error_code ec) { read_op op; op.ec_ = ec; - op.n_ = n; return op; } /// Create an op that completes inline via `await_ready`. - static read_op immediate(std::error_code ec, std::size_t n) + static read_op immediate(std::error_code ec) { - read_op op = result(ec, n); + read_op op = result(ec); op.immediate_ = true; return op; } @@ -115,7 +106,7 @@ class read_op : public awaitable_sender_base return std::noop_coroutine(); } - io_result await_resume() noexcept + io_result<> await_resume() noexcept { // The op reports its own disposition in-band: a stop-wait // ends with the waker's error::canceled, which the sender @@ -124,9 +115,66 @@ class read_op : public awaitable_sender_base if(stop_task_) { auto [ec] = stop_task_->await_resume(); - return {ec ? ec : ec_, n_}; + return {ec ? ec : ec_}; } - return {ec_, n_}; + return {ec_}; + } +}; + +/** Mock byte-producing op: payload via caller-owned state. + + Demonstrates how a base-derived op delivers a count now that + compound results cannot be senders: `await_resume()` reports + only the disposition, and the byte count goes to a + caller-owned location, the same ownership model as the buffer + it would describe. The base has no wrapper seam to intercept, + so the side channel is baked in at op-design time. +*/ +class counted_read_op + : public awaitable_sender_base +{ + std::error_code ec_{}; + std::size_t n_ = 0; + std::size_t* n_out_ = nullptr; + + io_env const* env_ = nullptr; + continuation cont_{}; + +public: + /// Create an op with a scripted `(ec, n)` completion that + /// reports `n` through `*n_out`. + static counted_read_op result( + std::error_code ec, std::size_t n, std::size_t* n_out) + { + counted_read_op op; + op.ec_ = ec; + op.n_ = n; + op.n_out_ = n_out; + return op; + } + + bool await_ready() const noexcept + { + return false; + } + + std::coroutine_handle<> await_suspend( + std::coroutine_handle<> h, + io_env const* env) + { + env_ = env; + cont_ = continuation{h}; + env_->executor.post(cont_); + return std::noop_coroutine(); + } + + io_result<> await_resume() noexcept + { + // Written before either protocol observes completion, so + // the count is valid on every channel, including + // set_stopped(), which cannot carry data itself. + *n_out_ = n_; + return {ec_}; } }; diff --git a/example/awaitable-sender/tests.cpp b/example/awaitable-sender/tests.cpp index dc22f1c29..b1403b3ee 100644 --- a/example/awaitable-sender/tests.cpp +++ b/example/awaitable-sender/tests.cpp @@ -13,7 +13,7 @@ #include -#include +#include #include #include @@ -31,7 +31,7 @@ #include namespace capy = boost::capy; -namespace ex = beman::execution; +namespace ex = stdexec; static int failures = 0; @@ -228,6 +228,20 @@ struct compound_op } }; +// The sanctioned path for compound results: a task +// wrapper inspects the full result, moves the payload out +// through a side channel, and returns only the code — the +// wrapper, not the bridge, decides the payload's fate. +capy::task read_some( + compound_op op, std::size_t* n_out) +{ + auto [ec, n] = co_await std::move(op); + *n_out = n; + co_return ec; +} + +static_assert(capy::AwaitableSender); + // Mock IoAwaitable that completes with no payload at all // (the `void` row of make_sigs, distinct from io_result<>'s // 1-tuple-ec row exercised by async_waker). @@ -298,38 +312,67 @@ struct throwing_op } }; -void test_compound_success() +void test_compound_workaround_success() { - auto out = run(capy::as_sender( - compound_op{.ec_ = {}, .n_ = 42})); + std::size_t n = 0; + auto out = run(capy::as_sender(read_some( + compound_op{.ec_ = {}, .n_ = 42}, &n))); + CHECK(out.ch == channel::value); + CHECK(n == 42); +} + +void test_compound_workaround_partial() +{ + // Partial success: the error reaches the error channel and + // the byte count still reaches the caller. + auto expected = std::make_error_code( + std::errc::connection_reset); + std::size_t n = 0; + auto out = run(capy::as_sender(read_some( + compound_op{.ec_ = expected, .n_ = 3}, &n))); + CHECK(out.ch == channel::error); + CHECK(out.ec == expected); + CHECK(n == 3); +} + +void test_counted_read_success() +{ + // The count is already in caller-owned state when the + // continuation runs, so the pipeline can consume it there. + std::size_t n = 0; + auto out = run(ex::then( + capy::counted_read_op::result({}, 42, &n), + [&] { return n; })); CHECK(out.ch == channel::value); CHECK(out.n == 42); } -void test_compound_error() +void test_counted_read_partial() { + // Partial success through the base: the disposition routes to + // the error channel while the count survives out-of-band. auto expected = std::make_error_code( std::errc::connection_reset); - auto out = run(capy::as_sender( - compound_op{.ec_ = expected, .n_ = 3})); + std::size_t n = 0; + auto out = run(capy::counted_read_op::result(expected, 3, &n)); CHECK(out.ch == channel::error); CHECK(out.ec == expected); + CHECK(n == 3); } void test_canceled_ec_routes_stopped() { // A canceled disposition surfaces on the stopped channel even // though the environment's token was never triggered. - auto out = run(capy::as_sender(compound_op{ - .ec_ = std::make_error_code( - std::errc::operation_canceled), - .n_ = 0})); + auto out = run(capy::as_sender(ec_op{ + std::make_error_code( + std::errc::operation_canceled)})); CHECK(out.ch == channel::stopped); } void test_value_survives_racing_stop() { - // The op ignores tokens and completes with a value; a stop + // The op ignores tokens and reports success in-band; a stop // request that merely landed by completion time must not // discard the result. capy::thread_pool pool(1); @@ -339,13 +382,12 @@ void test_value_survives_racing_stop() std::latch done(1); test_outcome out; auto op = ex::connect( - capy::as_sender(compound_op{.ec_ = {}, .n_ = 7}), + capy::as_sender(ec_op{}), test_receiver{ {pool_ex, ss.get_token()}, &out, &done}); ex::start(op); done.wait(); CHECK(out.ch == channel::value); - CHECK(out.n == 7); } void test_empty_io_result_success() @@ -449,33 +491,31 @@ void test_cancel_inplace_token() void test_then_pipeline() { auto out = run(ex::then( - capy::as_sender(compound_op{.ec_ = {}, .n_ = 41}), - [](std::size_t n) { return n + 1; })); + capy::as_sender(value_op{}), + [](int i) { return i + 25; })); CHECK(out.ch == channel::value); - CHECK(out.n == 42); + CHECK(out.i == 42); } void test_read_op_native_success() { - auto out = run(capy::read_op::result({}, 7)); + auto out = run(capy::read_op::result({})); CHECK(out.ch == channel::value); - CHECK(out.n == 7); } void test_read_op_native_error() { auto expected = std::make_error_code( std::errc::broken_pipe); - auto out = run(capy::read_op::result(expected, 0)); + auto out = run(capy::read_op::result(expected)); CHECK(out.ch == channel::error); CHECK(out.ec == expected); } void test_read_op_immediate() { - auto out = run(capy::read_op::immediate({}, 9)); + auto out = run(capy::read_op::immediate({})); CHECK(out.ch == channel::value); - CHECK(out.n == 9); } void test_read_op_stopped() @@ -517,9 +557,8 @@ void test_read_op_stopped_before_start() void test_read_op_through_adaptor() { - auto out = run(capy::as_sender(capy::read_op::result({}, 5))); + auto out = run(capy::as_sender(capy::read_op::result({}))); CHECK(out.ch == channel::value); - CHECK(out.n == 5); } // Symmetric with test_cancel_std_token, but for the native @@ -545,18 +584,17 @@ void test_read_op_cancel_std_token() void test_adaptor_immediate() { auto out = run(capy::as_sender( - capy::read_op::immediate({}, 3))); + capy::read_op::immediate({}))); CHECK(out.ch == channel::value); - CHECK(out.n == 3); } void test_read_op_then_pipeline() { auto out = run(ex::then( - capy::read_op::result({}, 41), - [](std::size_t n) { return n + 1; })); + capy::read_op::result({}), + [] { return 42; })); CHECK(out.ch == channel::value); - CHECK(out.n == 42); + CHECK(out.i == 42); } // sync_wait's environment provides get_scheduler but neither @@ -564,16 +602,15 @@ void test_read_op_then_pipeline() // bridge end-to-end on the run_loop. void test_sync_wait_value() { - auto r = ex::sync_wait(capy::read_op::result({}, 7)); + auto r = ex::sync_wait(capy::read_op::result({})); CHECK(r.has_value()); - CHECK(std::get<0>(*r) == 7); } void test_sync_wait_pipeline() { auto r = ex::sync_wait(ex::then( - capy::read_op::result({}, 41), - [](std::size_t n) { return n + 1; })); + capy::read_op::result({}), + [] { return 42; })); CHECK(r.has_value()); CHECK(std::get<0>(*r) == 42); } @@ -584,7 +621,7 @@ void test_sync_wait_error_throws() try { (void)ex::sync_wait(capy::read_op::result( - std::make_error_code(std::errc::broken_pipe), 0)); + std::make_error_code(std::errc::broken_pipe))); } catch(...) { @@ -681,9 +718,8 @@ capy::task record_outcome( test_outcome* out, std::latch* done) { - auto [ec, n] = co_await std::move(op); + auto [ec] = co_await std::move(op); out->ec = ec; - out->n = n; out->ch = !ec ? channel::value : ec == std::errc::operation_canceled ? channel::stopped @@ -693,33 +729,32 @@ capy::task record_outcome( void test_coawait_matches_sender_success() { - auto sender_out = run(capy::read_op::result({}, 7)); + auto sender_out = run(capy::read_op::result({})); capy::thread_pool pool(1); auto pool_ex = pool.get_executor(); test_outcome coawait_out; std::latch done(1); capy::run_async(pool_ex)(record_outcome( - capy::read_op::result({}, 7), &coawait_out, &done)); + capy::read_op::result({}), &coawait_out, &done)); WAIT_OR_DIE(done); CHECK(coawait_out.ch == channel::value); CHECK(sender_out.ch == coawait_out.ch); - CHECK(sender_out.n == coawait_out.n); } void test_coawait_matches_sender_error() { auto expected = std::make_error_code( std::errc::broken_pipe); - auto sender_out = run(capy::read_op::result(expected, 0)); + auto sender_out = run(capy::read_op::result(expected)); capy::thread_pool pool(1); auto pool_ex = pool.get_executor(); test_outcome coawait_out; std::latch done(1); capy::run_async(pool_ex)(record_outcome( - capy::read_op::result(expected, 0), + capy::read_op::result(expected), &coawait_out, &done)); WAIT_OR_DIE(done); @@ -796,60 +831,45 @@ static_assert( // awaitable-only ops. static_assert(std::is_same_v< decltype(capy::ensure_sender( - capy::read_op::result({}, 0))), + capy::read_op::result({}))), capy::read_op>); static_assert(std::is_same_v< - decltype(capy::ensure_sender(compound_op{})), - capy::awaitable_sender>); + decltype(capy::ensure_sender(ec_op{})), + capy::awaitable_sender>); void test_ensure_sender_passthrough() { auto out = run(capy::ensure_sender( - capy::read_op::result({}, 7))); + capy::read_op::result({}))); CHECK(out.ch == channel::value); - CHECK(out.n == 7); } void test_ensure_sender_lifts() { - auto out = run(capy::ensure_sender( - compound_op{.ec_ = {}, .n_ = 5})); + auto out = run(capy::ensure_sender(ec_op{})); CHECK(out.ch == channel::value); - CHECK(out.n == 5); } -// beman never calls the C++26 static-template signature query, so -// nothing else instantiates its body; pin it here so it stays -// callable in constant evaluation and agrees with the beman-compat -// instance form. +// stdexec dispatches the C++26 static-template signature query; +// pin read_op's deduced signatures so drift in make_sigs is +// caught at compile time. static_assert(std::is_same_v< decltype(capy::read_op:: get_completion_signatures()), - decltype(std::declval() - .get_completion_signatures(0))>); - -// Compile-fail probe: uncomment to verify the non-aggregate -// static_assert in awaitable_sender_base fires. (Completion -// signatures are deduced from await_resume(), so a signature -// mismatch is no longer expressible; the aggregate requirement -// is the remaining connect-time check.) -// Expected diagnostic: "Derived must declare a constructor". + ex::completion_signatures< + ex::set_value_t(), + ex::set_error_t(std::error_code), + ex::set_stopped_t()>>); + +// Compile-fail probe: uncomment to verify the compound-result +// rejection fires on both paths. Expected diagnostics: "cannot +// be senders" (make_sigs, CRTP path) and "as_sender does not +// accept" (as_sender path). // -// struct aggregate_op -// : capy::awaitable_sender_base +// void probe() // { -// bool await_ready() const noexcept { return false; } -// auto await_suspend( -// std::coroutine_handle<>, capy::io_env const*) -// { -// return std::noop_coroutine(); -// } -// capy::io_result await_resume() noexcept -// { -// return {}; -// } -// }; -// void probe() { (void)run(aggregate_op{}); } +// (void)capy::as_sender(compound_op{}); +// } int main() { @@ -859,8 +879,10 @@ int main() test_throwing_op_error(); test_ec_success(); test_ec_error(); - test_compound_success(); - test_compound_error(); + test_compound_workaround_success(); + test_compound_workaround_partial(); + test_counted_read_success(); + test_counted_read_partial(); test_canceled_ec_routes_stopped(); test_value_survives_racing_stop(); test_cancel_std_token();