Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 21 additions & 19 deletions example/awaitable-sender/awaitable_sender.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,21 @@ struct awaitable_sender
channels based on its type:

- `void` - calls `set_value()`.
- `error_code` or an empty `io_result` - calls
`set_value()` when the code is zero, `set_error(ec)`
otherwise.
- 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
- `error_code`, or any single-element tuple-like whose
sole element is `error_code` (such as an empty
`io_result`) - calls `set_value()` when the code is
zero, `set_error(ec)` otherwise.
- Any other single value `T` - calls `set_value(T)`.

Compound results are rejected at compile time, and the
constraint is structural, not nominal: any tuple-like whose
element 0 is `error_code` with additional elements is
refused, whether it is `io_result<size_t>`,
`std::tuple<error_code, size_t>`, or a user-defined type of
the same shape. 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<error_code>` that inspects the full result, moves the
payload out through a side channel, and returns the code.

Expand Down Expand Up @@ -116,12 +118,12 @@ auto as_sender(IoAw&& aw)
using R = awaitable_result_t<std::decay_t<IoAw>>;
static_assert(
!detail::is_compound_ec_result_v<R>,
"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<error_code> that inspects the full result "
"and returns the error code.");
"as_sender does not accept awaitables whose result "
"destructures into (error_code, ...): completion "
"channels are exclusive, so a partial success "
"(error_code plus payload) would be silently dropped. "
"Wrap the operation in a task<error_code> that inspects "
"the full result and returns the error code.");
return awaitable_sender<std::decay_t<IoAw>>{
std::forward<IoAw>(aw)};
}
Expand Down
67 changes: 44 additions & 23 deletions example/awaitable-sender/awaitable_sender_detail.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -81,32 +81,53 @@ struct io_sender_env

namespace detail {

// Channel splitting keys on capy's io_result, the type that
// 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. 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
// Channel splitting is structural, not nominal (P4093): any
// tuple-like whose element 0 is error_code is an outcome, no
// matter its name. A bare error_code or a single-element
// tuple-like is a binary outcome and splits across the
// channels; one with payload elements is rejected in
// make_sigs, because exclusive completion channels cannot
// carry a partial success without dropping data. The SFINAE
// probe avoids naming std::tuple_size members on foreign
// types, which would hard-error for non-tuples (&& does not
// short-circuit instantiation).
template<class T, class = void>
struct has_tuple_protocol : std::false_type {};

template<class T>
struct has_tuple_protocol<T,
std::void_t<typename std::tuple_size<T>::type>>
: std::bool_constant<(std::tuple_size<T>::value > 0)> {};

template<class T, bool = has_tuple_protocol<T>::value>
struct is_ec_outcome
: std::is_same<T, std::error_code> {};

template<class T>
struct is_ec_outcome<T, true>
: std::bool_constant<
std::tuple_size_v<T> == 1 &&
std::is_same_v<
std::tuple_element_t<0, T>,
std::error_code>> {};

template<class T>
struct io_result_arity
: std::integral_constant<std::size_t, 0> {};
constexpr bool is_ec_outcome_v = is_ec_outcome<T>::value;

template<class... Ts>
struct io_result_arity<io_result<Ts...>>
: std::integral_constant<std::size_t, 1 + sizeof...(Ts)> {};
template<class T, bool = has_tuple_protocol<T>::value>
struct is_compound_ec_result : std::false_type {};

template<class T>
constexpr bool is_ec_outcome_v =
std::is_same_v<T, std::error_code> ||
io_result_arity<T>::value == 1;
struct is_compound_ec_result<T, true>
: std::bool_constant<
std::tuple_size_v<T> >= 2 &&
std::is_same_v<
std::tuple_element_t<0, T>,
std::error_code>> {};

template<class T>
constexpr bool is_compound_ec_result_v =
io_result_arity<T>::value >= 2;
is_compound_ec_result<T>::value;

// -------------------------------------------------------
// frame_cb: synthetic coroutine frame for callback handles
Expand Down Expand Up @@ -140,12 +161,12 @@ auto make_sigs()

static_assert(
!is_compound_ec_result_v<R>,
"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<error_code> that inspects the full result "
"and returns the error code.");
"IoAwaitables whose result destructures into "
"(error_code, ...) 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<error_code> that inspects "
"the full result and returns the error code.");

constexpr bool nothrow_resume =
noexcept(std::declval<A&>().await_resume());
Expand Down
38 changes: 18 additions & 20 deletions example/awaitable-sender/tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -796,26 +796,24 @@ void test_coawait_stopped_midflight()
CHECK(coawait_out.ch == channel::stopped);
}

// Channel splitting is keyed on io_result, not tuple shape: a
// std::tuple that happens to lead with error_code is a value.
struct tuple_result_op
{
bool await_ready() const noexcept { return false; }
auto await_suspend(
std::coroutine_handle<>, capy::io_env const*)
{
return std::noop_coroutine();
}
std::tuple<std::error_code, int> await_resume() noexcept
{
return {};
}
};
static_assert(std::is_same_v<
decltype(capy::detail::make_sigs<tuple_result_op>()),
ex::completion_signatures<
ex::set_value_t(std::tuple<std::error_code, int>),
ex::set_stopped_t()>>);
// Channel splitting is structural, not nominal (P4093): any
// tuple-like leading with error_code splits when it carries no
// payload and is rejected when it does, io_result or not.
static_assert(capy::detail::is_ec_outcome_v<std::error_code>);
static_assert(capy::detail::is_ec_outcome_v<capy::io_result<>>);
static_assert(capy::detail::is_ec_outcome_v<
std::tuple<std::error_code>>);
static_assert(capy::detail::is_compound_ec_result_v<
capy::io_result<std::size_t>>);
static_assert(capy::detail::is_compound_ec_result_v<
std::tuple<std::error_code, int>>);
static_assert(capy::detail::is_compound_ec_result_v<
std::pair<std::error_code, std::size_t>>);
// no tuple protocol, no error_code lead: plain values
static_assert(!capy::detail::is_ec_outcome_v<int> &&
!capy::detail::is_compound_ec_result_v<int>);
static_assert(!capy::detail::is_compound_ec_result_v<
std::pair<int, std::error_code>>);

// AwaitableSender partitions the world correctly: read_op models
// both protocols; compound_op is awaitable-only; the as_sender
Expand Down
Loading