From 929cf60c5b9e11c48e64054aca8f553c76b89000 Mon Sep 17 00:00:00 2001 From: Vinnie Falco Date: Sun, 4 Jan 2026 05:43:25 -0800 Subject: [PATCH] Add affine protocol and coroutine-first I/O research Core changes: - Rename async_result -> async_op, make_affine -> affine - Upgrade task to affine awaitable protocol with executor affinity - Add spawn() for top-level task execution with exception propagation - Make executor::post/submit const, add executor_dispatcher adapter Research: - Add coroutine-first I/O framework design exploration (coro-first-io.md) - Document P2300/P2762/P3552/P3826 analysis and networking implications - Add N4242/N4482 executor proposal notes --- .gitignore | 4 + doc/modules/ROOT/pages/coroutines.adoc | 114 +-- doc/research/execution.md | 318 +++++++ include/boost/capy.hpp | 4 +- include/boost/capy/affine.hpp | 677 +++++++++++++++ .../capy/{async_result.hpp => async_op.hpp} | 139 +-- include/boost/capy/bcrypt.hpp | 6 +- include/boost/capy/executor.hpp | 51 +- include/boost/capy/make_affine.hpp | 350 -------- include/boost/capy/task.hpp | 654 +++++++++----- test/cmake_test/CMakeLists.txt | 25 +- test/unit/{make_affine.cpp => affine.cpp} | 8 +- test/unit/{async_result.cpp => async_op.cpp} | 100 ++- test/unit/task.cpp | 811 ++++++++++++++++-- 14 files changed, 2449 insertions(+), 812 deletions(-) create mode 100644 doc/research/execution.md create mode 100644 include/boost/capy/affine.hpp rename include/boost/capy/{async_result.hpp => async_op.hpp} (71%) delete mode 100644 include/boost/capy/make_affine.hpp rename test/unit/{make_affine.cpp => affine.cpp} (81%) rename test/unit/{async_result.cpp => async_op.cpp} (79%) diff --git a/.gitignore b/.gitignore index 9e1d7e0c..24e3dede 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,7 @@ /.cache /.clangd /compile_commands.json + +/.cursor/ +/build_clang/ + diff --git a/doc/modules/ROOT/pages/coroutines.adoc b/doc/modules/ROOT/pages/coroutines.adoc index 4d73cda9..49e55917 100644 --- a/doc/modules/ROOT/pages/coroutines.adoc +++ b/doc/modules/ROOT/pages/coroutines.adoc @@ -14,7 +14,7 @@ Capy provides lightweight coroutine support for C++20, enabling asynchronous code that reads like synchronous code. The library offers two awaitable types: `task` for lazy coroutine-based -operations, and `async_result` for bridging callback-based +operations, and `async_op` for bridging callback-based APIs into the coroutine world. This section covers the awaitable types provided by the library, @@ -37,28 +37,33 @@ A `task` owns its coroutine handle and destroys it automatically. Exceptions thrown within the coroutine are captured and rethrown when the result is retrieved via `co_await`. +Tasks support scheduler affinity through the `on()` method, which +binds the task to an executor. When a task has affinity, all +internal `co_await` expressions resume on the specified executor, +ensuring consistent execution context. + The `task` specialization is used for coroutines that perform work but do not produce a value. These coroutines use `co_return;` with no argument. -=== async_result +=== async_op -xref:reference:boost/capy/async_result.adoc[`async_result`] bridges traditional callback-based asynchronous +xref:reference:boost/capy/async_op.adoc[`async_op`] bridges traditional callback-based asynchronous APIs with coroutines. It wraps a deferred operation—a callable that accepts a completion handler, starts an asynchronous operation, and invokes the handler with the result. -The key advantage of `async_result` is its type-erased design. The +The key advantage of `async_op` is its type-erased design. The implementation details are hidden behind an abstract interface, allowing runtime-specific code such as Boost.Asio to be confined -to source files. Headers that return `async_result` do not need +to source files. Headers that return `async_op` do not need to include Asio or other heavyweight dependencies, keeping compile times low and interfaces clean. -Use xref:reference:boost/capy/make_async_result.adoc[`make_async_result()`] to create an `async_result` from any +Use xref:reference:boost/capy/make_async_op.adoc[`make_async_op()`] to create an `async_op` from any callable that follows the deferred operation pattern. -The `async_result` specialization is used for operations that +The `async_op` specialization is used for operations that signal completion without producing a value, such as timers, write operations, or connection establishment. The completion handler takes no arguments. @@ -86,38 +91,38 @@ Use `task` when composing asynchronous operations purely within the coroutine world. Tasks can await other tasks, forming a tree of dependent operations. -=== When to use async_result +=== When to use async_op -Return `async_result` from a regular (non-coroutine) function that +Return `async_op` from a regular (non-coroutine) function that wraps an existing callback-based API. The function does not use `co_await` or `co_return`; instead it constructs and returns an -`async_result` using `make_async_result()`. +`async_op` using `make_async_op()`. [source,cpp] ---- -async_result async_read(socket& s, buffer& b) +async_op async_read(socket& s, buffer& b) { - return make_async_result( + return make_async_op( [&](auto handler) { s.async_read(b, std::move(handler)); }); } ---- -Use `async_result` at the boundary between callback-based code and +Use `async_op` at the boundary between callback-based code and coroutines. It serves as an adapter that lets coroutines `co_await` operations implemented with traditional completion handlers. === Choosing between them * Writing new asynchronous logic? Use `task`. -* Wrapping an existing callback API? Use `async_result`. +* Wrapping an existing callback API? Use `async_op`. * Composing multiple awaitable operations? Use `task`. * Exposing a library function without leaking dependencies? Use - `async_result` with the implementation in a source file. + `async_op` with the implementation in a source file. In practice, application code is primarily `task`-based, while -`async_result` appears at integration points with I/O libraries +`async_op` appears at integration points with I/O libraries and other callback-driven systems. == Examples @@ -170,12 +175,12 @@ the source file. #ifndef TIMER_HPP #define TIMER_HPP -#include +#include namespace mylib { // Returns the number of milliseconds actually elapsed -boost::capy::async_result +boost::capy::async_op async_wait(int milliseconds); } // namespace mylib @@ -191,10 +196,10 @@ async_wait(int milliseconds); namespace mylib { -boost::capy::async_result +boost::capy::async_op async_wait(int milliseconds) { - return boost::capy::make_async_result( + return boost::capy::make_async_op( [milliseconds](auto handler) { // In a real implementation, this would use @@ -217,22 +222,22 @@ async_wait(int milliseconds) === Void operations -This example shows `task` and `async_result` for +This example shows `task` and `async_op` for operations that complete without producing a value. [source,cpp] ---- #include -#include +#include using boost::capy::task; -using boost::capy::async_result; -using boost::capy::make_async_result; +using boost::capy::async_op; +using boost::capy::make_async_op; // Wrap a callback-based timer (void result) -async_result async_sleep(int milliseconds) +async_op async_sleep(int milliseconds) { - return make_async_result( + return make_async_op( [milliseconds](auto on_done) { // In real code, this would start a timer @@ -258,66 +263,63 @@ task run_sequence() } ---- -=== Running a task to completion +=== Spawning tasks on an executor -Tasks are lazy and require a driver to execute. This example -shows a simple synchronous driver that runs a task until it -completes. +Tasks are lazy and require a driver to execute. The `spawn()` function +starts a task on an executor and delivers the result to a completion +handler. This is useful for launching tasks from non-coroutine code +or integrating tasks into callback-based systems. [source,cpp] ---- #include +#include using boost::capy::task; - -template -T run(task t) -{ - bool done = false; - t.handle().promise().on_done_ = [&done]{ done = true; }; - t.handle().resume(); - - // In a real application, this would integrate with - // an event loop rather than spinning - while (!done) - { - // Process pending I/O events here - } - - return t.await_resume(); -} +using boost::capy::executor; +using boost::capy::spawn; task compute() { co_return 42; } -int main() +void start_computation(executor ex) { - int result = run(compute()); - return result == 42 ? 0 : 1; + // Spawn a task on the executor with a completion handler + spawn(ex, compute(), [](auto result) { + if (result.has_value()) + std::cout << "Result: " << *result << std::endl; + else + std::cerr << "Error occurred\n"; + }); } ---- +The `spawn()` function takes an executor, a task, and a completion handler. +The handler receives `system::result` which holds +either the task's return value or any exception thrown during execution. +The task runs to completion on the executor with proper scheduler affinity. + === Complete request handler -This example combines tasks and async_result to implement a +This example combines tasks and async_op to implement a request handler that reads a request, processes it, and sends a response. [source,cpp] ---- #include -#include +#include #include using boost::capy::task; -using boost::capy::async_result; +using boost::capy::async_op; -// Forward declarations - implementations use async_result +// Forward declarations - implementations use async_op // to wrap the underlying I/O library -async_result async_read(int fd); -async_result async_write(int fd, std::string data); +async_op async_read(int fd); +async_op async_write(int fd, std::string data); // Pure coroutine logic using task task process_request(std::string const& request) diff --git a/doc/research/execution.md b/doc/research/execution.md new file mode 100644 index 00000000..613e68eb --- /dev/null +++ b/doc/research/execution.md @@ -0,0 +1,318 @@ +# Execution Model: post, dispatch, and defer + +Research notes on executor semantics for capy, incorporating material from +N4242 (Executors and Asynchronous Operations) and N4482 (Executor Semantics). + +## The Minimal Executor Concept + +After discussion with Peter Dimov, we concluded the minimal foundational executor API is: + +```cpp +struct executor +{ + template + void post(NullaryCallable&&); + + template + void dispatch(NullaryCallable&&); +}; +``` + +This is the smallest API upon which everything—including senders—can be built. + +## Progress Guarantees + +From N4482, executors provide a **parallel progress guarantee**: submitted work +will eventually execute, but only after its first step begins. This is weaker +than a concurrent guarantee (where work makes progress independently). + +| Guarantee | Meaning | Example | +|-----------|---------|---------| +| **Parallel** | Work runs eventually, shares threads | Thread pool, io_context | +| **Concurrent** | Work makes independent progress | new_thread_executor | +| **Weakly parallel** | May need external nudging | Resumable functions on executors | + +Coroutines built on parallel executors get weakly parallel progress—the +executor provides the parallel guarantee, but the coroutine itself may suspend +indefinitely waiting for events. + +## Semantic Definitions + +### post + +**Never blocks the caller pending completion of f.** + +From N4242 §15.9.1: +> The executor shall not invoke f in the current thread of execution prior to +> returning from post. + +- Guaranteed the callable does NOT run before `post()` returns +- Never runs inline, always goes through the queue +- Use for: new independent work + +### dispatch + +**Permitted to block the caller until f finishes.** + +From N4242 §15.9.1: +> The executor may invoke f in the current thread of execution prior to +> returning from dispatch. + +- If called from within the executor's context → execute immediately +- If called from outside → enqueue like `post` +- May block until completion if running inline +- Use for: continuations (resuming suspended work) + +### defer + +**Like post, but hints continuation relationship.** + +From N4242 §15.9.1: +> defer is used to convey the intention of the caller that the submitted +> function is a continuation of the current call context. The executor may +> use this information to optimize or otherwise adjust the way in which f +> is invoked. + +- Always enqueues (never inline) +- Implementation may defer beyond current handler invocation +- Enables thread-local queue optimization (see below) +- Use for: yielding to other queued work, fairness + +## Why Both post and dispatch? + +### Correctness, Not Just Optimization + +From N4482 §3.1.1: +> A consequence of dispatch()'s specification is that its use can introduce +> deadlock. This occurs when the caller holds a mutex and f attempts to +> acquire the same mutex. Thus the choice between dispatch() and post() +> impacts **program correctness**. A hint, which by definition need not be +> respected, is an inappropriate way for the caller to express its intention. + +The distinction is **not a hint**—it affects whether your program deadlocks. + +### post alone is insufficient + +With only `post`, every continuation bounces through a queue even when already on the correct executor: + +```cpp +// Already running on executor X +async_op.then([](auto result) { + // With post-only: queued, context switch, runs later + // With dispatch: runs inline, zero overhead +}); +``` + +### dispatch expresses continuation semantics + +When an async operation completes, the handler is a *continuation* of the current logical operation. The completing operation knows: + +- "I'm finishing on context X" +- "The handler wants to run on context Y" +- If X == Y → run it now (dispatch) +- If X ≠ Y → post to Y + +This is the essential optimization that makes coroutines competitive with hand-written state machines. + +## The defer Optimization + +From N4242 §9, consider a chain of async reads where each completes immediately +(data already in kernel buffers): + +**With post (naïve):** +``` +#6 — lock mutex +#7 — dequeue read_loop +#8 — unlock mutex +#9 — call read_loop +#1 — call post +#2 — lock mutex +#3 — enqueue read_loop +#4 — notify condition +#5 — unlock mutex +(repeat) +``` + +Each cycle requires **two lock/unlock pairs** plus a condition variable notification. + +**With defer (optimized):** +``` +#3 — lock mutex +#4 — flush thread-local queue to main queue +#5 — dequeue read_loop +#6 — unlock mutex +#7 — call read_loop +#1 — call defer +#2 — enqueue to thread-local queue (no lock!) +(repeat) +``` + +By using a thread-local queue, defer eliminates one lock/unlock pair and avoids +unnecessary thread wakeups. On modern hardware: ~15ns for uncontended lock vs +~2ns for thread-local access. + +## Formal Executor Requirements + +From N4242 §15.9.1, an executor type X must provide: + +| Expression | Semantics | +|------------|-----------| +| `x.dispatch(f, a)` | May invoke f in current thread prior to returning | +| `x.post(f, a)` | Shall not invoke f in current thread prior to returning | +| `x.defer(f, a)` | Same as post, but hints f is a continuation | +| `x.on_work_started()` | Increment outstanding work count | +| `x.on_work_finished()` | Decrement outstanding work count | +| `x.context()` | Return reference to execution_context | + +The allocator parameter `a` allows the executor to use caller-provided memory +for storing the queued function object. + +## Coroutine Affinity Example + +Consider a coroutine chain with affinity: + +```cpp +task c() { co_return; } +task b() { co_await c(); } +task a() { co_await b(); } + +spawn(ex, a().on(ex), handler); // a has affinity, b and c inherit +``` + +### Transition Analysis + +| Transition | Type | Recommended | Rationale | +|------------|------|-------------|-----------| +| a → b (starting b) | Starting child | Symmetric transfer | Initiating new work, already on `ex` | +| b → c (starting c) | Starting child | Symmetric transfer | Same—continuing the chain | +| c → b (c completes) | Continuation | **dispatch** | Resuming suspended parent | +| b → a (b completes) | Continuation | **dispatch** | Resuming suspended parent | + +### Execution Trace with Async I/O + +``` +1. Thread A (ex): a runs, awaits b → symmetric transfer +2. Thread A (ex): b runs, awaits c → symmetric transfer +3. Thread A (ex): c runs, starts async I/O, suspends +4. Thread B (I/O): I/O completes, resumes c +5. Thread B (I/O): c's final_suspend — NOT on ex! + → dispatch: posts continuation to ex +6. Thread A (ex): b resumes from queue +7. Thread A (ex): b's final_suspend — already on ex + → dispatch: runs inline (no queue bounce) +8. Thread A (ex): a resumes immediately +``` + +With **dispatch**: Steps 5 posts (wrong context), step 7 runs inline (right context). + +With **post everywhere**: Step 7 would unnecessarily queue. + +With **symmetric transfer everywhere**: Step 5 would violate affinity. + +## capy::task Should Always Dispatch + +For `capy::task`, all uses of the dispatcher are resuming suspended coroutines: + +| Site | What's Happening | Nature | +|------|------------------|--------| +| `final_suspend` | Child done, resume parent | Continuation | +| Async op completes | Resume awaiting task | Continuation | +| `spawn` completion | Task done, call handler | Continuation | + +There's no case where task submits new independent work through the dispatcher. Therefore, the `executor_dispatcher` should use dispatch: + +```cpp +struct executor_dispatcher +{ + executor ex_; + + template + void operator()(F&& f) const + { + if (ex_) + ex_.dispatch(std::forward(f)); // continuation semantics + else + std::forward(f)(); + } +}; +``` + +## When to Use post from await_suspend + +From the perspective of an async operation's completion path: + +### Use dispatch (default) + +- Normal async completion +- Efficient inline execution when on correct context +- The common case + +### Use post when: + +1. **Synchronous completion safety**: Operation might complete before initiation returns + ```cpp + start_async_read([h, &d](Data result) { + // Might be called BEFORE start_async_read returns! + d.post(h); // Safe: never inline + }); + ``` + +2. **Reentrancy**: Completing while holding locks + ```cpp + void complete_all_waiters() { + std::lock_guard lock(mutex_); + for (auto& w : waiters_) { + w.dispatcher.post(w.handle); // User code runs after lock released + } + } + ``` + +3. **Stack depth**: Batch-completing many operations + ```cpp + for (auto& op : completed_ops) { + op.dispatcher.post(op.handle); // Constant stack depth + } + ``` + +4. **Fairness**: Yielding to other queued work after heavy computation + +### Decision Framework + +| Situation | Use | +|-----------|-----| +| Normal async completion | dispatch | +| Might complete synchronously | post | +| Completing while holding locks | post | +| Batch-completing many ops | post | +| Want to yield/be fair | post or defer | + +## Relationship to Senders (P2300) + +The sender model's `schedule()` + `start()` decomposes into these primitives: + +```cpp +auto sender = schedule(ex) | then([]{ /* work */ }); +``` + +When started, the scheduler internally uses: +- `dispatch` if already on correct context (inline optimization) +- `post` if not (deferred execution) + +The post/dispatch model is foundational—senders are built on top of it. + +## References + +- N4242: Executors and Asynchronous Operations, Revision 1 (2014) +- N4482: Some notes on executors and the Networking Library Proposal (2015) +- Boost.Asio executor model: https://www.boost.org/doc/libs/release/doc/html/boost_asio/overview/model/executors.html +- P2300: std::execution: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2300r10.html + +## Summary + +- **post** = "here's new independent work" (always queued, never blocks caller) +- **dispatch** = "here's the next step of what I'm doing" (inline if same context, may block) +- **defer** = "queue this continuation" (enables thread-local optimization) + +The choice between dispatch and post is **correctness**, not optimization. +For coroutine continuations, dispatch is correct. Post is the escape hatch +when you need guaranteed deferred execution. diff --git a/include/boost/capy.hpp b/include/boost/capy.hpp index d7654513..f80d7429 100644 --- a/include/boost/capy.hpp +++ b/include/boost/capy.hpp @@ -10,14 +10,14 @@ #ifndef BOOST_CAPY_HPP #define BOOST_CAPY_HPP +#include #include -#include +#include #include #include #include #include #include -#include #include #include #include diff --git a/include/boost/capy/affine.hpp b/include/boost/capy/affine.hpp new file mode 100644 index 00000000..7594de9e --- /dev/null +++ b/include/boost/capy/affine.hpp @@ -0,0 +1,677 @@ +// +// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/capy +// + +#ifndef BOOST_CAPY_AFFINE_HPP +#define BOOST_CAPY_AFFINE_HPP + +#include + +#ifdef BOOST_CAPY_HAS_CORO + +#include +#include +#include +#include +#include +#include + +namespace boost { +namespace capy { + +/** Concept for types that can dispatch coroutine resumption. + + A dispatcher is a callable that accepts a coroutine handle + and arranges for it to be resumed on the target execution + context. Since std::coroutine_handle has operator() which + calls resume(), the dispatcher can invoke the handle directly. + + @par Example + @code + struct my_dispatcher + { + void operator()(std::coroutine_handle<> h) const + { + // Queue h for execution on target context + thread_pool_.post([h] { h(); }); + } + }; + @endcode + + @tparam D The dispatcher type to check. + @tparam P The promise type for the coroutine handle (default void). +*/ +template +concept dispatcher = requires(D d, std::coroutine_handle

h) { d(h); }; + +/** Concept for awaitables that support scheduler affinity. + + An affine_awaitable is an awaitable that accepts a dispatcher + in its await_suspend method, enabling zero-overhead scheduler + affinity. When an operation completes, it uses the dispatcher + to resume the coroutine on the correct execution context. + + @par Requirements + The type must provide `await_suspend(handle, dispatcher)` + accepting a coroutine handle and a dispatcher reference. + The dispatcher must satisfy the dispatcher concept. + The other awaitable requirements (await_ready, await_resume) + are enforced by the compiler when used in a co_await expression. + + @par Example + @code + struct affine_async_op + { + int result_; + + bool await_ready() const noexcept { return false; } + + template + void await_suspend(std::coroutine_handle<> h, Dispatcher& d) const + { + // Start async work, then resume via dispatcher + start_async([h, &d]() { + d(h); + }); + } + + int await_resume() const noexcept { return result_; } + }; + @endcode + + @tparam A The awaitable type to check. + @tparam D The dispatcher type. + @tparam P The promise type for the coroutine handle (default void). +*/ +template +concept affine_awaitable = + dispatcher && + requires(A a, std::coroutine_handle

h, D& d) { + a.await_suspend(h, d); + }; + +/** Wrapper that bridges affine awaitables to standard coroutine machinery. + + This adapter wraps an affine_awaitable and provides the standard + awaiter interface expected by the compiler. It captures a pointer + to the dispatcher and forwards it to the awaitable's extended + await_suspend method. + + @par Usage + This is typically used in await_transform to adapt affine awaitables: + @code + template + auto await_transform(Awaitable&& a) + { + if constexpr (affine_awaitable) { + return affine_awaiter{ + std::forward(a), &dispatcher_}; + } + // ... handle other cases + } + @endcode + + @par Dispatcher + The dispatcher must satisfy the dispatcher concept, i.e., + be callable with a coroutine handle: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @tparam Awaitable The affine awaitable type being wrapped. + @tparam Dispatcher The dispatcher type for resumption. +*/ +template +struct affine_awaiter { + Awaitable awaitable_; + Dispatcher* dispatcher_; + + bool await_ready() { + return awaitable_.await_ready(); + } + + auto await_suspend(std::coroutine_handle<> h) { + return awaitable_.await_suspend(h, *dispatcher_); + } + + decltype(auto) await_resume() { + return awaitable_.await_resume(); + } +}; + +template +affine_awaiter(A&&, D*) -> affine_awaiter; + +/** Unified context serving as both dispatcher and scheduler. + + This class wraps a scheduler and provides a unified interface + that works with both P2300 senders and traditional awaitables. + It acts as a dispatcher (callable with coroutine handles) while + also providing access to the underlying scheduler for sender + operations like continues_on. + + @par Dispatcher Interface + The class satisfies the dispatcher concept: + @code + resume_context ctx{scheduler}; + ctx(h); // Dispatches coroutine handle via scheduler + @endcode + + @par Scheduler Access + For P2300 sender operations: + @code + auto sender = continues_on(some_sender, ctx.scheduler()); + @endcode + + @par Scheduler Requirements + The scheduler type must provide a dispatch method: + @code + struct Scheduler + { + template + void dispatch(F&& f); + }; + @endcode + + @tparam Scheduler The underlying scheduler type. +*/ +template +class resume_context { + Scheduler* sched_; + +public: + /** Construct from a scheduler reference. + + @param s The scheduler to wrap. Must remain valid for the + lifetime of this context. + */ + explicit resume_context(Scheduler& s) noexcept + : sched_(&s) + { + } + + resume_context(resume_context const&) = default; + resume_context& operator=(resume_context const&) = default; + + /** Dispatch a continuation via the scheduler. + + @param f A nullary function object to dispatch. + */ + template + void operator()(F&& f) const { + sched_->dispatch(std::forward(f)); + } + + /** Access the underlying scheduler. + + @return A reference to the wrapped scheduler. + */ + Scheduler& scheduler() const noexcept { + return *sched_; + } + + bool operator==(resume_context const&) const noexcept = default; +}; + +/** CRTP mixin providing scheduler affinity for promise types. + + This mixin adds dispatcher storage and an affinity-aware + final_suspend to promise types. When a dispatcher is set, + the continuation is resumed through it; otherwise, direct + symmetric transfer is used. + + @par Usage + Inherit from this mixin using CRTP: + @code + struct promise_type + : affine_promise + { + // Your promise implementation... + // final_suspend() is provided by the mixin + }; + @endcode + + @par Behavior + - If a dispatcher is set, final_suspend dispatches the + continuation through it before returning noop_coroutine + - If no dispatcher is set, final_suspend performs direct + symmetric transfer to the continuation + - An optional done flag can be set to signal completion + + @par Dispatcher + The dispatcher must satisfy the dispatcher concept, i.e., + be callable with a coroutine handle: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @tparam Derived The derived promise type (CRTP). + @tparam Dispatcher The dispatcher type for resumption. +*/ +template +class affine_promise { +protected: + std::coroutine_handle<> continuation_; + std::optional dispatcher_; + bool* done_flag_ = nullptr; + +public: + /** Set the continuation handle for symmetric transfer. + + @param h The coroutine handle to resume when this + coroutine completes. + */ + void set_continuation(std::coroutine_handle<> h) noexcept { + continuation_ = h; + } + + /** Set the dispatcher for affine resumption. + + @param d The dispatcher to use for resuming the + continuation. + */ + void set_dispatcher(Dispatcher d) { + dispatcher_.emplace(std::move(d)); + } + + /** Set a flag to be marked true on completion. + + @param flag Reference to a bool that will be set to + true when the coroutine reaches final_suspend. + */ + void set_done_flag(bool& flag) noexcept { + done_flag_ = &flag; + } + + /** Return a final awaiter with affinity support. + + If a dispatcher is set, the continuation is resumed + through it. Otherwise, direct symmetric transfer occurs. + + @return An awaiter for final suspension. + */ + auto final_suspend() noexcept { + struct final_awaiter { + affine_promise* p_; + + bool await_ready() noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<>) noexcept { + if (p_->done_flag_) + *p_->done_flag_ = true; + + if (p_->dispatcher_) { + // Resume continuation via dispatcher + if (p_->continuation_) + (*p_->dispatcher_)(p_->continuation_); + return std::noop_coroutine(); + } + // Direct symmetric transfer + return p_->continuation_ ? p_->continuation_ + : std::noop_coroutine(); + } + + void await_resume() noexcept {} + }; + return final_awaiter{this}; + } +}; + +/** CRTP mixin providing awaitable interface for task types. + + This mixin makes a task type awaitable with support for both + legacy coroutines (no dispatcher) and affine coroutines + (with dispatcher). It provides both overloads of await_suspend. + + @par Requirements + The derived class must provide: + - `handle()` returning the coroutine_handle + + The promise type must provide: + - `set_continuation(handle)` to store the caller + - `set_dispatcher(dispatcher)` to store the dispatcher + - `result()` to retrieve the coroutine result + + @par Usage + @code + template + class task + : public affine_task, my_dispatcher> + { + handle_type handle_; + + public: + handle_type handle() const { return handle_; } + // ... + }; + @endcode + + @par Await Paths + - Legacy: `co_await task` calls await_suspend(handle) + - Affine: await_transform wraps in affine_awaiter which + calls await_suspend(handle, dispatcher) + + @par Dispatcher + The dispatcher must satisfy the dispatcher concept, i.e., + be callable with a coroutine handle: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @tparam T The result type of the task. + @tparam Derived The derived task type (CRTP). + @tparam Dispatcher The dispatcher type for affine resumption. +*/ +template +class affine_task { + Derived& self() { return static_cast(*this); } + Derived const& self() const { return static_cast(*this); } + +public: + /** Check if the task has already completed. + + @return true if the coroutine is done. + */ + bool await_ready() const noexcept { + return self().handle().done(); + } + + /** Suspend and start the task (legacy path). + + This overload is used when no dispatcher is available. + The continuation will be resumed via direct symmetric + transfer when the task completes. + + @param caller The calling coroutine's handle. + @return The task's coroutine handle to resume. + */ + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller) noexcept { + self().handle().promise().set_continuation(caller); + return self().handle(); + } + + /** Suspend and start the task (affine path). + + This overload is used when a dispatcher is available. + The continuation will be resumed through the dispatcher + when the task completes, ensuring scheduler affinity. + + @param caller The calling coroutine's handle. + @param d The dispatcher for resuming the continuation. + @return The task's coroutine handle to resume. + */ + template + requires std::convertible_to + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller, D&& d) noexcept { + self().handle().promise().set_dispatcher(std::forward(d)); + self().handle().promise().set_continuation(caller); + return self().handle(); + } + + /** Retrieve the task result. + + @return The value produced by the coroutine, or rethrows + any captured exception. + */ + decltype(auto) await_resume() { + return self().handle().promise().result(); + } +}; + +namespace detail { + +template +auto get_awaitable(T&& expr) { + if constexpr (requires { std::forward(expr).operator co_await(); }) + return std::forward(expr).operator co_await(); + else if constexpr (requires { operator co_await(std::forward(expr)); }) + return operator co_await(std::forward(expr)); + else + return std::forward(expr); +} + +template +using awaitable_type = decltype(get_awaitable(std::declval())); + +template +using await_result_t = decltype(std::declval>().await_resume()); + +template +struct dispatch_awaitable { + Dispatcher& dispatcher_; + + bool await_ready() const noexcept { return false; } + + void await_suspend(std::coroutine_handle<> h) const { + dispatcher_(h); + } + + void await_resume() const noexcept {} +}; + +struct transfer_to_caller { + std::coroutine_handle<> caller_; + + bool await_ready() noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<>) noexcept { + return caller_; + } + + void await_resume() noexcept {} +}; + +template +class affinity_trampoline +{ +public: + struct promise_type { + std::optional value_; + std::exception_ptr exception_; + std::coroutine_handle<> caller_; + + affinity_trampoline get_return_object() { + return affinity_trampoline{ + std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() noexcept { return {}; } + + transfer_to_caller final_suspend() noexcept { + return {caller_}; + } + + template + void return_value(U&& v) { + value_.emplace(std::forward(v)); + } + + void unhandled_exception() { + exception_ = std::current_exception(); + } + }; + +private: + std::coroutine_handle handle_; + +public: + explicit affinity_trampoline(std::coroutine_handle h) + : handle_(h) + { + } + + affinity_trampoline(affinity_trampoline&& o) noexcept + : handle_(std::exchange(o.handle_, {})) + { + } + + ~affinity_trampoline() { + if (handle_) + handle_.destroy(); + } + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller) noexcept { + handle_.promise().caller_ = caller; + return handle_; + } + + T await_resume() { + if (handle_.promise().exception_) + std::rethrow_exception(handle_.promise().exception_); + return std::move(*handle_.promise().value_); + } +}; + +template<> +class affinity_trampoline +{ +public: + struct promise_type { + std::exception_ptr exception_; + std::coroutine_handle<> caller_; + + affinity_trampoline get_return_object() { + return affinity_trampoline{ + std::coroutine_handle::from_promise(*this)}; + } + + std::suspend_always initial_suspend() noexcept { return {}; } + + transfer_to_caller final_suspend() noexcept { + return {caller_}; + } + + void return_void() noexcept {} + + void unhandled_exception() { + exception_ = std::current_exception(); + } + }; + +private: + std::coroutine_handle handle_; + +public: + explicit affinity_trampoline(std::coroutine_handle h) + : handle_(h) + { + } + + affinity_trampoline(affinity_trampoline&& o) noexcept + : handle_(std::exchange(o.handle_, {})) + { + } + + ~affinity_trampoline() { + if (handle_) + handle_.destroy(); + } + + bool await_ready() const noexcept { return false; } + + std::coroutine_handle<> + await_suspend(std::coroutine_handle<> caller) noexcept { + handle_.promise().caller_ = caller; + return handle_; + } + + void await_resume() { + if (handle_.promise().exception_) + std::rethrow_exception(handle_.promise().exception_); + } +}; + +} // detail + +/** Create an affinity trampoline for a legacy awaitable. + + This function wraps an awaitable in a trampoline coroutine + that ensures resumption occurs via the specified dispatcher. + After the inner awaitable completes, the trampoline dispatches + the continuation to the dispatcher before transferring control + back to the caller. + + This is the fallback path for awaitables that don't implement + the affine_awaitable protocol. Prefer implementing the protocol + for zero-overhead affinity. + + @par Usage + Typically used in await_transform for legacy awaitables: + @code + template + auto await_transform(Awaitable&& a) + { + using A = std::remove_cvref_t; + + if constexpr (affine_awaitable) { + // Zero overhead path + return affine_awaiter{ + std::forward(a), &dispatcher_}; + } else { + // Trampoline fallback + return make_affine( + std::forward(a), dispatcher_); + } + } + @endcode + + @par Dispatcher Requirements + The dispatcher must satisfy the dispatcher concept: + @code + struct Dispatcher + { + void operator()(std::coroutine_handle<> h); + }; + @endcode + + @param awaitable The awaitable to wrap. + @param dispatcher A callable used to dispatch the continuation. + Must remain valid until the awaitable completes. + + @return An awaitable that yields the same result as the wrapped + awaitable, with resumption occurring via the dispatcher. +*/ +template +auto make_affine(Awaitable&& awaitable, Dispatcher& dispatcher) + -> detail::affinity_trampoline> +{ + using result_t = detail::await_result_t; + + if constexpr (std::is_void_v) { + co_await detail::get_awaitable(std::forward(awaitable)); + co_await detail::dispatch_awaitable{dispatcher}; + } else { + auto result = co_await detail::get_awaitable( + std::forward(awaitable)); + co_await detail::dispatch_awaitable{dispatcher}; + co_return result; + } +} + +} // capy +} // boost + +#endif + +#endif diff --git a/include/boost/capy/async_result.hpp b/include/boost/capy/async_op.hpp similarity index 71% rename from include/boost/capy/async_result.hpp rename to include/boost/capy/async_op.hpp index a5b2fa2e..065e549f 100644 --- a/include/boost/capy/async_result.hpp +++ b/include/boost/capy/async_op.hpp @@ -7,8 +7,8 @@ // Official repository: https://github.com/cppalliance/capy // -#ifndef BOOST_CAPY_ASYNC_RESULT_HPP -#define BOOST_CAPY_ASYNC_RESULT_HPP +#ifndef BOOST_CAPY_ASYNC_OP_HPP +#define BOOST_CAPY_ASYNC_OP_HPP #include @@ -23,46 +23,31 @@ namespace boost { namespace capy { - -/** Concept for a deferred operation that produces a value. - - A deferred operation is a callable that accepts a completion - handler. When invoked, it initiates an asynchronous operation - and calls the handler with the result when complete. - - @tparam Op The operation type. - @tparam T The result type. -*/ -template -concept deferred_operation = std::invocable>; - -//----------------------------------------------------------------------------- - namespace detail { template -struct async_result_impl_base +struct async_op_impl_base { - virtual ~async_result_impl_base() = default; + virtual ~async_op_impl_base() = default; virtual void start(std::function on_done) = 0; virtual T get_result() = 0; }; -struct async_result_void_impl_base +struct async_op_void_impl_base { - virtual ~async_result_void_impl_base() = default; + virtual ~async_op_void_impl_base() = default; virtual void start(std::function on_done) = 0; virtual void get_result() = 0; }; template -struct async_result_impl : async_result_impl_base +struct async_op_impl : async_op_impl_base { DeferredOp op_; std::variant result_{}; explicit - async_result_impl(DeferredOp&& op) + async_op_impl(DeferredOp&& op) : op_(std::forward(op)) { } @@ -88,13 +73,13 @@ struct async_result_impl : async_result_impl_base }; template -struct async_result_void_impl : async_result_void_impl_base +struct async_op_void_impl : async_op_void_impl_base { DeferredOp op_; std::exception_ptr exception_{}; explicit - async_result_void_impl(DeferredOp&& op) + async_op_void_impl(DeferredOp&& op) : op_(std::forward(op)) { } @@ -131,9 +116,9 @@ struct async_result_void_impl : async_result_void_impl_base @par Example @code // Wrap a callback-based timer - async_result async_sleep(std::chrono::milliseconds ms) + async_op async_sleep(std::chrono::milliseconds ms) { - return make_async_result( + return make_async_op( [ms](auto&& handler) { // Start timer, call handler when done start_timer(ms, std::move(handler)); @@ -148,12 +133,12 @@ struct async_result_void_impl : async_result_void_impl_base @tparam T The type of value produced by the asynchronous operation. - @see make_async_result, task + @see make_async_op, task */ template -class async_result +class async_op { - std::unique_ptr> impl_; + std::unique_ptr> impl_; // Workaround: clang fails to match friend function template declarations #if defined(__clang__) && (__clang_major__ == 16 || \ @@ -161,7 +146,7 @@ class async_result public: #endif explicit - async_result(std::unique_ptr> p) + async_op(std::unique_ptr> p) : impl_(std::move(p)) { } @@ -172,8 +157,8 @@ class async_result template requires (!std::is_void_v) - friend async_result - make_async_result(DeferredOp&& op); + friend async_op + make_async_op(DeferredOp&& op); public: /** Return whether the result is ready. @@ -199,6 +184,22 @@ class async_result impl_->start([h]{ h.resume(); }); } + /** Suspend the caller with scheduler affinity. + + Initiates the asynchronous operation and arranges for + the caller to be resumed through the dispatcher when + it completes, maintaining scheduler affinity. + + @param h The coroutine handle of the awaiting coroutine. + @param dispatcher The dispatcher to resume through. + */ + template + void + await_suspend(std::coroutine_handle<> h, Dispatcher& dispatcher) + { + impl_->start([h, &dispatcher]{ dispatcher(h); }); + } + /** Return the result after completion. @return The value produced by the asynchronous operation. @@ -217,7 +218,7 @@ class async_result /** An awaitable wrapper for callback-based operations with no result. - This specialization of async_result is used for asynchronous + This specialization of async_op is used for asynchronous operations that signal completion but do not produce a value, such as timers, write operations, or connection establishment. @@ -228,9 +229,9 @@ class async_result @par Example @code // Wrap a callback-based timer - async_result async_sleep(std::chrono::milliseconds ms) + async_op async_sleep(std::chrono::milliseconds ms) { - return make_async_result( + return make_async_op( [ms](auto handler) { start_timer(ms, [h = std::move(handler)]{ h(); }); }); @@ -242,12 +243,12 @@ class async_result } @endcode - @see async_result, make_async_result + @see async_op, make_async_op */ template<> -class async_result +class async_op { - std::unique_ptr impl_; + std::unique_ptr impl_; // Workaround: clang fails to match friend function template declarations #if defined(__clang__) && (__clang_major__ == 16 || \ @@ -255,7 +256,7 @@ class async_result public: #endif explicit - async_result(std::unique_ptr p) + async_op(std::unique_ptr p) : impl_(std::move(p)) { } @@ -266,8 +267,8 @@ class async_result template requires std::is_void_v - friend async_result - make_async_result(DeferredOp&& op); + friend async_op + make_async_op(DeferredOp&& op); public: /** Return whether the result is ready. @@ -293,6 +294,22 @@ class async_result impl_->start([h]{ h.resume(); }); } + /** Suspend the caller with scheduler affinity. + + Initiates the asynchronous operation and arranges for + the caller to be resumed through the dispatcher when + it completes, maintaining scheduler affinity. + + @param h The coroutine handle of the awaiting coroutine. + @param dispatcher The dispatcher to resume through. + */ + template + void + await_suspend(std::coroutine_handle<> h, Dispatcher& dispatcher) + { + impl_->start([h, &dispatcher]{ dispatcher(h); }); + } + /** Complete the await and check for exceptions. @throws Any exception that occurred during the operation. @@ -306,16 +323,16 @@ class async_result //----------------------------------------------------------------------------- -/** Return an async_result from a deferred operation. +/** Return an async_op from a deferred operation. - This factory function creates an awaitable async_result that + This factory function creates an awaitable async_op that wraps a callback-based asynchronous operation. @par Example @code - async_result async_read() + async_op async_read() { - return make_async_result( + return make_async_op( [](auto handler) { // Simulate async read handler("Hello, World!"); @@ -329,31 +346,31 @@ class async_result it should initiate the asynchronous operation and call the handler with the result when complete. - @return An async_result that can be awaited in a coroutine. + @return An async_op that can be awaited in a coroutine. - @see async_result + @see async_op */ template requires (!std::is_void_v) [[nodiscard]] -async_result -make_async_result(DeferredOp&& op) +async_op +make_async_op(DeferredOp&& op) { - using impl_type = detail::async_result_impl>; - return async_result( + using impl_type = detail::async_op_impl>; + return async_op( std::make_unique(std::forward(op))); } -/** Return an async_result from a deferred operation. +/** Return an async_op from a deferred operation. This overload is used for operations that signal completion without producing a value. @par Example @code - async_result async_wait(int milliseconds) + async_op async_wait(int milliseconds) { - return make_async_result( + return make_async_op( [milliseconds](auto on_done) { // Start timer, call on_done() when elapsed start_timer(milliseconds, std::move(on_done)); @@ -365,18 +382,18 @@ make_async_result(DeferredOp&& op) arguments. When invoked, it should initiate the operation and call the handler when complete. - @return An async_result that can be awaited in a coroutine. + @return An async_op that can be awaited in a coroutine. - @see async_result + @see async_op */ template requires std::is_void_v [[nodiscard]] -async_result -make_async_result(DeferredOp&& op) +async_op +make_async_op(DeferredOp&& op) { - using impl_type = detail::async_result_void_impl>; - return async_result( + using impl_type = detail::async_op_void_impl>; + return async_op( std::make_unique(std::forward(op))); } diff --git a/include/boost/capy/bcrypt.hpp b/include/boost/capy/bcrypt.hpp index a6753661..8956d035 100644 --- a/include/boost/capy/bcrypt.hpp +++ b/include/boost/capy/bcrypt.hpp @@ -22,13 +22,13 @@ #include // Hash a password - boost::capy::bcrypt::result r; - boost::capy::bcrypt::hash(r, "my_password", 12); + capy::bcrypt::result r; + capy::bcrypt::hash(r, "my_password", 12); // Store r.str() in database... // Verify later - boost::system::error_code ec; + system::error_code ec; bool ok = boost::capy::bcrypt::compare("my_password", stored_hash, ec); if (ec) handle_malformed_hash(); diff --git a/include/boost/capy/executor.hpp b/include/boost/capy/executor.hpp index 4aa086b3..5fdfd639 100644 --- a/include/boost/capy/executor.hpp +++ b/include/boost/capy/executor.hpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include #include @@ -24,6 +24,15 @@ namespace boost { namespace capy { +#if 0 +class execution_context +{ +public: +private: + void post(work* w); +}; +#endif + /** A lightweight handle for submitting work to an execution context. This class provides a value-type interface for submitting @@ -309,7 +318,7 @@ class executor */ template void - post(F&& f); + post(F&& f) const; /** Submit work and invoke a handler on completion. @@ -330,7 +339,7 @@ class executor */ template auto - submit(F&& f, Handler&& handler) -> + submit(F&& f, Handler&& handler) const -> typename std::enable_if::type>::return_type>::value>::type; @@ -353,16 +362,17 @@ class executor */ template auto - submit(F&& f, Handler&& handler) -> + submit(F&& f, Handler&& handler) const -> typename std::enable_if::type >::return_type>::value>::type; #ifdef BOOST_CAPY_HAS_CORO + /** Submit work and return an awaitable result. The work function is executed asynchronously. The - returned async_result can be awaited in a coroutine + returned async_op can be awaited in a coroutine to obtain the result. @param f The work function to execute. @@ -371,14 +381,14 @@ class executor */ template auto - submit(F&& f) -> - async_result>> + submit(F&& f) const -> + async_op>> requires (!std::is_void_v>>); /** Submit work and return an awaitable result. The work function is executed asynchronously. The returned - async_result can be awaited in a coroutine to wait + async_op can be awaited in a coroutine to wait for completion. @param f The work function to execute. @@ -387,9 +397,10 @@ class executor */ template auto - submit(F&& f) -> - async_result + submit(F&& f) const -> + async_op requires std::is_void_v>>; + #endif }; @@ -561,7 +572,7 @@ class executor::factory @param ex The executor to submit work to. */ explicit - factory(executor& ex) noexcept + factory(executor const& ex) noexcept : ops_(ex.ops_.get()) , obj_(ex.obj_) , storage_(nullptr) @@ -618,7 +629,7 @@ class executor::factory template void executor:: -post(F&& f) +post(F&& f) const { struct callable : work { @@ -648,7 +659,7 @@ post(F&& f) template auto executor:: -submit(F&& f, Handler&& handler) -> +submit(F&& f, Handler&& handler) const -> typename std::enable_if::type >::return_type>::value>::type @@ -681,7 +692,7 @@ submit(F&& f, Handler&& handler) -> template auto executor:: -submit(F&& f, Handler&& handler) -> +submit(F&& f, Handler&& handler) const -> typename std::enable_if::type >::return_type>::value>::type @@ -715,13 +726,13 @@ submit(F&& f, Handler&& handler) -> template auto executor:: -submit(F&& f) -> - async_result>> +submit(F&& f) const -> + async_op>> requires (!std::is_void_v>>) { using T = std::invoke_result_t>; - return make_async_result( + return make_async_op( [ex = *this, f = std::forward(f)](auto on_done) mutable { ex.post( @@ -736,11 +747,11 @@ submit(F&& f) -> template auto executor:: -submit(F&& f) -> - async_result +submit(F&& f) const -> + async_op requires std::is_void_v>> { - return make_async_result( + return make_async_op( [ex = *this, f = std::forward(f)](auto on_done) mutable { ex.post( diff --git a/include/boost/capy/make_affine.hpp b/include/boost/capy/make_affine.hpp deleted file mode 100644 index 324ada8f..00000000 --- a/include/boost/capy/make_affine.hpp +++ /dev/null @@ -1,350 +0,0 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// - -#ifndef BOOST_CAPY_MAKE_AFFINE_HPP -#define BOOST_CAPY_MAKE_AFFINE_HPP - -#include - -#ifdef BOOST_CAPY_HAS_CORO - -#include - -#include -#include -#include -#include -#include - -namespace boost { -namespace capy { - -namespace detail { - -/** Awaitable that dispatches resumption through an executor. - - If the executor is empty (no affinity), resumes inline. - Otherwise posts the resumption to the executor. -*/ -struct dispatch_awaitable -{ - mutable executor ex_; - - bool - await_ready() const noexcept - { - return !ex_; // skip suspend if no affinity - } - - void - await_suspend(std::coroutine_handle<> h) const - { - ex_.post([h]{ h.resume(); }); - } - - void - await_resume() const noexcept - { - } -}; - -template -auto -get_awaitable(T&& expr) -{ - if constexpr(requires { std::forward(expr).operator co_await(); }) - return std::forward(expr).operator co_await(); - else if constexpr(requires { operator co_await(std::forward(expr)); }) - return operator co_await(std::forward(expr)); - else - return std::forward(expr); -} - -template -using awaitable_type = decltype(get_awaitable(std::declval())); - -template -using await_result_t = decltype(std::declval>().await_resume()); - -struct transfer_to_caller -{ - std::coroutine_handle<> caller_; - - bool - await_ready() noexcept - { - return false; - } - - std::coroutine_handle<> - await_suspend(std::coroutine_handle<>) noexcept - { - return caller_; - } - - void - await_resume() noexcept - { - } -}; - -template -class affinity_trampoline -{ -public: - struct promise_type - { - std::optional value_; - std::exception_ptr exception_; - std::coroutine_handle<> caller_; - - affinity_trampoline - get_return_object() - { - return affinity_trampoline{ - std::coroutine_handle::from_promise(*this)}; - } - - std::suspend_always - initial_suspend() noexcept - { - return {}; - } - - transfer_to_caller - final_suspend() noexcept - { - return {caller_}; - } - - template - void - return_value(U&& v) - { - value_.emplace(std::forward(v)); - } - - void - unhandled_exception() - { - exception_ = std::current_exception(); - } - }; - -private: - std::coroutine_handle handle_; - -public: - explicit - affinity_trampoline(std::coroutine_handle h) - : handle_(h) - { - } - - affinity_trampoline(affinity_trampoline&& o) noexcept - : handle_(std::exchange(o.handle_, {})) - { - } - - ~affinity_trampoline() - { - if(handle_) - handle_.destroy(); - } - - bool - await_ready() const noexcept - { - return false; - } - - std::coroutine_handle<> - await_suspend(std::coroutine_handle<> caller) noexcept - { - handle_.promise().caller_ = caller; - return handle_; - } - - T - await_resume() - { - if(handle_.promise().exception_) - std::rethrow_exception(handle_.promise().exception_); - return std::move(*handle_.promise().value_); - } -}; - -template<> -class affinity_trampoline -{ -public: - struct promise_type - { - std::exception_ptr exception_; - std::coroutine_handle<> caller_; - - affinity_trampoline - get_return_object() - { - return affinity_trampoline{ - std::coroutine_handle::from_promise(*this)}; - } - - std::suspend_always - initial_suspend() noexcept - { - return {}; - } - - transfer_to_caller - final_suspend() noexcept - { - return {caller_}; - } - - void - return_void() noexcept - { - } - - void - unhandled_exception() - { - exception_ = std::current_exception(); - } - }; - -private: - std::coroutine_handle handle_; - -public: - explicit - affinity_trampoline(std::coroutine_handle h) - : handle_(h) - { - } - - affinity_trampoline(affinity_trampoline&& o) noexcept - : handle_(std::exchange(o.handle_, {})) - { - } - - ~affinity_trampoline() - { - if(handle_) - handle_.destroy(); - } - - bool - await_ready() const noexcept - { - return false; - } - - std::coroutine_handle<> - await_suspend(std::coroutine_handle<> caller) noexcept - { - handle_.promise().caller_ = caller; - return handle_; - } - - void - await_resume() - { - if(handle_.promise().exception_) - std::rethrow_exception(handle_.promise().exception_); - } -}; - -} // detail - -//------------------------------------------------ - -/** Create an affinity trampoline for an awaitable. - - This function wraps an awaitable in a trampoline coroutine - that ensures resumption occurs via the specified executor. - After the inner awaitable completes, the trampoline dispatches - the continuation to the executor before transferring control - back to the caller. - - When used with `await_transform`, this enables executor affinity - for coroutines - ensuring that after any `co_await`, the coroutine - resumes on its designated executor regardless of where the - awaited operation completed. - - If the executor is empty (no affinity), the trampoline resumes - the caller inline without any dispatch overhead. - - @par Example - @code - struct my_task - { - struct promise_type - { - executor ex; - - template - auto - await_transform(Awaitable&& a) - { - return make_affine( - std::forward(a), - ex); - } - - // ... other promise_type members - }; - - // ... other task members - }; - @endcode - - @par HALO Optimization - The trampoline coroutine is designed to be elided by the - compiler's Heap Allocation eLision Optimization (HALO), - resulting in zero allocation overhead. - - @param awaitable The awaitable to wrap. - @param ex The executor to dispatch resumption through. - If empty, resumption occurs inline. - - @return An awaitable trampoline that yields the same result - as the wrapped awaitable. -*/ -template -auto -make_affine( - Awaitable&& awaitable, - executor ex) -> - detail::affinity_trampoline< - detail::await_result_t> -{ - using result_t = detail::await_result_t; - - if constexpr(std::is_void_v) - { - co_await detail::get_awaitable(std::forward(awaitable)); - co_await detail::dispatch_awaitable{ex}; - } - else - { - auto result = co_await detail::get_awaitable(std::forward(awaitable)); - co_await detail::dispatch_awaitable{ex}; - co_return result; - } -} - -} // capy -} // boost - -#endif - -#endif - diff --git a/include/boost/capy/task.hpp b/include/boost/capy/task.hpp index fb3a7d95..fc68a789 100644 --- a/include/boost/capy/task.hpp +++ b/include/boost/capy/task.hpp @@ -1,11 +1,16 @@ -// -// Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com) -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/capy -// +// Copyright Vinnie Falco +// SPDX-License-Identifier: BSL-1.0 + +/** + @file task.hpp + + Lazy coroutine task type with executor affinity. + + Provides task, a lazy coroutine that produces a value of type T, + and spawn() for running tasks with completion handlers. Tasks support + executor affinity via on() to control which executor resumes the + coroutine after each co_await. +*/ #ifndef BOOST_CAPY_TASK_HPP #define BOOST_CAPY_TASK_HPP @@ -14,34 +19,57 @@ #ifdef BOOST_CAPY_HAS_CORO +#include +#include #include -#include + +#include #include #include #include #include #include -#include namespace boost { namespace capy { -// Forward declaration for is_task trait -template -class task; - namespace detail { -template -struct is_task_impl : std::false_type {}; +/** Adapter that wraps executor and satisfies the dispatcher concept. -template -struct is_task_impl> : std::true_type {}; + This struct provides operator() by delegating to executor::post(), + enabling use with the affine awaitable protocol. It is stored as + a data member in the promise to ensure stable lifetime. +*/ +struct executor_dispatcher +{ + executor ex_; -template -inline constexpr bool is_task_v = - is_task_impl>::value; + executor_dispatcher() = default; + + explicit + executor_dispatcher(executor ex) noexcept + : ex_(std::move(ex)) + { + } + + template + void + operator()(F&& f) const + { + if (ex_) + ex_.post(std::forward(f)); + else + std::forward(f)(); + } + + explicit + operator bool() const noexcept + { + return static_cast(ex_); + } +}; } // detail @@ -71,10 +99,11 @@ inline constexpr bool is_task_v = @tparam T The type of value produced by the coroutine. - @see async_result + @see async_op, launch */ template class task + : public affine_task, detail::executor_dispatcher> { public: /** The coroutine promise type. @@ -84,44 +113,92 @@ class task notification. */ struct promise_type + : affine_promise { - /// Storage for the result value or exception - std::variant result{}; + /// Storage for the result value or exception (empty exception_ptr = incomplete) + system::result result_{std::exception_ptr{}}; + + /// Dispatcher for await_transform (always present for consistent types) + detail::executor_dispatcher await_dispatcher_{}; + + /** Get the executor for affinity. + + @return The executor used for resumption affinity. + */ + executor + get_executor() const noexcept + { + return await_dispatcher_.ex_; + } + + /** Set the executor for affinity. + + @param ex The executor to resume on after co_await. + */ + void + set_executor(executor ex) noexcept + { + await_dispatcher_ = detail::executor_dispatcher{std::move(ex)}; + // Also set on base class for final_suspend behavior + if (await_dispatcher_) + this->affine_promise::set_dispatcher(await_dispatcher_); + else + this->dispatcher_.reset(); + } - /// Callback invoked when the coroutine completes - std::function on_done; + /** Set the dispatcher for affinity (inheritance). - /// Executor for affinity (empty = no affinity) - executor ex{}; + Called by affine_task::await_suspend when a parent task + awaits this task with a dispatcher. Only sets the dispatcher + if not already set, so explicit affinity via on() takes + precedence over inherited affinity. + + @param d The dispatcher to use for resumption. + */ + void + set_dispatcher(detail::executor_dispatcher d) + { + // Only inherit if not explicitly set (explicit affinity takes precedence) + if (!await_dispatcher_) + { + await_dispatcher_ = d; + this->affine_promise::set_dispatcher(std::move(d)); + } + } /** Transform awaitables for executor affinity. - Wraps all co_await expressions with make_affine to ensure - the coroutine resumes on the configured executor. Also - propagates affinity to child tasks that lack explicit - affinity. + Wraps co_await expressions to ensure the coroutine resumes + on the configured executor. Uses affine_awaiter for + affine-aware awaitables (zero overhead) and make_affine + trampoline for legacy awaitables. @param a The awaitable to transform. @return An affinity-wrapped awaitable. */ template - auto await_transform(Awaitable&& a) + auto + await_transform(Awaitable&& a) { - // Propagate affinity to child tasks without explicit affinity - if constexpr (detail::is_task_v) + // Use if constexpr to get consistent return type per branch + if constexpr (affine_awaitable) { - auto& child_ex = a.handle().promise().ex; - if (!child_ex && ex) - child_ex = ex; + // Affine-aware: use affine_awaiter (zero overhead) + return affine_awaiter{std::forward(a), &await_dispatcher_}; + } + else + { + // Legacy: use make_affine trampoline + return make_affine(std::forward(a), await_dispatcher_); } - return make_affine(std::forward(a), ex); } /** Returns the task object for this coroutine. @return A task owning the coroutine handle. */ - task get_return_object() + task + get_return_object() { return task{std::coroutine_handle::from_promise(*this)}; } @@ -132,40 +209,52 @@ class task @return An awaitable that always suspends. */ - std::suspend_always initial_suspend() noexcept { return {}; } - - /** Suspend at the end and notify completion. - - @return An awaitable that suspends and invokes the - completion callback if set. - */ - auto final_suspend() noexcept + std::suspend_always + initial_suspend() noexcept { - struct awaiter - { - promise_type* p_; - bool await_ready() noexcept { return false; } - void await_suspend(std::coroutine_handle<>) noexcept - { - if (p_->on_done) - p_->on_done(); - } - void await_resume() noexcept {} - }; - return awaiter{this}; + return {}; } /** Store the return value. @param v The value to store as the coroutine result. */ - void return_value(T v) { result.template emplace<1>(std::move(v)); } + void + return_value(T v) + { + result_ = std::move(v); + } /** Store an unhandled exception. Captures the current exception for later rethrowing. */ - void unhandled_exception() { result.template emplace<2>(std::current_exception()); } + void + unhandled_exception() + { +#if defined(__GNUC__) && __GNUC__ >= 12 && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + result_ = std::current_exception(); +#if defined(__GNUC__) && __GNUC__ >= 12 && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + } + + /** Retrieve the result for await_resume. + + @return The value produced by the coroutine. + + @throws Any exception that was thrown inside the coroutine. + */ + T + result() + { + if (result_.has_error()) + std::rethrow_exception(result_.error()); + return std::move(*result_); + } }; private: @@ -176,67 +265,46 @@ class task @param h The coroutine handle to take ownership of. */ - explicit task(std::coroutine_handle h) : h_(h) {} + explicit + task(std::coroutine_handle h) + : h_(h) + { + } /** Destructor. Destroys the owned coroutine if present. */ - ~task() { if (h_) h_.destroy(); } + ~task() + { + if (h_) + h_.destroy(); + } /** Move constructor. @param o The task to move from. After the move, @p o will be empty. */ - task(task&& o) noexcept : h_(std::exchange(o.h_, {})) {} - - /// Move assignment is deleted. - task& operator=(task&&) = delete; - - /** Check if the task is ready. - - @return Always returns false; the task must be awaited. - */ - bool await_ready() const noexcept { return false; } - - /** Suspend the caller and start this task. - - Sets up the completion callback to resume the caller - when this task completes, then transfers control to - this task's coroutine. - - @param caller The coroutine handle of the awaiting coroutine. - - @return The coroutine handle to resume (this task's handle). - */ - std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept + task(task&& o) noexcept + : h_(std::exchange(o.h_, {})) { - h_.promise().on_done = [caller]{ caller.resume(); }; - return h_; } - /** Retrieve the result after completion. - - @return The value produced by the coroutine. - - @throws Any exception that was thrown inside the coroutine. - */ - [[nodiscard]] - T await_resume() - { - auto& r = h_.promise().result; - if (r.index() == 2) - std::rethrow_exception(std::get<2>(r)); - return std::move(std::get<1>(r)); - } + /// Move assignment is deleted. + task& + operator=(task&&) = delete; /** Access the underlying coroutine handle. @return The coroutine handle, without transferring ownership. */ [[nodiscard]] - std::coroutine_handle handle() const noexcept { return h_; } + std::coroutine_handle + handle() const noexcept + { + return h_; + } /** Release ownership of the coroutine handle. @@ -246,7 +314,8 @@ class task @return The coroutine handle. */ [[nodiscard]] - std::coroutine_handle release() noexcept + std::coroutine_handle + release() noexcept { return std::exchange(h_, {}); } @@ -264,23 +333,25 @@ class task @par Example @code - task example(executor pool) + task example(executor ex) { - // parse_request resumes on pool after internal co_awaits - auto data = co_await parse_request().on(pool); + // parse_request resumes on ex after internal co_awaits + auto data = co_await parse_request().on(ex); } @endcode */ - task& on(executor e) & + task& + on(executor ex) & { - h_.promise().ex = e; + h_.promise().set_executor(std::move(ex)); return *this; } /// @copydoc on(executor) - task&& on(executor e) && + task&& + on(executor ex) && { - h_.promise().ex = e; + h_.promise().set_executor(std::move(ex)); return std::move(*this); } }; @@ -311,10 +382,11 @@ class task } @endcode - @see task, async_result + @see task, async_op, launch */ template<> class task + : public affine_task, detail::executor_dispatcher> { public: /** The coroutine promise type for void tasks. @@ -323,44 +395,92 @@ class task and manages exception storage and completion notification. */ struct promise_type + : affine_promise { - /// Storage for an exception, if one was thrown - std::exception_ptr exception_{}; + /// Storage for exception (nullptr = success) + std::exception_ptr error_; + + /// Dispatcher for await_transform (always present for consistent types) + detail::executor_dispatcher await_dispatcher_{}; + + /** Get the executor for affinity. + + @return The executor used for resumption affinity. + */ + executor + get_executor() const noexcept + { + return await_dispatcher_.ex_; + } + + /** Set the executor for affinity. + + @param ex The executor to resume on after co_await. + */ + void + set_executor(executor ex) noexcept + { + await_dispatcher_ = detail::executor_dispatcher{std::move(ex)}; + // Also set on base class for final_suspend behavior + if (await_dispatcher_) + this->affine_promise::set_dispatcher(await_dispatcher_); + else + this->dispatcher_.reset(); + } - /// Callback invoked when the coroutine completes - std::function on_done; + /** Set the dispatcher for affinity (inheritance). - /// Executor for affinity (empty = no affinity) - executor ex{}; + Called by affine_task::await_suspend when a parent task + awaits this task with a dispatcher. Only sets the dispatcher + if not already set, so explicit affinity via on() takes + precedence over inherited affinity. + + @param d The dispatcher to use for resumption. + */ + void + set_dispatcher(detail::executor_dispatcher d) + { + // Only inherit if not explicitly set (explicit affinity takes precedence) + if (!await_dispatcher_) + { + await_dispatcher_ = d; + this->affine_promise::set_dispatcher(std::move(d)); + } + } /** Transform awaitables for executor affinity. - Wraps all co_await expressions with make_affine to ensure - the coroutine resumes on the configured executor. Also - propagates affinity to child tasks that lack explicit - affinity. + Wraps co_await expressions to ensure the coroutine resumes + on the configured executor. Uses affine_awaiter for + affine-aware awaitables (zero overhead) and make_affine + trampoline for legacy awaitables. @param a The awaitable to transform. @return An affinity-wrapped awaitable. */ template - auto await_transform(Awaitable&& a) + auto + await_transform(Awaitable&& a) { - // Propagate affinity to child tasks without explicit affinity - if constexpr (detail::is_task_v) + // Use if constexpr to get consistent return type per branch + if constexpr (affine_awaitable) + { + // Affine-aware: use affine_awaiter (zero overhead) + return affine_awaiter{std::forward(a), &await_dispatcher_}; + } + else { - auto& child_ex = a.handle().promise().ex; - if (!child_ex && ex) - child_ex = ex; + // Legacy: use make_affine trampoline + return make_affine(std::forward(a), await_dispatcher_); } - return make_affine(std::forward(a), ex); } /** Returns the task object for this coroutine. @return A task owning the coroutine handle. */ - task get_return_object() + task + get_return_object() { return task{std::coroutine_handle::from_promise(*this)}; } @@ -371,40 +491,42 @@ class task @return An awaitable that always suspends. */ - std::suspend_always initial_suspend() noexcept { return {}; } - - /** Suspend at the end and notify completion. - - @return An awaitable that suspends and invokes the - completion callback if set. - */ - auto final_suspend() noexcept + std::suspend_always + initial_suspend() noexcept { - struct awaiter - { - promise_type* p_; - bool await_ready() noexcept { return false; } - void await_suspend(std::coroutine_handle<>) noexcept - { - if (p_->on_done) - p_->on_done(); - } - void await_resume() noexcept {} - }; - return awaiter{this}; + return {}; } /** Signal coroutine completion. Called when the coroutine executes `co_return;`. */ - void return_void() noexcept {} + void + return_void() noexcept + { + error_ = nullptr; + } /** Store an unhandled exception. Captures the current exception for later rethrowing. */ - void unhandled_exception() { exception_ = std::current_exception(); } + void + unhandled_exception() noexcept + { + error_ = std::current_exception(); + } + + /** Retrieve the result for await_resume. + + @throws Any exception that was thrown inside the coroutine. + */ + void + result() + { + if (error_) + std::rethrow_exception(error_); + } }; private: @@ -415,62 +537,46 @@ class task @param h The coroutine handle to take ownership of. */ - explicit task(std::coroutine_handle h) : h_(h) {} + explicit + task(std::coroutine_handle h) + : h_(h) + { + } /** Destructor. Destroys the owned coroutine if present. */ - ~task() { if (h_) h_.destroy(); } + ~task() + { + if (h_) + h_.destroy(); + } /** Move constructor. @param o The task to move from. After the move, @p o will be empty. */ - task(task&& o) noexcept : h_(std::exchange(o.h_, {})) {} - - /// Move assignment is deleted. - task& operator=(task&&) = delete; - - /** Check if the task is ready. - - @return Always returns false; the task must be awaited. - */ - bool await_ready() const noexcept { return false; } - - /** Suspend the caller and start this task. - - Sets up the completion callback to resume the caller - when this task completes, then transfers control to - this task's coroutine. - - @param caller The coroutine handle of the awaiting coroutine. - - @return The coroutine handle to resume (this task's handle). - */ - std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept + task(task&& o) noexcept + : h_(std::exchange(o.h_, {})) { - h_.promise().on_done = [caller]{ caller.resume(); }; - return h_; } - /** Complete the await operation. - - @throws Any exception that was thrown inside the coroutine. - */ - void await_resume() - { - if (h_.promise().exception_) - std::rethrow_exception(h_.promise().exception_); - } + /// Move assignment is deleted. + task& + operator=(task&&) = delete; /** Access the underlying coroutine handle. @return The coroutine handle, without transferring ownership. */ [[nodiscard]] - std::coroutine_handle handle() const noexcept { return h_; } + std::coroutine_handle + handle() const noexcept + { + return h_; + } /** Release ownership of the coroutine handle. @@ -480,7 +586,8 @@ class task @return The coroutine handle. */ [[nodiscard]] - std::coroutine_handle release() noexcept + std::coroutine_handle + release() noexcept { return std::exchange(h_, {}); } @@ -498,27 +605,182 @@ class task @par Example @code - task example(executor pool) + task example(executor ex) { - // do_work resumes on pool after internal co_awaits - co_await do_work().on(pool); + // do_work resumes on ex after internal co_awaits + co_await do_work().on(ex); } @endcode */ - task& on(executor e) & + task& + on(executor ex) & { - h_.promise().ex = e; + h_.promise().set_executor(std::move(ex)); return *this; } /// @copydoc on(executor) - task&& on(executor e) && + task&& + on(executor ex) && { - h_.promise().ex = e; + h_.promise().set_executor(std::move(ex)); return std::move(*this); } }; +//----------------------------------------------------------------------------- + +namespace detail { + +/** Fire-and-forget coroutine for spawn(). + + This coroutine runs the spawned task and delivers the result + to the completion handler. It never suspends at final_suspend, + so the frame is destroyed immediately upon completion. +*/ +template +struct spawner +{ + struct promise_type + { + spawner + get_return_object() noexcept + { + return {}; + } + + std::suspend_never + initial_suspend() noexcept + { + return {}; + } + + std::suspend_never + final_suspend() noexcept + { + return {}; + } + + void + return_void() noexcept + { + } + + void + unhandled_exception() + { + // Handler is called with exception in spawn's try/catch + std::terminate(); + } + }; +}; + +} // detail + +/** Spawn a task on an executor with a completion handler. + + This function starts a task running on the specified executor. + When the task completes (with a value or exception), the handler + is invoked with the result. + + The handler receives `system::result` which + holds either the task's return value or any exception that was + thrown during execution. + + The coroutine frame is allocated using the executor's allocator. + + @param ex The executor to run the task on. + @param t The task to spawn. Ownership is transferred. + @param handler The completion handler to invoke with the result. + + @par Handler Signature + @code + void handler(system::result result); + @endcode + + @par Example + @code + task compute() + { + co_return 42; + } + + void start_work(executor ex) + { + spawn(ex, compute(), [](auto result) { + if (result.has_value()) + std::cout << "Result: " << *result << std::endl; + else + std::cerr << "Error occurred\n"; + }); + } + @endcode + + @see task, executor, system::result +*/ +template +void +spawn(executor ex, task t, Handler&& handler) +{ + using result_type = system::result; + t.on(ex); + auto do_spawn = []( + task t, + std::decay_t h) -> detail::spawner + { +#if defined(__GNUC__) && __GNUC__ >= 12 && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + try + { + h(result_type(co_await t)); + } + catch (...) + { + h(result_type(std::current_exception())); + } +#if defined(__GNUC__) && __GNUC__ >= 12 && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + }; + do_spawn(std::move(t), std::forward(handler)); +} + +/** Spawn a void task on an executor with a completion handler. + + @copydetails spawn(executor,task,Handler&&) +*/ +template +void +spawn(executor ex, task t, Handler&& handler) +{ + using result_type = system::result; + t.on(ex); + auto do_spawn = []( + task t, + std::decay_t h) -> detail::spawner + { +#if defined(__GNUC__) && __GNUC__ >= 12 && !defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + try + { + co_await t; + h(result_type()); + } + catch (...) + { + h(result_type(std::current_exception())); + } +#if defined(__GNUC__) && __GNUC__ >= 12 && !defined(__clang__) +#pragma GCC diagnostic pop +#endif + }; + do_spawn(std::move(t), std::forward(handler)); +} + } // capy } // boost diff --git a/test/cmake_test/CMakeLists.txt b/test/cmake_test/CMakeLists.txt index a32bc2e4..34176f50 100644 --- a/test/cmake_test/CMakeLists.txt +++ b/test/cmake_test/CMakeLists.txt @@ -16,8 +16,29 @@ if(BOOST_CI_INSTALL_TEST) find_package(Boost CONFIG REQUIRED COMPONENTS capy) else() set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) - set(BOOST_INCLUDE_LIBRARIES capy) - add_subdirectory(../../../.. boostorg/boost) + add_subdirectory(../.. boostorg/capy) + + set(deps + # Primary dependencies + + assert + config + core + system + throw_exception + + # Secondary dependencies + + compat + variant2 + winapi + mp11 + predef + ) + + foreach(dep IN LISTS deps) + add_subdirectory(../../../${dep} boostorg/${dep} EXCLUDE_FROM_ALL) + endforeach() endif() add_executable(main main.cpp) diff --git a/test/unit/make_affine.cpp b/test/unit/affine.cpp similarity index 81% rename from test/unit/make_affine.cpp rename to test/unit/affine.cpp index 99d9ea76..f5502df1 100644 --- a/test/unit/make_affine.cpp +++ b/test/unit/affine.cpp @@ -8,7 +8,7 @@ // // Test that header file is self-contained. -#include +#include #ifdef BOOST_CAPY_HAS_CORO @@ -17,7 +17,7 @@ namespace boost { namespace capy { -struct make_affine_test +struct affine_test { void run() @@ -26,8 +26,8 @@ struct make_affine_test }; TEST_SUITE( - make_affine_test, - "boost.capy.make_affine"); + affine_test, + "boost.capy.affine"); } // capy } // boost diff --git a/test/unit/async_result.cpp b/test/unit/async_op.cpp similarity index 79% rename from test/unit/async_result.cpp rename to test/unit/async_op.cpp index 8713316c..eb477149 100644 --- a/test/unit/async_result.cpp +++ b/test/unit/async_op.cpp @@ -8,7 +8,7 @@ // // Test that header file is self-contained. -#include +#include #ifdef BOOST_CAPY_HAS_CORO @@ -50,32 +50,32 @@ struct async_test_exception : std::runtime_error struct result_with_error { int value; - boost::system::error_code ec; + system::error_code ec; result_with_error() = default; - result_with_error(int v, boost::system::error_code e = {}) + result_with_error(int v, system::error_code e = {}) : value(v) , ec(e) { } }; -struct async_result_test +struct async_op_test { - static async_result + static async_op async_int_value() { - return make_async_result( + return make_async_op( [](auto cb) { cb(42); }); } - static async_result + static async_op async_string_value() { - return make_async_result( + return make_async_op( [](auto cb) { cb("hello async"); }); @@ -98,35 +98,35 @@ struct async_result_test void testBasicValue() { - // async_result returning int + // async_op returning int { auto t = task_awaiting_int(); BOOST_TEST_EQ(run_task(t), 42); } - // async_result returning string + // async_op returning string { auto t = task_awaiting_string(); BOOST_TEST_EQ(run_task(t), "hello async"); } } - static async_result + static async_op async_returns_success() { - return make_async_result( + return make_async_op( [](auto cb) { - cb(100, boost::system::error_code{}); + cb(100, system::error_code{}); }); } - static async_result + static async_op async_returns_error() { - return make_async_result( + return make_async_op( [](auto cb) { - cb(0, boost::system::errc::make_error_code( - boost::system::errc::invalid_argument)); + cb(0, system::errc::make_error_code( + system::errc::invalid_argument)); }); } @@ -156,7 +156,7 @@ struct async_result_test void testErrorHandling() { - // async_result with success + // async_op with success { auto t = task_awaits_success(); auto r = run_task(t); @@ -164,13 +164,13 @@ struct async_result_test BOOST_TEST(!r.ec); } - // async_result with error + // async_op with error { auto t = task_awaits_error(); auto r = run_task(t); BOOST_TEST_EQ(r.value, 0); BOOST_TEST(r.ec); - BOOST_TEST_EQ(r.ec, boost::system::errc::invalid_argument); + BOOST_TEST_EQ(r.ec, system::errc::invalid_argument); } // task checks error and returns appropriate value @@ -180,24 +180,24 @@ struct async_result_test } } - static async_result + static async_op async_value_1() { - return make_async_result( + return make_async_op( [](auto cb) { cb(10); }); } - static async_result + static async_op async_value_2() { - return make_async_result( + return make_async_op( [](auto cb) { cb(20); }); } - static async_result + static async_op async_value_3() { - return make_async_result( + return make_async_op( [](auto cb) { cb(30); }); } @@ -227,14 +227,14 @@ struct async_result_test void testMoveOperations() { - // async_result is move constructible + // async_op is move constructible { auto ar1 = async_int_value(); auto ar2 = std::move(ar1); (void)ar2; } - // async_result is move assignable + // async_op is move assignable { auto ar1 = async_int_value(); auto ar2 = async_string_value(); @@ -243,10 +243,10 @@ struct async_result_test } } - static async_result + static async_op async_with_captured_state(int multiplier) { - return make_async_result( + return make_async_op( [multiplier](auto cb) { cb(10 * multiplier); }); @@ -282,10 +282,10 @@ struct async_result_test } }; - static async_result + static async_op async_complex() { - return make_async_result( + return make_async_op( [](auto cb) { cb(1, "test", 3.14); }); @@ -330,14 +330,12 @@ struct async_result_test BOOST_TEST_EQ(run_task(t), 94); } - //---------------------------------------------------------- - // async_result tests - //---------------------------------------------------------- + // async_op tests - static async_result + static async_op async_void_basic() { - return make_async_result( + return make_async_op( [](auto on_done) { on_done(); }); @@ -353,17 +351,16 @@ struct async_result_test void testVoidAsyncBasic() { - bool done = false; auto t = task_awaits_void_async(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); } - static async_result + static async_op async_void_step() { - return make_async_result( + return make_async_op( [](auto on_done) { on_done(); }); @@ -397,11 +394,10 @@ struct async_result_test void testVoidAsyncChain() { - bool done = false; auto t = task_awaits_multiple_void(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); } void @@ -419,10 +415,10 @@ struct async_result_test (void)ar2; } - static async_result + static async_op async_void_deferred() { - return make_async_result( + return make_async_op( [](auto on_done) { // Simulate deferred completion on_done(); @@ -455,7 +451,7 @@ struct async_result_test testComplexResult(); testTaskChaining(); - // async_result tests + // async_op tests testVoidAsyncBasic(); testVoidAsyncWithValue(); testVoidAsyncChain(); @@ -466,8 +462,8 @@ struct async_result_test }; TEST_SUITE( - async_result_test, - "boost.capy.async_result"); + async_op_test, + "boost.capy.async_op"); } // capy } // boost diff --git a/test/unit/task.cpp b/test/unit/task.cpp index 3b5aa754..e2e19576 100644 --- a/test/unit/task.cpp +++ b/test/unit/task.cpp @@ -12,7 +12,7 @@ #ifdef BOOST_CAPY_HAS_CORO -#include +#include #include #include "test_suite.hpp" @@ -21,6 +21,7 @@ #include #include #include +#include namespace boost { namespace capy { @@ -83,6 +84,12 @@ struct test_exception : std::runtime_error } }; +[[noreturn]] inline void +throw_test_exception(char const* msg) +{ + throw test_exception(msg); +} + struct task_test { static task @@ -259,41 +266,41 @@ struct task_test while (!h.done()) h.resume(); - auto& result = h.promise().result; - BOOST_TEST_EQ(result.index(), 1u); - BOOST_TEST_EQ(std::get<1>(result), 42); + auto& result = h.promise().result_; + BOOST_TEST(result.has_value()); + BOOST_TEST_EQ(*result, 42); h.destroy(); } } - static async_result + static async_op async_returns_value() { - return make_async_result( + return make_async_op( [](auto cb) { cb(123); }); } - static async_result + static async_op async_with_delayed_completion() { - return make_async_result( + return make_async_op( [](auto cb) { cb(456); }); } static task - task_awaits_async_result() + task_awaits_async_op() { int v = co_await async_returns_value(); co_return v + 1; } static task - task_awaits_multiple_async_results() + task_awaits_multiple_async_ops() { int v1 = co_await async_returns_value(); int v2 = co_await async_with_delayed_completion(); @@ -303,15 +310,15 @@ struct task_test void testTaskAwaitsAsyncResult() { - // task awaits single async_result + // task awaits single async_op { - auto t = task_awaits_async_result(); + auto t = task_awaits_async_op(); BOOST_TEST_EQ(run_task(t), 124); } - // task awaits multiple async_results + // task awaits multiple async_ops { - auto t = task_awaits_multiple_async_results(); + auto t = task_awaits_multiple_async_ops(); BOOST_TEST_EQ(run_task(t), 579); } } @@ -323,9 +330,7 @@ struct task_test BOOST_TEST(!t.await_ready()); } - //---------------------------------------------------------- // task tests - //---------------------------------------------------------- static task void_task_basic() @@ -343,11 +348,9 @@ struct task_test void testVoidTaskBasic() { - bool done = false; auto t = void_task_basic(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + while (!t.handle().done()) + t.handle().resume(); t.await_resume(); // should not throw } @@ -355,8 +358,8 @@ struct task_test testVoidTaskException() { auto t = void_task_throws(); - t.handle().promise().on_done = []{ }; - t.handle().resume(); + while (!t.handle().done()) + t.handle().resume(); BOOST_TEST_THROWS(t.await_resume(), test_exception); } @@ -380,20 +383,18 @@ struct task_test { // void task awaits value-returning task { - bool done = false; auto t = void_task_awaits_value(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); } // void task awaits another void task { - bool done = false; auto t = void_task_awaits_void(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); } } @@ -415,11 +416,10 @@ struct task_test void testVoidTaskChain() { - bool done = false; auto t = void_task_chain(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); } void @@ -435,7 +435,7 @@ struct task_test } static task - void_task_awaits_async_result() + void_task_awaits_async_op() { int v = co_await async_returns_value(); (void)v; @@ -445,16 +445,13 @@ struct task_test void testVoidTaskAwaitsAsyncResult() { - bool done = false; - auto t = void_task_awaits_async_result(); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); - BOOST_TEST(done); + auto t = void_task_awaits_async_op(); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); } - //---------------------------------------------------------- // executor affinity tests - //---------------------------------------------------------- void testExecutorDefault() @@ -463,14 +460,14 @@ struct task_test { auto t = returns_int(); auto& p = t.handle().promise(); - BOOST_TEST(!p.ex); + BOOST_TEST(!p.get_executor()); } // task executor defaults to empty (no affinity) { auto t = void_task_basic(); auto& p = t.handle().promise(); - BOOST_TEST(!p.ex); + BOOST_TEST(!p.get_executor()); } } @@ -510,22 +507,19 @@ struct task_test // Verify that executor is used for void tasks sync_executor ctx; executor ex(ctx); - bool done = false; auto t = void_task_with_async_for_affinity_test(); t.on(ex); - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); - BOOST_TEST(done); // Work should have been posted through the executor BOOST_TEST_GE(ctx.submit_count.load(), 1); } - //---------------------------------------------------------- // on() method tests - //---------------------------------------------------------- void testOnSetsExecutor() @@ -538,7 +532,7 @@ struct task_test t.on(ex); // Executor should now be set - BOOST_TEST(static_cast(t.handle().promise().ex)); + BOOST_TEST(static_cast(t.handle().promise().get_executor())); BOOST_TEST_EQ(run_task(t), 124); // Work should have been posted through the executor BOOST_TEST_GE(ctx.submit_count.load(), 1); @@ -554,11 +548,10 @@ struct task_test auto t = void_task_with_async_for_affinity_test(); t.on(ex); - bool done = false; - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); - BOOST_TEST(done); // Work should have been posted through the executor BOOST_TEST_GE(ctx.submit_count.load(), 1); } @@ -592,11 +585,10 @@ struct task_test }; auto t = make_task().on(ex); - bool done = false; - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); - BOOST_TEST(done); BOOST_TEST_GE(ctx.submit_count.load(), 1); } @@ -612,9 +604,7 @@ struct task_test BOOST_TEST(&ref == &t); } - //---------------------------------------------------------- - // Affinity propagation tests (ABC problem) - //---------------------------------------------------------- + // Affinity propagation tests static task inner_task_c() @@ -683,11 +673,10 @@ struct task_test auto t = outer_void_task_a(); t.on(ex); - bool done = false; - t.handle().promise().on_done = [&done]{ done = true; }; - t.handle().resume(); + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); - BOOST_TEST(done); BOOST_TEST_GE(ctx.submit_count.load(), 1); } @@ -728,6 +717,525 @@ struct task_test // Task should complete without any executor } + // Affinity preservation tests + + /** Executor that tracks submissions with an ID. + */ + struct tracking_executor + { + friend struct executor::access; + + int id; + std::atomic submit_count{0}; + mutable std::vector* submission_log; + + explicit + tracking_executor(int id_, std::vector* log) + : id(id_) + , submission_log(log) + { + } + + private: + struct header + { + std::size_t size; + }; + + void* + allocate(std::size_t size, std::size_t /*align*/) + { + std::size_t total = sizeof(header) + size; + void* p = std::malloc(total); + auto* h = new(p) header{total}; + return h + 1; + } + + void + deallocate(void* p, std::size_t /*size*/, std::size_t /*align*/) + { + auto* h = static_cast(p) - 1; + std::free(h); + } + + void + submit(executor::work* w) + { + ++submit_count; + if (submission_log) + submission_log->push_back(id); + w->invoke(); + w->~work(); + deallocate(w, 0, 0); + } + }; + + static async_op + async_op_immediate(int value) + { + return make_async_op( + [value](auto cb) { + cb(value); + }); + } + + void + testInheritedAffinityVerification() + { + // Test that child tasks actually use inherited affinity + // by checking that all resumptions go through the parent's executor + std::vector log; + tracking_executor ctx(1, &log); + executor ex(ctx); + + // Chain: outer -> middle -> inner, only outer has .on() + auto inner = []() -> task { + co_return co_await async_op_immediate(100); + }; + + auto middle = [inner]() -> task { + int v = co_await inner(); + co_return v + co_await async_op_immediate(10); + }; + + auto outer = [middle]() -> task { + int v = co_await middle(); + co_return v + co_await async_op_immediate(1); + }; + + auto t = outer(); + t.on(ex); + + BOOST_TEST_EQ(run_task(t), 111); + // All three async_ops should have resumed through executor 1 + BOOST_TEST_GE(ctx.submit_count.load(), 3); + for (int id : log) + BOOST_TEST_EQ(id, 1); + } + + void + testCrossExecutorAsyncOp() + { + // Test: async_op "completes" but task resumes on its affinity executor + // This verifies the dispatcher is correctly used for resumption + std::vector log; + tracking_executor ctx1(1, &log); + tracking_executor ctx2(2, &log); + executor ex1(ctx1); + executor ex2(ctx2); + + // Create a task with affinity to ex1 + auto task_with_affinity = []() -> task { + // This async_op completes inline (simulating completion on "other" context) + int v = co_await async_op_immediate(42); + co_return v; + }; + + auto t = task_with_affinity(); + t.on(ex1); + + BOOST_TEST_EQ(run_task(t), 42); + // Resumption should go through ex1, not ex2 + BOOST_TEST_GE(ctx1.submit_count.load(), 1); + BOOST_TEST_EQ(ctx2.submit_count.load(), 0); + // All logged submissions should be to executor 1 + for (int id : log) + BOOST_TEST_EQ(id, 1); + } + + void + testMixedAffinityChain() + { + // Test: outer has ex1, inner explicitly has ex2 + // Verify each task uses its own affinity + std::vector outer_log; + std::vector inner_log; + tracking_executor ctx1(1, &outer_log); + tracking_executor ctx2(2, &inner_log); + executor ex1(ctx1); + executor ex2(ctx2); + + // Inner task with explicit affinity to ex2 + auto make_inner = [ex2]() -> task { + auto inner = []() -> task { + co_return co_await async_op_immediate(100); + }; + return inner().on(ex2); + }; + + // Outer task with affinity to ex1 + auto outer = [make_inner]() -> task { + int v = co_await make_inner(); + // This await should use ex1 (outer's affinity) + v += co_await async_op_immediate(1); + co_return v; + }; + + auto t = outer(); + t.on(ex1); + + BOOST_TEST_EQ(run_task(t), 101); + // Inner's async should use ex2 + BOOST_TEST_GE(ctx2.submit_count.load(), 1); + // Outer's async should use ex1 + BOOST_TEST_GE(ctx1.submit_count.load(), 1); + } + + void + testAffinityPreservedAcrossMultipleAwaits() + { + // Test that affinity is preserved across multiple co_await expressions + std::vector log; + tracking_executor ctx(1, &log); + executor ex(ctx); + + auto multi_await = []() -> task { + int sum = 0; + sum += co_await async_op_immediate(1); + sum += co_await async_op_immediate(2); + sum += co_await async_op_immediate(3); + sum += co_await async_op_immediate(4); + sum += co_await async_op_immediate(5); + co_return sum; + }; + + auto t = multi_await(); + t.on(ex); + + BOOST_TEST_EQ(run_task(t), 15); + // All 5 awaits should use the same executor + BOOST_TEST_EQ(ctx.submit_count.load(), 5); + BOOST_TEST_EQ(log.size(), 5u); + for (int id : log) + BOOST_TEST_EQ(id, 1); + } + + void + testAffinityWithNestedVoidTasks() + { + // Test affinity propagation through void task nesting + std::vector log; + tracking_executor ctx(1, &log); + executor ex(ctx); + + std::atomic counter{0}; + + auto leaf = [&counter]() -> task { + co_await async_op_immediate(0); + ++counter; + co_return; + }; + + auto branch = [leaf, &counter]() -> task { + co_await leaf(); + co_await async_op_immediate(0); + ++counter; + co_return; + }; + + auto root = [branch, &counter]() -> task { + co_await branch(); + co_await async_op_immediate(0); + ++counter; + co_return; + }; + + auto t = root(); + t.on(ex); + + while (!t.handle().done()) + t.handle().resume(); + t.await_resume(); + + BOOST_TEST_EQ(counter.load(), 3); + // All async_ops should dispatch through executor + BOOST_TEST_GE(ctx.submit_count.load(), 3); + for (int id : log) + BOOST_TEST_EQ(id, 1); + } + + void + testFinalSuspendUsesDispatcher() + { + // Test that when child task completes, it resumes parent via dispatcher + std::vector log; + tracking_executor ctx(1, &log); + executor ex(ctx); + + // Simple child that just returns a value + auto child = []() -> task { + co_return 42; + }; + + // Parent awaits child, then does work + auto parent = [child]() -> task { + int v = co_await child(); // child's final_suspend should use dispatcher + co_return v + 1; + }; + + auto t = parent(); + t.on(ex); + + BOOST_TEST_EQ(run_task(t), 43); + // Child's completion should dispatch through executor + BOOST_TEST_GE(ctx.submit_count.load(), 1); + } + + // spawn() tests + + void + testSpawnValueTask() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto compute = []() -> task { + co_return 42; + }; + + spawn(ex, compute(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_value()); + BOOST_TEST_EQ(*(*received), 42); + BOOST_TEST_GE(ctx.submit_count.load(), 1); + } + + void + testSpawnVoidTask() + { + sync_executor ctx; + executor ex(ctx); + bool task_done = false; + std::optional> received; + + auto do_work = [&task_done]() -> task { + task_done = true; + co_return; + }; + + spawn(ex, do_work(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_value()); + BOOST_TEST(task_done); + BOOST_TEST_GE(ctx.submit_count.load(), 1); + } + + void + testSpawnTaskWithException() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto throwing_task = []() -> task { + throw_test_exception("spawn test"); + co_return 0; + }; + + spawn(ex, throwing_task(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_error()); + bool caught = false; + try { std::rethrow_exception(received->error()); } + catch (test_exception const&) { caught = true; } + BOOST_TEST(caught); + } + + void + testSpawnVoidTaskWithException() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto throwing_void_task = []() -> task { + throw_test_exception("void spawn exception"); + co_return; + }; + + spawn(ex, throwing_void_task(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_error()); + bool caught = false; + try { std::rethrow_exception(received->error()); } + catch (test_exception const&) { caught = true; } + BOOST_TEST(caught); + } + + void + testSpawnWithNestedAwaits() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto inner = []() -> task { + co_return 10; + }; + + auto outer = [inner]() -> task { + int a = co_await inner(); + int b = co_await inner(); + co_return a + b; + }; + + spawn(ex, outer(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_value()); + BOOST_TEST_EQ(*(*received), 20); + } + + void + testSpawnWithAsyncOp() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto task_with_async = []() -> task { + int v = co_await async_op_immediate(100); + co_return v + 1; + }; + + spawn(ex, task_with_async(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_value()); + BOOST_TEST_EQ(*(*received), 101); + BOOST_TEST_GE(ctx.submit_count.load(), 1); + } + + void + testSpawnAffinityPropagation() + { + std::vector log; + tracking_executor ctx(1, &log); + executor ex(ctx); + std::optional> received; + + auto inner = []() -> task { + co_return co_await async_op_immediate(50); + }; + + auto outer = [inner]() -> task { + int v = co_await inner(); + v += co_await async_op_immediate(5); + co_return v; + }; + + spawn(ex, outer(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_value()); + BOOST_TEST_EQ(*(*received), 55); + BOOST_TEST_GE(ctx.submit_count.load(), 2); + for (int id : log) + BOOST_TEST_EQ(id, 1); + } + + void + testSpawnChained() + { + sync_executor ctx; + executor ex(ctx); + int sum = 0; + + auto task1 = []() -> task { co_return 1; }; + auto task2 = []() -> task { co_return 2; }; + auto task3 = []() -> task { co_return 3; }; + + spawn(ex, task1(), [&](auto r) { if (r) sum += *r; }); + spawn(ex, task2(), [&](auto r) { if (r) sum += *r; }); + spawn(ex, task3(), [&](auto r) { if (r) sum += *r; }); + + BOOST_TEST_EQ(sum, 6); + } + + void + testSpawnResultErrorAccess() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto failing = []() -> task { + throw std::runtime_error("specific error"); + co_return 0; + }; + + spawn(ex, failing(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(!received->has_value()); + BOOST_TEST(received->has_error()); + BOOST_TEST(received->error() != nullptr); + + bool caught = false; + try + { + std::rethrow_exception(received->error()); + } + catch (std::runtime_error const& e) + { + BOOST_TEST(std::string(e.what()) == "specific error"); + caught = true; + } + BOOST_TEST(caught); + } + + void + testSpawnDeeplyNested() + { + sync_executor ctx; + executor ex(ctx); + std::optional> received; + + auto level3 = []() -> task { + co_return co_await async_op_immediate(1); + }; + + auto level2 = [level3]() -> task { + int v = co_await level3(); + co_return v + co_await async_op_immediate(10); + }; + + auto level1 = [level2]() -> task { + int v = co_await level2(); + co_return v + co_await async_op_immediate(100); + }; + + spawn(ex, level1(), [&](auto result) { + received = result; + }); + + BOOST_TEST(received.has_value()); + BOOST_TEST(received->has_value()); + BOOST_TEST_EQ(*(*received), 111); + BOOST_TEST_GE(ctx.submit_count.load(), 3); + } + void run() { @@ -763,6 +1271,177 @@ struct task_test testAffinityPropagationVoid(); testExplicitAffinityOverridesInheritance(); testNoAffinityRunsInline(); + + // affinity preservation tests + testInheritedAffinityVerification(); + testCrossExecutorAsyncOp(); + testMixedAffinityChain(); + testAffinityPreservedAcrossMultipleAwaits(); + testAffinityWithNestedVoidTasks(); + testFinalSuspendUsesDispatcher(); + + // spawn() function tests + testSpawnValueTask(); + testSpawnVoidTask(); + testSpawnTaskWithException(); + testSpawnVoidTaskWithException(); + testSpawnWithNestedAwaits(); + testSpawnWithAsyncOp(); + testSpawnAffinityPropagation(); + testSpawnChained(); + testSpawnResultErrorAccess(); + testSpawnDeeplyNested(); + testGccUninitialized(); + } + + // GCC 12+ -Wmaybe-uninitialized false positive tests + // https://github.com/boostorg/variant2/issues/XXX + // These attempt to reproduce the warning without coroutines. + void + testGccUninitialized() + { + using result_void = system::result; + using result_string = system::result; + + // Test 1: Simple copy construction + { + result_void r1; + result_void r2(r1); + (void)r2; + } + + // Test 2: Copy assignment + { + result_void r1; + result_void r2; + r2 = r1; + (void)r2; + } + + // Test 3: std::optional assignment (matches spawn pattern) + { + std::optional opt; + opt = result_void{}; + (void)opt; + } + + // Test 4: Pass to function via copy + { + auto fn = [](result_void r) { (void)r; }; + fn(result_void{}); + } + + // Test 5: Lambda capture + optional (closest to spawn) + { + auto fn = [](result_void r) { + std::optional opt; + opt = r; + return opt.has_value(); + }; + (void)fn(result_void{}); + } + + // Test 6: Non-void result with string (triggers string warning) + { + result_string r1; + result_string r2(r1); + (void)r2; + } + + // Test 7: Assign exception to result holding value + { + result_string r1{"hello"}; + r1 = std::make_exception_ptr(std::runtime_error("test")); + (void)r1; + } + + // Test 8: Optional with string result + { + std::optional opt; + opt = result_string{}; + (void)opt; + } + +#ifdef BOOST_CAPY_HAS_CORO + // Minimal fire-and-forget coroutine for testing + struct fire_and_forget + { + struct promise_type + { + fire_and_forget get_return_object() { return {}; } + std::suspend_never initial_suspend() noexcept { return {}; } + std::suspend_never final_suspend() noexcept { return {}; } + void return_void() {} + void unhandled_exception() { std::terminate(); } + }; + }; + + // Test 9: Coroutine returning result (mimics spawn) + { + auto coro = []() -> fire_and_forget { + result_void r{}; + (void)r; + co_return; + }; + coro(); + } + + // Test 10: Coroutine with handler call (closest to actual spawn) + { + std::optional received; + auto handler = [&](result_void r) { + received = r; + }; + auto coro = [&]() -> fire_and_forget { + handler(result_void{}); + co_return; + }; + coro(); + (void)received; + } + + // Test 11: Coroutine with try/catch like spawn + { + std::optional received; + auto handler = [&](result_void r) { + received = r; + }; + auto coro = [&]() -> fire_and_forget { + try + { + handler(result_void{}); + } + catch (...) + { + handler(result_void{std::current_exception()}); + } + co_return; + }; + coro(); + (void)received; + } + + // Test 12: Coroutine with string result + { + std::optional received; + auto handler = [&](result_string r) { + received = r; + }; + auto coro = [&]() -> fire_and_forget { + try + { + handler(result_string{"test"}); + } + catch (...) + { + handler(result_string{std::current_exception()}); + } + co_return; + }; + coro(); + (void)received; + } +#endif } };