TLA Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 : // Copyright (c) 2026 Michael Vandeberg
4 : //
5 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 : //
8 : // Official repository: https://github.com/cppalliance/capy
9 : //
10 :
11 : #ifndef BOOST_CAPY_TASK_HPP
12 : #define BOOST_CAPY_TASK_HPP
13 :
14 : #include <boost/capy/detail/config.hpp>
15 : #include <boost/capy/concept/executor.hpp>
16 : #include <boost/capy/concept/context_awaitable.hpp>
17 : #include <boost/capy/ex/context_awaitable_promise_base.hpp>
18 : #include <boost/capy/ex/await_context.hpp>
19 : #include <boost/capy/ex/frame_allocator.hpp>
20 : #include <boost/capy/detail/await_suspend_helper.hpp>
21 : #include <boost/capy/results.hpp>
22 :
23 : #include <exception>
24 : #include <optional>
25 : #include <type_traits>
26 : #include <utility>
27 : #include <variant>
28 :
29 : namespace boost {
30 : namespace capy {
31 :
32 : namespace detail {
33 :
34 : // Helper base for result storage and return_void/return_value
35 : template<typename T>
36 : struct task_return_base
37 : {
38 : std::optional<T> result_;
39 :
40 HIT 899 : void return_value(T value)
41 : {
42 899 : result_ = std::move(value);
43 899 : }
44 :
45 284 : T&& result() noexcept
46 : {
47 284 : return std::move(*result_);
48 : }
49 : };
50 :
51 : template<>
52 : struct task_return_base<void>
53 : {
54 4165 : void return_void()
55 : {
56 4165 : }
57 : };
58 :
59 : } // namespace detail
60 :
61 : /** Defers a coroutine body until awaited, then runs it inline on the caller's thread.
62 :
63 : Use `task<T>` as the return type for coroutines that perform I/O
64 : and return a value of type `T`. The coroutine body does not start
65 : executing until the task is awaited, enabling efficient composition
66 : without unnecessary eager execution.
67 :
68 : The task participates in the I/O awaitable protocol: when awaited,
69 : it receives the caller's executor and stop token, propagating them
70 : to nested `co_await` expressions. This enables cancellation and
71 : proper completion dispatch across executor boundaries.
72 :
73 : @par Await-effects
74 :
75 : Let `t` be a `task<T>`. `co_await t` always suspends the awaiting
76 : coroutine, then transfers control directly into the task's coroutine
77 : body on the current thread; no executor operation is posted. The task
78 : records the caller's environment (executor, stop token, and frame
79 : allocator) by pointer rather than copying it. It propagates that
80 : environment to every `co_await` inside the body.
81 :
82 : The body runs until it returns or exits via an exception. Control
83 : then transfers directly back to the awaiting coroutine, again
84 : without an executor operation.
85 :
86 : `task` never inspects the stop token; it only propagates it. A task
87 : body observes a stop request through the results of the operations it
88 : awaits, or by reading the token itself.
89 :
90 : @par Await-returns
91 : The value the body passed to `co_return`, moved out of the task, or
92 : nothing when `T` is `void`.
93 :
94 : If the body exits via an unhandled exception, that exception is
95 : rethrown instead.
96 :
97 : @par Await-postcondition
98 : The task's coroutine has run to completion and is suspended at its
99 : final suspend point. The task still owns the frame, but not the
100 : result: the await moves it out, so a task must not be awaited twice.
101 :
102 : @par Thread Safety
103 : Distinct objects: Safe.
104 : Shared objects: Unsafe.
105 :
106 : @par Example
107 :
108 : @par !example example
109 :
110 :
111 : @tparam T The result type. Use `task<>` for `task<void>`.
112 :
113 : @see @ref ContextRunnable, @ref ContextAwaitable, @ref run, @ref run_async
114 : */
115 : template<typename T = void>
116 : struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
117 : task
118 : {
119 : /** Stores `task<T>`'s result and joins the I/O awaitable protocol via `context_awaitable_promise_base`.
120 :
121 : This is the promise object the compiler associates with a
122 : `task<T>` coroutine. It satisfies the coroutine promise
123 : requirements and participates in the I/O awaitable protocol via
124 : @ref context_awaitable_promise_base. It is part of the coroutine
125 : machinery and is not intended to be used directly by callers.
126 :
127 : Result storage and `return_value`/`return_void` are provided by
128 : `detail::task_return_base<T>`.
129 :
130 : @see @ref context_awaitable_promise_base, @ref ContextRunnable
131 : */
132 : struct promise_type
133 : : context_awaitable_promise_base<promise_type>
134 : , detail::task_return_base<T>
135 : {
136 : private:
137 : friend task;
138 : union { std::exception_ptr ep_; };
139 : bool has_ep_;
140 :
141 : public:
142 : /// Construct the promise with no stored exception.
143 109249 : promise_type() noexcept
144 109249 : : has_ep_(false)
145 : {
146 109249 : }
147 :
148 : /// Destroy the promise, releasing any stored exception.
149 109249 : ~promise_type()
150 : {
151 109249 : if(has_ep_)
152 485 : ep_.~exception_ptr();
153 109249 : }
154 :
155 : /** Return the exception captured by the coroutine body, if any.
156 :
157 : @return The stored exception, or a null `std::exception_ptr`
158 : if the coroutine did not exit via an unhandled exception.
159 : */
160 5040 : std::exception_ptr exception() const noexcept
161 : {
162 5040 : if(has_ep_)
163 726 : return ep_;
164 4314 : return {};
165 : }
166 :
167 : /** Return the owning `task` for this coroutine.
168 :
169 : Called by the compiler to produce the object returned to the
170 : caller when the coroutine is created.
171 :
172 : @return A `task` owning the coroutine frame.
173 : */
174 109249 : task get_return_object()
175 : {
176 109249 : return task{std::coroutine_handle<promise_type>::from_promise(*this)};
177 : }
178 :
179 : /** Return the initial-suspend awaiter.
180 :
181 : The coroutine always suspends at the initial suspend point,
182 : so the body does not start until the task is awaited. When the
183 : body is resumed, the awaiter restores the thread-local frame
184 : allocator from the stored environment.
185 :
186 : @return An awaiter that suspends unconditionally.
187 : */
188 109249 : auto initial_suspend() noexcept
189 : {
190 : struct awaiter
191 : {
192 : promise_type* p_;
193 :
194 109249 : bool await_ready() const noexcept
195 : {
196 109249 : return false;
197 : }
198 :
199 109249 : void await_suspend(std::coroutine_handle<>) const noexcept
200 : {
201 109249 : }
202 :
203 108166 : void await_resume() const noexcept
204 : {
205 : // Restore TLS when body starts executing
206 108166 : set_current_frame_allocator(p_->environment()->frame_allocator);
207 108166 : }
208 : };
209 109249 : return awaiter{this};
210 : }
211 :
212 : /** Return the final-suspend awaiter.
213 :
214 : The coroutine always suspends at the final suspend point. The
215 : awaiter's `await_suspend` performs symmetric transfer to the
216 : stored continuation (consuming it), resuming the awaiting
217 : coroutine.
218 :
219 : @return An awaiter that suspends and transfers to the
220 : continuation.
221 : */
222 5549 : auto final_suspend() noexcept
223 : {
224 : struct awaiter
225 : {
226 : promise_type* p_;
227 :
228 5549 : bool await_ready() const noexcept
229 : {
230 5549 : return false;
231 : }
232 :
233 5549 : std::coroutine_handle<> await_suspend(std::coroutine_handle<>) const noexcept
234 : {
235 5549 : return p_->continuation();
236 : }
237 :
238 : void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
239 : };
240 5549 : return awaiter{this};
241 : }
242 :
243 : /** Capture the in-flight exception from the coroutine body.
244 :
245 : Called by the compiler when the coroutine body exits via an
246 : unhandled exception. The captured exception is rethrown when
247 : the task is awaited.
248 : */
249 485 : void unhandled_exception() noexcept
250 : {
251 485 : new (&ep_) std::exception_ptr(std::current_exception());
252 485 : has_ep_ = true;
253 485 : }
254 :
255 : /** Awaiter wrapping a nested `co_await` of an @ref ContextAwaitable.
256 :
257 : Forwards the environment to the inner awaitable's
258 : environment-taking `await_suspend` and restores the
259 : thread-local frame allocator before the body resumes.
260 :
261 : @tparam Awaitable The awaitable being transformed.
262 : */
263 : template<class Awaitable>
264 : struct transform_awaiter
265 : {
266 : /// The wrapped awaitable, decayed and stored by value.
267 : std::decay_t<Awaitable> a_;
268 :
269 : /// The promise of the coroutine performing the `co_await`.
270 : promise_type* p_;
271 :
272 : /** Report whether the wrapped awaitable is already complete.
273 :
274 : Where `BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND` is on,
275 : this always returns `false`, and @ref await_suspend asks
276 : the wrapped awaitable instead. See
277 : `detail::await_ready_then_suspend`.
278 :
279 : @return The wrapped awaitable's own `await_ready` result:
280 : `true` if no suspension is needed. Always `false` where
281 : the workaround is on.
282 : */
283 108232 : bool await_ready() noexcept
284 : {
285 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
286 : return false;
287 : #else
288 108232 : return a_.await_ready();
289 : #endif
290 : }
291 :
292 : /** Restore the frame allocator, then resume the wrapped
293 : awaitable.
294 :
295 : Reinstalls the thread-local frame allocator from the stored
296 : environment before the body continues. This is needed
297 : because the resumption may arrive on a different thread
298 : than the one that suspended.
299 :
300 : @return The wrapped awaitable's await-result, forwarded
301 : unchanged.
302 : */
303 5615 : decltype(auto) await_resume()
304 : {
305 : // Restore TLS before body resumes
306 5615 : set_current_frame_allocator(p_->environment()->frame_allocator);
307 5615 : return a_.await_resume();
308 : }
309 :
310 : /** Suspend by calling the wrapped awaitable with the
311 : environment.
312 :
313 : This is the plain `await_suspend` the compiler calls for the
314 : nested `co_await`. It forwards to the wrapped awaitable's
315 : @ref ContextAwaitable overload, supplying the promise's stored
316 : environment as the second argument. It then hands back
317 : that call's result unchanged, so the wrapped awaitable's
318 : suspension decision, whatever form it takes, is preserved.
319 :
320 : @param h The coroutine performing the `co_await`.
321 :
322 : @return Whatever the wrapped awaitable's `await_suspend`
323 : returns. When that is a `std::coroutine_handle<>`, the
324 : handle is routed through `detail::symmetric_transfer`.
325 : Where `BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND` is on,
326 : this function instead returns `bool` from
327 : `detail::await_ready_then_suspend`. It first asks the
328 : wrapped awaitable's `await_ready`. When that reports
329 : ready, it returns `false` to continue without
330 : suspending. Otherwise it resumes any returned handle on
331 : the current stack and returns `true`. Where the
332 : workaround is off, a returned handle is passed back
333 : unchanged for symmetric transfer.
334 : */
335 : template<class Promise>
336 107616 : auto await_suspend(std::coroutine_handle<Promise> h) noexcept
337 : {
338 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
339 : return detail::await_ready_then_suspend(a_, h, p_->environment());
340 : #else
341 : using R = decltype(a_.await_suspend(h, p_->environment()));
342 : if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
343 106578 : return detail::symmetric_transfer(a_.await_suspend(h, p_->environment()));
344 : else
345 1038 : return a_.await_suspend(h, p_->environment());
346 : #endif
347 : }
348 : };
349 :
350 : /** Transform a nested awaitable before `co_await`.
351 :
352 : Wraps an @ref ContextAwaitable in a @ref transform_awaiter so the
353 : coroutine's environment is propagated into it. A diagnostic
354 : is emitted if the awaitable does not satisfy @ref ContextAwaitable.
355 :
356 : @param a The awaitable expression from `co_await a`.
357 :
358 : @return A @ref transform_awaiter wrapping `a`.
359 : */
360 : template<class Awaitable>
361 108232 : auto transform_awaitable(Awaitable&& a)
362 : {
363 : using A = std::decay_t<Awaitable>;
364 : if constexpr (ContextAwaitable<A>)
365 : {
366 : return transform_awaiter<Awaitable>{
367 213947 : std::forward<Awaitable>(a), this};
368 : }
369 : else
370 : {
371 : static_assert(ContextAwaitable<A>, "requires ContextAwaitable");
372 : }
373 105715 : }
374 : };
375 :
376 : /** Handle to the owned coroutine frame.
377 :
378 : Null when the task is empty (for example after a move or after
379 : @ref release). Prefer @ref handle to read this; the member is
380 : public for use by the coroutine machinery.
381 : */
382 : std::coroutine_handle<promise_type> h_;
383 :
384 : /** Destroy the task and its coroutine frame if owned.
385 :
386 : @par Preconditions
387 : The frame is not lent: no parent is suspended awaiting it,
388 : unless its chain is being unwound after an @ref abandon.
389 : Destroying a lent frame otherwise calls `std::terminate`; end
390 : a running chain by requesting stop, or by destroying its
391 : execution context.
392 : */
393 113323 : ~task()
394 : {
395 113323 : destroy_frame();
396 113323 : }
397 :
398 : /** Report whether the awaited task is already complete.
399 :
400 : Always returns `false`; a task is lazy and has not started when
401 : it is awaited, so the awaiting coroutine always suspends.
402 :
403 : @return `false`.
404 : */
405 2108 : bool await_ready() const noexcept
406 : {
407 2108 : return false;
408 : }
409 :
410 : /** Return the task's result, rethrowing any captured exception.
411 :
412 : If the coroutine body exited via an unhandled exception, that
413 : exception is rethrown here. Otherwise the result is returned by
414 : move (for `task<T>`) or nothing is returned (for `task<void>`).
415 :
416 : @return The result value for non-void `T`; otherwise `void`.
417 :
418 : @throws The exception captured by the coroutine body, if any.
419 :
420 : @note Discarding a `results` silently drops the error
421 : code, so that overload is marked `[[nodiscard]]`.
422 : */
423 566 : [[nodiscard]] auto await_resume()
424 : requires detail::is_results_v<T>
425 : {
426 566 : lent_ = false;
427 566 : if(h_.promise().has_ep_)
428 104 : std::rethrow_exception(h_.promise().ep_);
429 462 : return std::move(*h_.promise().result_);
430 : }
431 :
432 243 : auto await_resume()
433 : requires (! detail::is_results_v<T>)
434 : {
435 243 : lent_ = false;
436 243 : if(h_.promise().has_ep_)
437 17 : std::rethrow_exception(h_.promise().ep_);
438 : if constexpr (! std::is_void_v<T>)
439 151 : return std::move(*h_.promise().result_);
440 : else
441 75 : return;
442 : }
443 :
444 : /** Start the task with the awaiting coroutine's context.
445 :
446 : Stores `cont` as the continuation to resume on completion.
447 : Stores `env` as the execution environment propagated to nested
448 : `co_await` expressions. Then transfers control into the task's
449 : coroutine body via the returned handle.
450 :
451 : @param cont The awaiting coroutine to resume when the task
452 : completes.
453 :
454 : @param env The execution environment (executor, stop token, and
455 : frame allocator). It must outlive the task.
456 :
457 : @return The task's coroutine handle, for symmetric transfer.
458 : */
459 2005 : std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, await_context const* env)
460 : {
461 2005 : h_.promise().set_continuation(cont);
462 2005 : h_.promise().set_environment(env);
463 2005 : lent_ = true;
464 2005 : return h_;
465 : }
466 :
467 : /** Return the coroutine handle.
468 :
469 : @note Do not call `destroy()` on the returned handle while the
470 : task is being awaited: the awaiting frame still holds it and
471 : destroys it again, which has undefined behavior. A holder that
472 : gives the coroutine up passes its continuation to @ref abandon
473 : instead. (Destroying or
474 : move-assigning over the task object itself while its frame is
475 : lent is a different operation, and calls `std::terminate`.)
476 : The task's lifetime is normally managed by `run_async`, `run`,
477 : or the awaiting parent. For cooperative cancellation, use
478 : `std::stop_token`.
479 :
480 : @note The operation, queue or primitive holding a suspended
481 : coroutine gives it up with @ref abandon, through the
482 : continuation it keeps, never by destroying a handle.
483 : `handle()` belongs to the owning task object.
484 :
485 : @return The coroutine handle.
486 : */
487 107273 : std::coroutine_handle<promise_type> handle() const noexcept
488 : {
489 107273 : return h_;
490 : }
491 :
492 : /** Release ownership of the coroutine frame.
493 :
494 : After calling this, destroying the task does not destroy the
495 : coroutine frame. The caller becomes responsible for the frame's
496 : lifetime.
497 :
498 : @note The caller may call `destroy()` on the released handle
499 : only when the task has not started or has fully completed.
500 : Destroying the released handle of a task that is being
501 : awaited is not supported and has undefined behavior.
502 :
503 : @par Postconditions
504 : `handle()` returns a null handle. Callers needing the
505 : original handle must save it, via @ref handle, before
506 : calling this.
507 : */
508 107130 : void release() noexcept
509 : {
510 107130 : h_ = nullptr;
511 107130 : }
512 :
513 : /** Copy construction is disabled; a task uniquely owns its frame.
514 :
515 : @param other The task that would be copied.
516 : */
517 : task(task const& other) = delete;
518 :
519 : /** Copy assignment is disabled; a task uniquely owns its frame.
520 :
521 : @param other The task that would be assigned from.
522 :
523 : @return A reference to `*this`.
524 : */
525 : task& operator=(task const& other) = delete;
526 :
527 : /** Construct by moving, transferring ownership of the frame.
528 :
529 : @par Postconditions
530 : `other` is empty and must not be awaited.
531 :
532 : @param other The task to move from.
533 : */
534 4074 : task(task&& other) noexcept
535 4074 : : h_(std::exchange(other.h_, nullptr))
536 : {
537 4074 : }
538 :
539 : /** Assign by moving, transferring ownership of the frame.
540 :
541 : If this task already owns a coroutine frame, that frame is
542 : destroyed first. Self-assignment is a no-op.
543 :
544 : @par Preconditions
545 : The frame being replaced is not lent: no parent is suspended
546 : awaiting it. Replacing a lent frame calls `std::terminate`.
547 :
548 : @par Postconditions
549 : `other` is empty and must not be awaited.
550 :
551 : @param other The task to move from.
552 :
553 : @return A reference to `*this`.
554 : */
555 1 : task& operator=(task&& other) noexcept
556 : {
557 1 : if(this != &other)
558 : {
559 1 : destroy_frame();
560 1 : h_ = std::exchange(other.h_, nullptr);
561 : }
562 1 : return *this;
563 : }
564 :
565 : private:
566 : // Set while a parent is suspended awaiting this task.
567 : bool lent_ = false;
568 :
569 109249 : explicit task(std::coroutine_handle<promise_type> h)
570 109249 : : h_(h)
571 : {
572 109249 : }
573 :
574 113324 : void destroy_frame() noexcept
575 : {
576 113324 : if(!h_)
577 111205 : return;
578 2119 : if(lent_ && !h_.done())
579 : {
580 : // Only the unwind of an abandoned chain destroys a frame
581 : // that is still awaited.
582 1277 : auto* owner = h_.promise().environment()->owner;
583 1277 : if(!owner || !owner->abandoned())
584 : std::terminate(); // LCOV_EXCL_LINE reached only in a death test's child process
585 : }
586 2119 : std::exchange(h_, nullptr).destroy();
587 2119 : lent_ = false;
588 : }
589 : };
590 :
591 : } // namespace capy
592 : } // namespace boost
593 :
594 : #endif
|