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
|