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
|