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/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 : #include <boost/capy/io_result.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 870 : void return_value(T value)
41 : {
42 870 : result_ = std::move(value);
43 870 : }
44 :
45 273 : T&& result() noexcept
46 : {
47 273 : return std::move(*result_);
48 : }
49 : };
50 :
51 : template<>
52 : struct task_return_base<void>
53 : {
54 1264 : void return_void()
55 : {
56 1264 : }
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. See @ref quitter for a task
89 : that stops its own body.
90 :
91 : @par Await-returns
92 : The value the body passed to `co_return`, moved out of the task, or
93 : nothing when `T` is `void`.
94 :
95 : If the body exits via an unhandled exception, that exception is
96 : rethrown instead.
97 :
98 : @par Await-postcondition
99 : The task's coroutine has run to completion and is suspended at its
100 : final suspend point. The task still owns the frame, but not the
101 : result: the await moves it out, so a task must not be awaited twice.
102 :
103 : @par Thread Safety
104 : Distinct objects: Safe.
105 : Shared objects: Unsafe.
106 :
107 : @par Example
108 :
109 : @code
110 : task<int> compute_value()
111 : {
112 : auto [ec, n] = co_await stream.read_some( buf );
113 : if( ec )
114 : co_return 0;
115 : co_return process( buf, n );
116 : }
117 :
118 : task<> run_session( tcp_socket sock )
119 : {
120 : int result = co_await compute_value();
121 : // ...
122 : }
123 : @endcode
124 :
125 : @tparam T The result type. Use `task<>` for `task<void>`.
126 :
127 : @see IoRunnable, IoAwaitable, run, run_async
128 : */
129 : template<typename T = void>
130 : struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
131 : task
132 : {
133 : /** Stores `task<T>`'s result and joins the I/O awaitable protocol via `io_awaitable_promise_base`.
134 :
135 : This is the promise object the compiler associates with a
136 : `task<T>` coroutine. It satisfies the coroutine promise
137 : requirements and participates in the I/O awaitable protocol via
138 : @ref io_awaitable_promise_base. It is part of the coroutine
139 : machinery and is not intended to be used directly by callers.
140 :
141 : Result storage and `return_value`/`return_void` are provided by
142 : `detail::task_return_base<T>`.
143 :
144 : @see io_awaitable_promise_base, IoRunnable
145 : */
146 : struct promise_type
147 : : io_awaitable_promise_base<promise_type>
148 : , detail::task_return_base<T>
149 : {
150 : private:
151 : friend task;
152 : union { std::exception_ptr ep_; };
153 : bool has_ep_;
154 :
155 : public:
156 : /// Construct the promise with no stored exception.
157 2772 : promise_type() noexcept
158 2772 : : has_ep_(false)
159 : {
160 2772 : }
161 :
162 : /// Destroy the promise, releasing any stored exception.
163 2772 : ~promise_type()
164 : {
165 2772 : if(has_ep_)
166 489 : ep_.~exception_ptr();
167 2772 : }
168 :
169 : /** Return the exception captured by the coroutine body, if any.
170 :
171 : @return The stored exception, or a null `std::exception_ptr`
172 : if the coroutine did not exit via an unhandled exception.
173 : */
174 2164 : std::exception_ptr exception() const noexcept
175 : {
176 2164 : if(has_ep_)
177 730 : return ep_;
178 1434 : return {};
179 : }
180 :
181 : /** Return the owning `task` for this coroutine.
182 :
183 : Called by the compiler to produce the object returned to the
184 : caller when the coroutine is created.
185 :
186 : @return A `task` owning the coroutine frame.
187 : */
188 2772 : task get_return_object()
189 : {
190 2772 : return task{std::coroutine_handle<promise_type>::from_promise(*this)};
191 : }
192 :
193 : /** Return the initial-suspend awaiter.
194 :
195 : The coroutine always suspends at the initial suspend point,
196 : so the body does not start until the task is awaited. When the
197 : body is resumed, the awaiter restores the thread-local frame
198 : allocator from the stored environment.
199 :
200 : @return An awaiter that suspends unconditionally.
201 : */
202 2772 : auto initial_suspend() noexcept
203 : {
204 : struct awaiter
205 : {
206 : promise_type* p_;
207 :
208 2772 : bool await_ready() const noexcept
209 : {
210 2772 : return false;
211 : }
212 :
213 2772 : void await_suspend(std::coroutine_handle<>) const noexcept
214 : {
215 2772 : }
216 :
217 2768 : void await_resume() const noexcept
218 : {
219 : // Restore TLS when body starts executing
220 2768 : set_current_frame_allocator(p_->environment()->frame_allocator);
221 2768 : }
222 : };
223 2772 : return awaiter{this};
224 : }
225 :
226 : /** Return the final-suspend awaiter.
227 :
228 : The coroutine always suspends at the final suspend point. The
229 : awaiter's `await_suspend` performs symmetric transfer to the
230 : stored continuation (consuming it), resuming the awaiting
231 : coroutine.
232 :
233 : @return An awaiter that suspends and transfers to the
234 : continuation.
235 : */
236 2623 : auto final_suspend() noexcept
237 : {
238 : struct awaiter
239 : {
240 : promise_type* p_;
241 :
242 2623 : bool await_ready() const noexcept
243 : {
244 2623 : return false;
245 : }
246 :
247 2623 : std::coroutine_handle<> await_suspend(std::coroutine_handle<>) const noexcept
248 : {
249 2623 : return p_->continuation();
250 : }
251 :
252 : void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
253 : };
254 2623 : return awaiter{this};
255 : }
256 :
257 : /** Capture the in-flight exception from the coroutine body.
258 :
259 : Called by the compiler when the coroutine body exits via an
260 : unhandled exception. The captured exception is rethrown when
261 : the task is awaited.
262 : */
263 489 : void unhandled_exception() noexcept
264 : {
265 489 : new (&ep_) std::exception_ptr(std::current_exception());
266 489 : has_ep_ = true;
267 489 : }
268 :
269 : /** Awaiter wrapping a nested `co_await` of an @ref IoAwaitable.
270 :
271 : Forwards the environment to the inner awaitable's
272 : environment-taking `await_suspend` and restores the
273 : thread-local frame allocator before the body resumes.
274 :
275 : @tparam Awaitable The awaitable being transformed.
276 : */
277 : template<class Awaitable>
278 : struct transform_awaiter
279 : {
280 : /// The wrapped awaitable, decayed and stored by value.
281 : std::decay_t<Awaitable> a_;
282 :
283 : /// The promise of the coroutine performing the `co_await`.
284 : promise_type* p_;
285 :
286 : /** Report whether the wrapped awaitable is already complete.
287 :
288 : @return The wrapped awaitable's own `await_ready` result:
289 : `true` if no suspension is needed.
290 : */
291 2883 : bool await_ready() noexcept
292 : {
293 2883 : return a_.await_ready();
294 : }
295 :
296 : /** Restore the frame allocator, then resume the wrapped
297 : awaitable.
298 :
299 : Reinstalls the thread-local frame allocator from the stored
300 : environment before the body continues. This is needed
301 : because the resumption may arrive on a different thread
302 : than the one that suspended.
303 :
304 : @return The wrapped awaitable's await-result, forwarded
305 : unchanged.
306 : */
307 2738 : decltype(auto) await_resume()
308 : {
309 : // Restore TLS before body resumes
310 2738 : set_current_frame_allocator(p_->environment()->frame_allocator);
311 2738 : return a_.await_resume();
312 : }
313 :
314 : /** Suspend by calling the wrapped awaitable with the
315 : environment.
316 :
317 : This is the plain `await_suspend` the compiler calls for the
318 : nested `co_await`. It forwards to the wrapped awaitable's
319 : @ref IoAwaitable overload, supplying the promise's stored
320 : environment as the second argument. It then hands back
321 : that call's result unchanged, so the wrapped awaitable's
322 : suspension decision, whatever form it takes, is preserved.
323 :
324 : @param h The coroutine performing the `co_await`.
325 :
326 : @return Whatever the wrapped awaitable's `await_suspend`
327 : returns. When that is a `std::coroutine_handle<>`, the
328 : handle is routed through `detail::symmetric_transfer`.
329 : On MSVC that helper resumes the handle on the current
330 : stack, and this function returns `void`, so the awaiting
331 : coroutine suspends unconditionally. On every other
332 : compiler the handle is returned unchanged for symmetric
333 : transfer.
334 : */
335 : template<class Promise>
336 2263 : auto await_suspend(std::coroutine_handle<Promise> h) noexcept
337 : {
338 : using R = decltype(a_.await_suspend(h, p_->environment()));
339 : if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
340 1263 : return detail::symmetric_transfer(a_.await_suspend(h, p_->environment()));
341 : else
342 1000 : return a_.await_suspend(h, p_->environment());
343 : }
344 : };
345 :
346 : /** Transform a nested awaitable before `co_await`.
347 :
348 : Wraps an @ref IoAwaitable in a @ref transform_awaiter so the
349 : coroutine's environment is propagated into it. A diagnostic
350 : is emitted if the awaitable does not satisfy @ref IoAwaitable.
351 :
352 : @param a The awaitable expression from `co_await a`.
353 :
354 : @return A @ref transform_awaiter wrapping `a`.
355 : */
356 : template<class Awaitable>
357 2883 : auto transform_awaitable(Awaitable&& a)
358 : {
359 : using A = std::decay_t<Awaitable>;
360 : if constexpr (IoAwaitable<A>)
361 : {
362 : return transform_awaiter<Awaitable>{
363 4420 : std::forward<Awaitable>(a), this};
364 : }
365 : else
366 : {
367 : static_assert(IoAwaitable<A>, "requires IoAwaitable");
368 : }
369 1537 : }
370 : };
371 :
372 : /** Handle to the owned coroutine frame.
373 :
374 : Null when the task is empty (for example after a move or after
375 : @ref release). Prefer @ref handle to read this; the member is
376 : public for use by the coroutine machinery.
377 : */
378 : std::coroutine_handle<promise_type> h_;
379 :
380 : /// Destroy the task and its coroutine frame if owned.
381 5862 : ~task()
382 : {
383 5862 : if(h_)
384 767 : h_.destroy();
385 5862 : }
386 :
387 : /** Report whether the awaited task is already complete.
388 :
389 : Always returns `false`; a task is lazy and has not started when
390 : it is awaited, so the awaiting coroutine always suspends.
391 :
392 : @return `false`.
393 : */
394 764 : bool await_ready() const noexcept
395 : {
396 764 : return false;
397 : }
398 :
399 : /** Return the task's result, rethrowing any captured exception.
400 :
401 : If the coroutine body exited via an unhandled exception, that
402 : exception is rethrown here. Otherwise the result is returned by
403 : move (for `task<T>`) or nothing is returned (for `task<void>`).
404 :
405 : @return The result value for non-void `T`; otherwise `void`.
406 :
407 : @throws The exception captured by the coroutine body, if any.
408 :
409 : @note Discarding an `io_result` silently drops the error
410 : code, so that overload is marked `[[nodiscard]]`.
411 : */
412 552 : [[nodiscard]] auto await_resume()
413 : requires detail::is_io_result_v<T>
414 : {
415 552 : if(h_.promise().has_ep_)
416 105 : std::rethrow_exception(h_.promise().ep_);
417 447 : return std::move(*h_.promise().result_);
418 : }
419 :
420 211 : auto await_resume()
421 : requires (! detail::is_io_result_v<T>)
422 : {
423 211 : if(h_.promise().has_ep_)
424 18 : std::rethrow_exception(h_.promise().ep_);
425 : if constexpr (! std::is_void_v<T>)
426 148 : return std::move(*h_.promise().result_);
427 : else
428 45 : return;
429 : }
430 :
431 : /** Start the task with the awaiting coroutine's context.
432 :
433 : Stores `cont` as the continuation to resume on completion.
434 : Stores `env` as the execution environment propagated to nested
435 : `co_await` expressions. Then transfers control into the task's
436 : coroutine body via the returned handle.
437 :
438 : @param cont The awaiting coroutine to resume when the task
439 : completes.
440 :
441 : @param env The execution environment (executor, stop token, and
442 : frame allocator). It must outlive the task.
443 :
444 : @return The task's coroutine handle, for symmetric transfer.
445 : */
446 683 : std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, io_env const* env)
447 : {
448 683 : h_.promise().set_continuation(cont);
449 683 : h_.promise().set_environment(env);
450 683 : return h_;
451 : }
452 :
453 : /** Return the coroutine handle.
454 :
455 : @note Do not call `destroy()` on the returned handle while the
456 : task is being awaited. The task's lifetime is normally managed
457 : by `run_async`, `run`, or the awaiting parent. Manually
458 : destroying a suspended task that another coroutine is awaiting
459 : produces undefined behavior. For cooperative cancellation, use
460 : `std::stop_token`.
461 :
462 : @return The coroutine handle.
463 : */
464 2088 : std::coroutine_handle<promise_type> handle() const noexcept
465 : {
466 2088 : return h_;
467 : }
468 :
469 : /** Release ownership of the coroutine frame.
470 :
471 : After calling this, destroying the task does not destroy the
472 : coroutine frame. The caller becomes responsible for the frame's
473 : lifetime.
474 :
475 : @note The caller may call `destroy()` on the released handle
476 : only when the task has not started or has fully completed.
477 : Destroying a suspended task that is being awaited produces
478 : undefined behavior.
479 :
480 : @par Postconditions
481 : `handle()` returns a null handle. Callers needing the
482 : original handle must save it, via @ref handle, before
483 : calling this.
484 : */
485 2005 : void release() noexcept
486 : {
487 2005 : h_ = nullptr;
488 2005 : }
489 :
490 : /** Copy construction is disabled; a task uniquely owns its frame.
491 :
492 : @param other The task that would be copied.
493 : */
494 : task(task const& other) = delete;
495 :
496 : /** Copy assignment is disabled; a task uniquely owns its frame.
497 :
498 : @param other The task that would be assigned from.
499 :
500 : @return A reference to `*this`.
501 : */
502 : task& operator=(task const& other) = delete;
503 :
504 : /** Construct by moving, transferring ownership of the frame.
505 :
506 : @par Postconditions
507 : `other` is empty and must not be awaited.
508 :
509 : @param other The task to move from.
510 : */
511 3090 : task(task&& other) noexcept
512 3090 : : h_(std::exchange(other.h_, nullptr))
513 : {
514 3090 : }
515 :
516 : /** Assign by moving, transferring ownership of the frame.
517 :
518 : If this task already owns a coroutine frame, that frame is
519 : destroyed first. Self-assignment is a no-op.
520 :
521 : @par Postconditions
522 : `other` is empty and must not be awaited.
523 :
524 : @param other The task to move from.
525 :
526 : @return A reference to `*this`.
527 : */
528 : task& operator=(task&& other) noexcept
529 : {
530 : if(this != &other)
531 : {
532 : if(h_)
533 : h_.destroy();
534 : h_ = std::exchange(other.h_, nullptr);
535 : }
536 : return *this;
537 : }
538 :
539 : private:
540 2772 : explicit task(std::coroutine_handle<promise_type> h)
541 2772 : : h_(h)
542 : {
543 2772 : }
544 : };
545 :
546 : } // namespace capy
547 : } // namespace boost
548 :
549 : #endif
|