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-09 22:00:30 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                 :     struct promise_type
      90                 :         : io_awaitable_promise_base<promise_type>
      91                 :         , detail::quitter_return_base<T>
      92                 :     {
      93                 :     private:
      94                 :         friend quitter;
      95                 : 
      96                 :         enum class completion { running, value, exception, stopped };
      97                 : 
      98                 :         union { std::exception_ptr ep_; };
      99                 :         completion state_;
     100                 : 
     101                 :     public:
     102              33 :         promise_type() noexcept
     103              33 :             : state_(completion::running)
     104                 :         {
     105              33 :         }
     106                 : 
     107              33 :         ~promise_type()
     108                 :         {
     109              33 :             if(state_ == completion::exception ||
     110              29 :                state_ == completion::stopped)
     111              20 :                 ep_.~exception_ptr();
     112              33 :         }
     113                 : 
     114                 :         /// Return a non-null exception_ptr when the coroutine threw
     115                 :         /// or was stopped.  Stopped quitters report the sentinel
     116                 :         /// stop_requested_exception so that run_async routes to
     117                 :         /// the error handler instead of accessing a non-existent
     118                 :         /// result.
     119              26 :         std::exception_ptr exception() const noexcept
     120                 :         {
     121              26 :             if(state_ == completion::exception ||
     122              20 :                state_ == completion::stopped)
     123              20 :                 return ep_;
     124               6 :             return {};
     125                 :         }
     126                 : 
     127                 :         /// True when the coroutine was stopped via the stop token.
     128              12 :         bool stopped() const noexcept
     129                 :         {
     130              12 :             return state_ == completion::stopped;
     131                 :         }
     132                 : 
     133              33 :         quitter get_return_object()
     134                 :         {
     135                 :             return quitter{
     136              33 :                 std::coroutine_handle<promise_type>::from_promise(*this)};
     137                 :         }
     138                 : 
     139              33 :         auto initial_suspend() noexcept
     140                 :         {
     141                 :             struct awaiter
     142                 :             {
     143                 :                 promise_type* p_;
     144                 : 
     145              33 :                 bool await_ready() const noexcept
     146                 :                 {
     147              33 :                     return false;
     148                 :                 }
     149                 : 
     150              33 :                 void await_suspend(std::coroutine_handle<>) const noexcept
     151                 :                 {
     152              33 :                 }
     153                 : 
     154                 :                 // Potentially-throwing: checks the stop token before
     155                 :                 // the coroutine body executes its first statement.
     156              33 :                 void await_resume() const
     157                 :                 {
     158              33 :                     set_current_frame_allocator(
     159              33 :                         p_->environment()->frame_allocator);
     160              33 :                     if(p_->environment()->stop_token.stop_requested())
     161               3 :                         throw detail::stop_requested_exception{};
     162              30 :                 }
     163                 :             };
     164              33 :             return awaiter{this};
     165                 :         }
     166                 : 
     167              33 :         auto final_suspend() noexcept
     168                 :         {
     169                 :             struct awaiter
     170                 :             {
     171                 :                 promise_type* p_;
     172                 : 
     173              33 :                 bool await_ready() const noexcept
     174                 :                 {
     175              33 :                     return false;
     176                 :                 }
     177                 : 
     178              33 :                 std::coroutine_handle<> await_suspend(
     179                 :                     std::coroutine_handle<>) const noexcept
     180                 :                 {
     181              33 :                     return p_->continuation();
     182                 :                 }
     183                 : 
     184                 :                 void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
     185                 :             };
     186              33 :             return awaiter{this};
     187                 :         }
     188                 : 
     189              20 :         void unhandled_exception()
     190                 :         {
     191                 :             try
     192                 :             {
     193              20 :                 throw;
     194                 :             }
     195              20 :             catch(detail::stop_requested_exception const&)
     196                 :             {
     197                 :                 // Store the exception_ptr so that run_async's
     198                 :                 // invoke_impl routes to the error handler
     199                 :                 // instead of accessing a non-existent result.
     200              16 :                 new (&ep_) std::exception_ptr(
     201                 :                     std::current_exception());
     202              16 :                 state_ = completion::stopped;
     203                 :             }
     204               4 :             catch(...)
     205                 :             {
     206               4 :                 new (&ep_) std::exception_ptr(
     207                 :                     std::current_exception());
     208               4 :                 state_ = completion::exception;
     209                 :             }
     210              20 :         }
     211                 : 
     212                 :         //------------------------------------------------------
     213                 :         // transform_awaitable — the key difference from task<T>
     214                 :         //------------------------------------------------------
     215                 : 
     216                 :         template<class Awaitable>
     217                 :         struct transform_awaiter
     218                 :         {
     219                 :             std::decay_t<Awaitable> a_;
     220                 :             promise_type* p_;
     221                 : 
     222              21 :             bool await_ready() noexcept
     223                 :             {
     224              21 :                 return a_.await_ready();
     225                 :             }
     226                 : 
     227                 :             // Check the stop token BEFORE the coroutine body
     228                 :             // sees the result of the I/O operation.
     229              21 :             decltype(auto) await_resume()
     230                 :             {
     231              21 :                 set_current_frame_allocator(
     232              21 :                     p_->environment()->frame_allocator);
     233              21 :                 if(p_->environment()->stop_token.stop_requested())
     234              13 :                     throw detail::stop_requested_exception{};
     235               8 :                 return a_.await_resume();
     236                 :             }
     237                 : 
     238                 :             template<class Promise>
     239              19 :             auto await_suspend(
     240                 :                 std::coroutine_handle<Promise> h) noexcept
     241                 :             {
     242                 :                 using R = decltype(
     243                 :                     a_.await_suspend(h, p_->environment()));
     244                 :                 if constexpr (std::is_same_v<
     245                 :                     R, std::coroutine_handle<>>)
     246              18 :                     return detail::symmetric_transfer(
     247              36 :                         a_.await_suspend(h, p_->environment()));
     248                 :                 else
     249               1 :                     return a_.await_suspend(
     250               2 :                         h, p_->environment());
     251                 :             }
     252                 :         };
     253                 : 
     254                 :         template<class Awaitable>
     255              21 :         auto transform_awaitable(Awaitable&& a)
     256                 :         {
     257                 :             using A = std::decay_t<Awaitable>;
     258                 :             if constexpr (IoAwaitable<A>)
     259                 :             {
     260                 :                 return transform_awaiter<Awaitable>{
     261              38 :                     std::forward<Awaitable>(a), this};
     262                 :             }
     263                 :             else
     264                 :             {
     265                 :                 static_assert(sizeof(A) == 0,
     266                 :                     "requires IoAwaitable");
     267                 :             }
     268              17 :         }
     269                 :     };
     270                 : 
     271                 :     std::coroutine_handle<promise_type> h_;
     272                 : 
     273                 :     /// Destroy the quitter and its coroutine frame if owned.
     274              82 :     ~quitter()
     275                 :     {
     276              82 :         if(h_)
     277              15 :             h_.destroy();
     278              82 :     }
     279                 : 
     280                 :     /// Return false; quitters are never immediately ready.
     281              15 :     bool await_ready() const noexcept
     282                 :     {
     283              15 :         return false;
     284                 :     }
     285                 : 
     286                 :     /** Return the result, rethrow exception, or propagate stop.
     287                 : 
     288                 :         When stopped, throws stop_requested_exception so that a
     289                 :         parent quitter also stops.  A parent task<T> will see this
     290                 :         as an unhandled exception — by design.
     291                 :     */
     292              12 :     auto await_resume()
     293                 :     {
     294              12 :         if(h_.promise().stopped())
     295               6 :             throw detail::stop_requested_exception{};
     296               6 :         if(h_.promise().state_ == promise_type::completion::exception)
     297               1 :             std::rethrow_exception(h_.promise().ep_);
     298                 :         if constexpr (! std::is_void_v<T>)
     299               4 :             return std::move(*h_.promise().result_);
     300                 :         else
     301               1 :             return;
     302                 :     }
     303                 : 
     304                 :     /// Start execution with the caller's context.
     305              15 :     std::coroutine_handle<> await_suspend(
     306                 :         std::coroutine_handle<> cont,
     307                 :         io_env const* env)
     308                 :     {
     309              15 :         h_.promise().set_continuation(cont);
     310              15 :         h_.promise().set_environment(env);
     311              15 :         return h_;
     312                 :     }
     313                 : 
     314                 :     /** Return the coroutine handle.
     315                 : 
     316                 :         @note Do not call `destroy()` on the returned handle while
     317                 :         the quitter is being awaited. The quitter's lifetime is
     318                 :         normally managed by `run_async`, `run`, or the awaiting
     319                 :         parent; manually destroying a suspended quitter that another
     320                 :         coroutine is awaiting produces undefined behavior. For
     321                 :         cooperative cancellation, use `std::stop_token`.
     322                 : 
     323                 :         @return The coroutine handle.
     324                 :     */
     325              20 :     std::coroutine_handle<promise_type> handle() const noexcept
     326                 :     {
     327              20 :         return h_;
     328                 :     }
     329                 : 
     330                 :     /** Release ownership of the coroutine frame.
     331                 : 
     332                 :         @note If the caller intends to call `destroy()` on the
     333                 :         released handle, it must do so only when the quitter has not
     334                 :         started or has fully completed. Destroying a suspended
     335                 :         quitter that is being awaited produces undefined behavior.
     336                 :     */
     337              18 :     void release() noexcept
     338                 :     {
     339              18 :         h_ = nullptr;
     340              18 :     }
     341                 : 
     342                 :     quitter(quitter const&) = delete;
     343                 :     quitter& operator=(quitter const&) = delete;
     344                 : 
     345                 :     /// Construct by moving, transferring ownership.
     346              49 :     quitter(quitter&& other) noexcept
     347              49 :         : h_(std::exchange(other.h_, nullptr))
     348                 :     {
     349              49 :     }
     350                 : 
     351                 :     /// Assign by moving, transferring ownership.
     352                 :     quitter& operator=(quitter&& other) noexcept
     353                 :     {
     354                 :         if(this != &other)
     355                 :         {
     356                 :             if(h_)
     357                 :                 h_.destroy();
     358                 :             h_ = std::exchange(other.h_, nullptr);
     359                 :         }
     360                 :         return *this;
     361                 :     }
     362                 : 
     363                 : private:
     364              33 :     explicit quitter(std::coroutine_handle<promise_type> h)
     365              33 :         : h_(h)
     366                 :     {
     367              33 :     }
     368                 : };
     369                 : 
     370                 : } // namespace capy
     371                 : } // namespace boost
     372                 : 
     373                 : #endif
        

Generated by: LCOV version 2.3