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