LCOV - code coverage report
Current view: top level - capy - quitter.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 95 95
Test Date: 2026-06-24 18:54:23 Functions: 97.6 % 127 124 3

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2026 Michael Vandeberg
       3                 : //
       4                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       5                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       6                 : //
       7                 : // Official repository: https://github.com/cppalliance/capy
       8                 : //
       9                 : 
      10                 : #ifndef BOOST_CAPY_QUITTER_HPP
      11                 : #define BOOST_CAPY_QUITTER_HPP
      12                 : 
      13                 : #include <boost/capy/detail/config.hpp>
      14                 : #include <boost/capy/detail/stop_requested_exception.hpp>
      15                 : #include <boost/capy/concept/executor.hpp>
      16                 : #include <boost/capy/concept/io_awaitable.hpp>
      17                 : #include <boost/capy/ex/io_awaitable_promise_base.hpp>
      18                 : #include <boost/capy/ex/io_env.hpp>
      19                 : #include <boost/capy/ex/frame_allocator.hpp>
      20                 : #include <boost/capy/detail/await_suspend_helper.hpp>
      21                 : 
      22                 : #include <exception>
      23                 : #include <optional>
      24                 : #include <type_traits>
      25                 : #include <utility>
      26                 : 
      27                 : /* Stop-aware coroutine task.
      28                 : 
      29                 :    quitter<T> is identical to task<T> except that when the stop token
      30                 :    is triggered, the coroutine body never sees the cancellation.  The
      31                 :    promise intercepts it on resume (in transform_awaiter::await_resume)
      32                 :    and throws a sentinel exception that unwinds through RAII destructors
      33                 :    to final_suspend.  The parent sees a "stopped" completion.
      34                 : 
      35                 :    See doc/quitter.md for the full design rationale. */
      36                 : 
      37                 : namespace boost {
      38                 : namespace capy {
      39                 : 
      40                 : namespace detail {
      41                 : 
      42                 : // Reuse the same return-value storage as task<T>.
      43                 : // task_return_base is defined in task.hpp, but quitter needs its own
      44                 : // copy to avoid a header dependency on task.hpp.
      45                 : template<typename T>
      46                 : struct quitter_return_base
      47                 : {
      48                 :     std::optional<T> result_;
      49                 : 
      50 HIT          11 :     void return_value(T value)
      51                 :     {
      52              11 :         result_ = std::move(value);
      53              11 :     }
      54                 : 
      55               5 :     T&& result() noexcept
      56                 :     {
      57               5 :         return std::move(*result_);
      58                 :     }
      59                 : };
      60                 : 
      61                 : template<>
      62                 : struct quitter_return_base<void>
      63                 : {
      64               2 :     void return_void()
      65                 :     {
      66               2 :     }
      67                 : };
      68                 : 
      69                 : } // namespace detail
      70                 : 
      71                 : /** Stop-aware lazy coroutine task satisfying @ref IoRunnable.
      72                 : 
      73                 :     When the stop token is triggered, the next `co_await` inside the
      74                 :     coroutine short-circuits: the body never sees the result and RAII
      75                 :     destructors run normally.  The parent observes a "stopped"
      76                 :     completion via @ref promise_type::stopped.
      77                 : 
      78                 :     Everything else — frame allocation, environment propagation,
      79                 :     symmetric transfer, move semantics — is identical to @ref task.
      80                 : 
      81                 :     @tparam T The result type.  Use `quitter<>` for `quitter<void>`.
      82                 : 
      83                 :     @see task, IoRunnable, IoAwaitable
      84                 : */
      85                 : template<typename T = void>
      86                 : struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
      87                 :     quitter
      88                 : {
      89                 :     /** The coroutine promise type for `quitter<T>`.
      90                 : 
      91                 :         This is the promise object the compiler associates with a
      92                 :         `quitter<T>` coroutine. It satisfies the coroutine promise
      93                 :         requirements and participates in the I/O awaitable protocol via
      94                 :         @ref io_awaitable_promise_base. Unlike @ref task::promise_type,
      95                 :         its `transform_awaitable` checks the stop token before each
      96                 :         awaited result reaches the body, throwing an internal sentinel
      97                 :         exception that unwinds to a "stopped" completion. It is part of
      98                 :         the coroutine machinery and is not intended to be used directly
      99                 :         by callers.
     100                 : 
     101                 :         Result storage and `return_value`/`return_void` are provided by
     102                 :         `detail::quitter_return_base<T>`.
     103                 : 
     104                 :         @see io_awaitable_promise_base, IoRunnable
     105                 :     */
     106                 :     struct promise_type
     107                 :         : io_awaitable_promise_base<promise_type>
     108                 :         , detail::quitter_return_base<T>
     109                 :     {
     110                 :     private:
     111                 :         friend quitter;
     112                 : 
     113                 :         enum class completion { running, value, exception, stopped };
     114                 : 
     115                 :         union { std::exception_ptr ep_; };
     116                 :         completion state_;
     117                 : 
     118                 :     public:
     119                 :         /// Construct the promise in the running state.
     120              33 :         promise_type() noexcept
     121              33 :             : state_(completion::running)
     122                 :         {
     123              33 :         }
     124                 : 
     125                 :         /// Destroy the promise, releasing any stored exception.
     126              33 :         ~promise_type()
     127                 :         {
     128              33 :             if(state_ == completion::exception ||
     129              29 :                state_ == completion::stopped)
     130              20 :                 ep_.~exception_ptr();
     131              33 :         }
     132                 : 
     133                 :         /// Return a non-null exception_ptr when the coroutine threw
     134                 :         /// or was stopped.  Stopped quitters report the sentinel
     135                 :         /// stop_requested_exception so that run_async routes to
     136                 :         /// the error handler instead of accessing a non-existent
     137                 :         /// result.
     138              26 :         std::exception_ptr exception() const noexcept
     139                 :         {
     140              26 :             if(state_ == completion::exception ||
     141              20 :                state_ == completion::stopped)
     142              20 :                 return ep_;
     143               6 :             return {};
     144                 :         }
     145                 : 
     146                 :         /// True when the coroutine was stopped via the stop token.
     147              12 :         bool stopped() const noexcept
     148                 :         {
     149              12 :             return state_ == completion::stopped;
     150                 :         }
     151                 : 
     152                 :         /** Return the owning `quitter` for this coroutine.
     153                 : 
     154                 :             Called by the compiler to produce the object returned to the
     155                 :             caller when the coroutine is created.
     156                 : 
     157                 :             @return A `quitter` owning the coroutine frame.
     158                 :         */
     159              33 :         quitter get_return_object()
     160                 :         {
     161                 :             return quitter{
     162              33 :                 std::coroutine_handle<promise_type>::from_promise(*this)};
     163                 :         }
     164                 : 
     165                 :         /** Return the initial-suspend awaiter.
     166                 : 
     167                 :             The coroutine always suspends at the initial suspend point,
     168                 :             so the body does not start until the quitter is awaited. When
     169                 :             the body is resumed, the awaiter restores the thread-local
     170                 :             frame allocator and, if stop has already been requested,
     171                 :             throws the internal sentinel exception so the body never
     172                 :             runs and the coroutine completes as stopped.
     173                 : 
     174                 :             @return An awaiter that suspends unconditionally.
     175                 :         */
     176              33 :         auto initial_suspend() noexcept
     177                 :         {
     178                 :             struct awaiter
     179                 :             {
     180                 :                 promise_type* p_;
     181                 : 
     182              33 :                 bool await_ready() const noexcept
     183                 :                 {
     184              33 :                     return false;
     185                 :                 }
     186                 : 
     187              33 :                 void await_suspend(std::coroutine_handle<>) const noexcept
     188                 :                 {
     189              33 :                 }
     190                 : 
     191                 :                 // Potentially-throwing: checks the stop token before
     192                 :                 // the coroutine body executes its first statement.
     193              33 :                 void await_resume() const
     194                 :                 {
     195              33 :                     set_current_frame_allocator(
     196              33 :                         p_->environment()->frame_allocator);
     197              33 :                     if(p_->environment()->stop_token.stop_requested())
     198               3 :                         throw detail::stop_requested_exception{};
     199              30 :                 }
     200                 :             };
     201              33 :             return awaiter{this};
     202                 :         }
     203                 : 
     204                 :         /** Return the final-suspend awaiter.
     205                 : 
     206                 :             The coroutine always suspends at the final suspend point. The
     207                 :             awaiter's `await_suspend` performs symmetric transfer to the
     208                 :             stored continuation, resuming the awaiting coroutine.
     209                 : 
     210                 :             @return An awaiter that suspends and transfers to the
     211                 :             continuation.
     212                 :         */
     213              33 :         auto final_suspend() noexcept
     214                 :         {
     215                 :             struct awaiter
     216                 :             {
     217                 :                 promise_type* p_;
     218                 : 
     219              33 :                 bool await_ready() const noexcept
     220                 :                 {
     221              33 :                     return false;
     222                 :                 }
     223                 : 
     224              33 :                 std::coroutine_handle<> await_suspend(
     225                 :                     std::coroutine_handle<>) const noexcept
     226                 :                 {
     227              33 :                     return p_->continuation();
     228                 :                 }
     229                 : 
     230                 :                 void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
     231                 :             };
     232              33 :             return awaiter{this};
     233                 :         }
     234                 : 
     235                 :         /** Capture the in-flight exception from the coroutine body.
     236                 : 
     237                 :             Called by the compiler when the coroutine body exits via an
     238                 :             unhandled exception. The internal stop sentinel is recorded as
     239                 :             a stopped completion; any other exception is recorded as an
     240                 :             exception completion. The stored exception is surfaced (or
     241                 :             routed to the error handler) when the quitter is awaited or run.
     242                 :         */
     243              20 :         void unhandled_exception()
     244                 :         {
     245                 :             try
     246                 :             {
     247              20 :                 throw;
     248                 :             }
     249              20 :             catch(detail::stop_requested_exception const&)
     250                 :             {
     251                 :                 // Store the exception_ptr so that run_async's
     252                 :                 // invoke_impl routes to the error handler
     253                 :                 // instead of accessing a non-existent result.
     254              16 :                 new (&ep_) std::exception_ptr(
     255                 :                     std::current_exception());
     256              16 :                 state_ = completion::stopped;
     257                 :             }
     258               4 :             catch(...)
     259                 :             {
     260               4 :                 new (&ep_) std::exception_ptr(
     261                 :                     std::current_exception());
     262               4 :                 state_ = completion::exception;
     263                 :             }
     264              20 :         }
     265                 : 
     266                 :         //------------------------------------------------------
     267                 :         // transform_awaitable — the key difference from task<T>
     268                 :         //------------------------------------------------------
     269                 : 
     270                 :         /** Awaiter wrapping a nested `co_await` of an @ref IoAwaitable.
     271                 : 
     272                 :             Forwards the environment to the inner awaitable's
     273                 :             environment-taking `await_suspend` and restores the
     274                 :             thread-local frame allocator before the body resumes. Unlike
     275                 :             `task`'s, it also checks the stop token on resumption, throwing
     276                 :             the internal sentinel so a stop request unwinds the body before
     277                 :             it observes the I/O result.
     278                 : 
     279                 :             @tparam Awaitable The awaitable being transformed.
     280                 :         */
     281                 :         template<class Awaitable>
     282                 :         struct transform_awaiter
     283                 :         {
     284                 :             std::decay_t<Awaitable> a_;
     285                 :             promise_type* p_;
     286                 : 
     287              21 :             bool await_ready() noexcept
     288                 :             {
     289              21 :                 return a_.await_ready();
     290                 :             }
     291                 : 
     292                 :             // Check the stop token BEFORE the coroutine body
     293                 :             // sees the result of the I/O operation.
     294              21 :             decltype(auto) await_resume()
     295                 :             {
     296              21 :                 set_current_frame_allocator(
     297              21 :                     p_->environment()->frame_allocator);
     298              21 :                 if(p_->environment()->stop_token.stop_requested())
     299              13 :                     throw detail::stop_requested_exception{};
     300               8 :                 return a_.await_resume();
     301                 :             }
     302                 : 
     303                 :             template<class Promise>
     304              19 :             auto await_suspend(
     305                 :                 std::coroutine_handle<Promise> h) noexcept
     306                 :             {
     307                 :                 using R = decltype(
     308                 :                     a_.await_suspend(h, p_->environment()));
     309                 :                 if constexpr (std::is_same_v<
     310                 :                     R, std::coroutine_handle<>>)
     311              18 :                     return detail::symmetric_transfer(
     312              36 :                         a_.await_suspend(h, p_->environment()));
     313                 :                 else
     314               1 :                     return a_.await_suspend(
     315               2 :                         h, p_->environment());
     316                 :             }
     317                 :         };
     318                 : 
     319                 :         /** Transform a nested awaitable before `co_await`.
     320                 : 
     321                 :             Wraps an @ref IoAwaitable in a @ref transform_awaiter so the
     322                 :             coroutine's environment is propagated into it and the stop
     323                 :             token is checked on resumption. A diagnostic is emitted if the
     324                 :             awaitable does not satisfy @ref IoAwaitable.
     325                 : 
     326                 :             @param a The awaitable expression from `co_await a`.
     327                 : 
     328                 :             @return A @ref transform_awaiter wrapping `a`.
     329                 :         */
     330                 :         template<class Awaitable>
     331              21 :         auto transform_awaitable(Awaitable&& a)
     332                 :         {
     333                 :             using A = std::decay_t<Awaitable>;
     334                 :             if constexpr (IoAwaitable<A>)
     335                 :             {
     336                 :                 return transform_awaiter<Awaitable>{
     337              38 :                     std::forward<Awaitable>(a), this};
     338                 :             }
     339                 :             else
     340                 :             {
     341                 :                 static_assert(sizeof(A) == 0,
     342                 :                     "requires IoAwaitable");
     343                 :             }
     344              17 :         }
     345                 :     };
     346                 : 
     347                 :     /** Handle to the owned coroutine frame.
     348                 : 
     349                 :         Null when the quitter is empty (for example after a move or after
     350                 :         @ref release). Prefer @ref handle to read this; the member is
     351                 :         public for use by the coroutine machinery.
     352                 :     */
     353                 :     std::coroutine_handle<promise_type> h_;
     354                 : 
     355                 :     /// Destroy the quitter and its coroutine frame if owned.
     356              82 :     ~quitter()
     357                 :     {
     358              82 :         if(h_)
     359              15 :             h_.destroy();
     360              82 :     }
     361                 : 
     362                 :     /// Return false; quitters are never immediately ready.
     363              15 :     bool await_ready() const noexcept
     364                 :     {
     365              15 :         return false;
     366                 :     }
     367                 : 
     368                 :     /** Return the result, rethrow exception, or propagate stop.
     369                 : 
     370                 :         When stopped, throws stop_requested_exception so that a
     371                 :         parent quitter also stops.  A parent task<T> will see this
     372                 :         as an unhandled exception — by design.
     373                 :     */
     374              12 :     auto await_resume()
     375                 :     {
     376              12 :         if(h_.promise().stopped())
     377               6 :             throw detail::stop_requested_exception{};
     378               6 :         if(h_.promise().state_ == promise_type::completion::exception)
     379               1 :             std::rethrow_exception(h_.promise().ep_);
     380                 :         if constexpr (! std::is_void_v<T>)
     381               4 :             return std::move(*h_.promise().result_);
     382                 :         else
     383               1 :             return;
     384                 :     }
     385                 : 
     386                 :     /// Start execution with the caller's context.
     387              15 :     std::coroutine_handle<> await_suspend(
     388                 :         std::coroutine_handle<> cont,
     389                 :         io_env const* env)
     390                 :     {
     391              15 :         h_.promise().set_continuation(cont);
     392              15 :         h_.promise().set_environment(env);
     393              15 :         return h_;
     394                 :     }
     395                 : 
     396                 :     /** Return the coroutine handle.
     397                 : 
     398                 :         @note Do not call `destroy()` on the returned handle while
     399                 :         the quitter is being awaited. The quitter's lifetime is
     400                 :         normally managed by `run_async`, `run`, or the awaiting
     401                 :         parent; manually destroying a suspended quitter that another
     402                 :         coroutine is awaiting produces undefined behavior. For
     403                 :         cooperative cancellation, use `std::stop_token`.
     404                 : 
     405                 :         @return The coroutine handle.
     406                 :     */
     407              20 :     std::coroutine_handle<promise_type> handle() const noexcept
     408                 :     {
     409              20 :         return h_;
     410                 :     }
     411                 : 
     412                 :     /** Release ownership of the coroutine frame.
     413                 : 
     414                 :         @note If the caller intends to call `destroy()` on the
     415                 :         released handle, it must do so only when the quitter has not
     416                 :         started or has fully completed. Destroying a suspended
     417                 :         quitter that is being awaited produces undefined behavior.
     418                 :     */
     419              18 :     void release() noexcept
     420                 :     {
     421              18 :         h_ = nullptr;
     422              18 :     }
     423                 : 
     424                 :     quitter(quitter const&) = delete;
     425                 :     quitter& operator=(quitter const&) = delete;
     426                 : 
     427                 :     /// Construct by moving, transferring ownership.
     428              49 :     quitter(quitter&& other) noexcept
     429              49 :         : h_(std::exchange(other.h_, nullptr))
     430                 :     {
     431              49 :     }
     432                 : 
     433                 :     /// Assign by moving, transferring ownership.
     434                 :     quitter& operator=(quitter&& other) noexcept
     435                 :     {
     436                 :         if(this != &other)
     437                 :         {
     438                 :             if(h_)
     439                 :                 h_.destroy();
     440                 :             h_ = std::exchange(other.h_, nullptr);
     441                 :         }
     442                 :         return *this;
     443                 :     }
     444                 : 
     445                 : private:
     446              33 :     explicit quitter(std::coroutine_handle<promise_type> h)
     447              33 :         : h_(h)
     448                 :     {
     449              33 :     }
     450                 : };
     451                 : 
     452                 : } // namespace capy
     453                 : } // namespace boost
     454                 : 
     455                 : #endif
        

Generated by: LCOV version 2.3