LCOV - code coverage report
Current view: top level - capy/ex - run.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 283 283
Test Date: 2026-10-30 19:48:36 Functions: 87.6 % 386 338 48

           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_RUN_HPP
      12                 : #define BOOST_CAPY_RUN_HPP
      13                 : 
      14                 : #include <boost/capy/detail/config.hpp>
      15                 : #include <boost/capy/detail/await_suspend_helper.hpp>
      16                 : #include <boost/capy/detail/run.hpp>
      17                 : #include <boost/capy/concept/executor.hpp>
      18                 : #include <boost/capy/concept/context_runnable.hpp>
      19                 : #include <boost/capy/ex/abandon.hpp>
      20                 : #include <boost/capy/ex/executor_ref.hpp>
      21                 : #include <coroutine>
      22                 : #include <boost/capy/ex/frame_alloc_mixin.hpp>
      23                 : #include <boost/capy/ex/frame_allocator.hpp>
      24                 : #include <boost/capy/ex/await_context.hpp>
      25                 : 
      26                 : #include <exception>
      27                 : #include <memory_resource>
      28                 : #include <stop_token>
      29                 : #include <type_traits>
      30                 : #include <utility>
      31                 : #include <variant>
      32                 : 
      33                 : /*
      34                 :     Allocator Lifetime Strategy
      35                 :     ===========================
      36                 : 
      37                 :     When using run() with a custom allocator:
      38                 : 
      39                 :         co_await run(ex, alloc)(my_task());
      40                 : 
      41                 :     The wrapper run(ex, alloc) returns is a temporary holding the
      42                 :     frame_memory_resource my_task()'s frame is allocated from, so it must
      43                 :     live until that frame is freed: the awaitable is awaited in the
      44                 :     full-expression that made the wrapper, and the temporary lives until
      45                 :     the co_await completes. Keeping the awaitable and awaiting it later
      46                 :     frees the task's frame through a dead resource.
      47                 : 
      48                 :     The awaitable also holds a copy of the allocator. In await_suspend it
      49                 :     overwrites the promise's saved frame_allocator pointer with the
      50                 :     awaitable's resource, so tasks the inner task creates allocate from
      51                 :     it through TLS.
      52                 : */
      53                 : 
      54                 : namespace boost::capy::detail {
      55                 : 
      56                 : /** Minimal coroutine that dispatches through the caller's executor.
      57                 : 
      58                 :     Sits between the inner task and the parent when executors
      59                 :     diverge. The inner task's `final_suspend` resumes this
      60                 :     trampoline via symmetric transfer. The trampoline's own
      61                 :     `final_suspend` posts the parent through the caller's executor to
      62                 :     restore the correct execution context.
      63                 : 
      64                 :     It also owns the inner task's segment. Abandoning that segment
      65                 :     resumes the trampoline on the inner executor, or has a closed
      66                 :     executor destroy it: either way it destroys the inner task, which
      67                 :     unwinds the segment, then abandons the caller's segment, which
      68                 :     unwinds the parent on its own executor. The caller's execution
      69                 :     context must outlive that.
      70                 : */
      71                 : struct boundary_trampoline
      72                 : {
      73                 :     struct promise_type
      74                 :         : detail::process_frame_mixin
      75                 :     {
      76                 :         executor_ref caller_ex_;
      77                 :         continuation parent_;
      78                 :         chain_owner segment_;
      79                 :         // The inner task reports a stop-caused abandon (a quitter). For
      80                 :         // any other task such an abandon is torn down like any other.
      81                 :         bool stoppable_ = false;
      82                 :         // The awaitable's trampoline object until the inner task
      83                 :         // completes or is unwound; null afterward.
      84                 :         boundary_trampoline* owner_ = nullptr;
      85                 :         // Destroys the inner task the awaitable owns.
      86                 :         void (*destroy_inner_)(void*) noexcept = nullptr;
      87                 :         void* awaitable_ = nullptr;
      88                 : 
      89                 :         // Unwind the inner segment, then hand the parent to its own
      90                 :         // segment's owner. The last touch of this frame: abandoning the
      91                 :         // parent can destroy the awaitable, and this frame with it.
      92 HIT          10 :         void unwind() noexcept
      93                 :         {
      94              10 :             destroy_inner_(awaitable_);
      95              10 :             owner_ = nullptr;
      96              10 :             auto parent = parent_;
      97              10 :             capy::abandon(parent);
      98              10 :         }
      99                 : 
     100              95 :         ~promise_type()
     101                 :         {
     102                 :             // Linked only when a closed executor destroyed this frame
     103                 :             // instead of resuming it to unwind.
     104              95 :             if(owner_)
     105                 :             {
     106               1 :                 owner_->h_ = nullptr;
     107               1 :                 unwind();
     108                 :             }
     109              95 :         }
     110                 : 
     111              95 :         boundary_trampoline get_return_object() noexcept
     112                 :         {
     113                 :             return boundary_trampoline{
     114              95 :                 std::coroutine_handle<promise_type>::from_promise(*this)};
     115                 :         }
     116                 : 
     117              95 :         std::suspend_always initial_suspend() noexcept { return {}; }
     118                 : 
     119              93 :         auto final_suspend() noexcept
     120                 :         {
     121                 :             struct awaiter
     122                 :             {
     123                 :                 promise_type* p_;
     124              93 :                 bool await_ready() const noexcept { return false; }
     125                 : 
     126              93 :                 auto await_suspend(
     127                 :                     std::coroutine_handle<>) noexcept
     128                 :                 {
     129                 :                     // Nothing here touches p_ after the post or the
     130                 :                     // unwind: either can destroy this frame.
     131              93 :                     auto& segment = p_->segment_;
     132                 :                     // See run_async's trampoline: only a quitter run
     133                 :                     // here is stopped through this owner.
     134              93 :                     BOOST_CAPY_ASSERT(
     135                 :                         (p_->stoppable_ || !segment.stopped()) &&
     136                 :                         "a quitter stopped a task's segment: "
     137                 :                         "see quitter::handle");
     138             109 :                     if(segment.abandoned() &&
     139              16 :                         !(p_->stoppable_ && segment.stopped()))
     140                 :                     {
     141               9 :                         p_->unwind();
     142                 :                     }
     143                 :                     else
     144                 :                     {
     145                 :                         // A quitter's stop: its frames are destroyed
     146                 :                         // here, and the parent resumes to receive the
     147                 :                         // stopped value.
     148              84 :                         if(segment.abandoned())
     149               7 :                             p_->destroy_inner_(p_->awaitable_);
     150              84 :                         p_->owner_ = nullptr;
     151              84 :                         auto ex = p_->caller_ex_;
     152              84 :                         ex.post(p_->parent_);
     153                 :                     }
     154              93 :                     return detail::symmetric_transfer(
     155             186 :                         std::noop_coroutine());
     156                 :                 }
     157                 : 
     158                 :                 void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
     159                 :             };
     160              93 :             return awaiter{this};
     161                 :         }
     162                 : 
     163              93 :         void return_void() noexcept {}
     164                 :         void unhandled_exception() noexcept {}
     165                 :     };
     166                 : 
     167                 :     std::coroutine_handle<promise_type> h_{nullptr};
     168                 : 
     169              96 :     boundary_trampoline() noexcept = default;
     170                 : 
     171             285 :     ~boundary_trampoline()
     172                 :     {
     173             285 :         if(h_)
     174                 :         {
     175              93 :             h_.promise().owner_ = nullptr;
     176              93 :             h_.destroy();
     177                 :         }
     178             285 :     }
     179                 : 
     180                 :     boundary_trampoline(boundary_trampoline const&) = delete;
     181                 :     boundary_trampoline& operator=(boundary_trampoline const&) = delete;
     182                 : 
     183              94 :     boundary_trampoline(boundary_trampoline&& o) noexcept
     184              94 :         : h_(std::exchange(o.h_, nullptr)) {}
     185                 : 
     186              96 :     boundary_trampoline& operator=(boundary_trampoline&& o) noexcept
     187                 :     {
     188              96 :         if(this != &o)
     189                 :         {
     190              96 :             if(h_)
     191                 :             {
     192               1 :                 h_.promise().owner_ = nullptr;
     193               1 :                 h_.destroy();
     194                 :             }
     195              96 :             h_ = std::exchange(o.h_, nullptr);
     196                 :         }
     197              96 :         return *this;
     198                 :     }
     199                 : 
     200                 : private:
     201              95 :     explicit boundary_trampoline(std::coroutine_handle<promise_type> h) noexcept
     202              95 :         : h_(h) {}
     203                 : };
     204                 : 
     205              95 : inline boundary_trampoline make_boundary_trampoline()
     206                 : {
     207                 :     co_return;
     208             190 : }
     209                 : 
     210                 : /** Awaitable that binds a ContextRunnable to a specific executor.
     211                 : 
     212                 :     Stores the executor and inner task by value. When co_awaited, the
     213                 :     co_await expression's lifetime extension keeps both alive for the
     214                 :     duration of the operation.
     215                 : 
     216                 :     A dispatch trampoline handles the executor switch on completion:
     217                 :     the inner task's `final_suspend` resumes the trampoline, which
     218                 :     dispatches back through the caller's executor.
     219                 : 
     220                 :     The `await_context` is owned by this awaitable and is guaranteed to
     221                 :     outlive the inner task and all awaitables in its chain. Awaitables
     222                 :     may store `await_context const*` without concern for dangling references.
     223                 : 
     224                 :     @tparam Task The ContextRunnable type
     225                 :     @tparam Ex The executor type
     226                 :     @tparam InheritStopToken If true, inherit caller's stop token
     227                 :     @tparam Alloc The allocator type (void for no allocator)
     228                 : */
     229                 : template<ContextRunnable Task, Executor Ex, bool InheritStopToken, class Alloc = void>
     230                 : struct [[nodiscard]] run_awaitable_ex
     231                 : {
     232                 :     Ex ex_;
     233                 :     frame_memory_resource<Alloc> resource_;
     234                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     235                 :     await_context env_;
     236                 :     executor_ref caller_ex_;
     237                 :     boundary_trampoline tr_;
     238                 :     continuation task_cont_;
     239                 :     bool holds_work_ = false;
     240                 :     Task inner_;  // Last: destroyed first, while env_ is still valid
     241                 : 
     242             189 :     ~run_awaitable_ex()
     243                 :     {
     244                 :         // Only a parent destroyed from above leaves the trampoline
     245                 :         // still linked: the inner task is still running.
     246             189 :         if(tr_.h_ && tr_.h_.promise().owner_)
     247                 :             std::terminate(); // LCOV_EXCL_LINE reached only in a death test's child process
     248                 :         // A parent unwound instead of resumed never reaches
     249                 :         // await_resume.
     250             189 :         if(holds_work_)
     251              13 :             caller_ex_.on_work_finished();
     252             189 :     }
     253                 : 
     254                 :     // Called by the trampoline to unwind the inner segment.
     255              17 :     static void destroy_inner(void* p) noexcept
     256                 :     {
     257              17 :         auto& self = *static_cast<run_awaitable_ex*>(p);
     258              17 :         auto h = self.inner_.handle();
     259              17 :         self.inner_.release();
     260              17 :         h.destroy();
     261              17 :     }
     262                 : 
     263                 :     // void allocator, inherit stop token
     264              82 :     run_awaitable_ex(Ex ex, Task inner)
     265                 :         requires (InheritStopToken && std::is_void_v<Alloc>)
     266              82 :         : ex_(std::move(ex))
     267              82 :         , inner_(std::move(inner))
     268                 :     {
     269              82 :     }
     270                 : 
     271                 :     // void allocator, explicit stop token
     272               6 :     run_awaitable_ex(Ex ex, Task inner, std::stop_token st)
     273                 :         requires (!InheritStopToken && std::is_void_v<Alloc>)
     274               6 :         : ex_(std::move(ex))
     275               6 :         , st_(std::move(st))
     276               6 :         , inner_(std::move(inner))
     277                 :     {
     278               6 :     }
     279                 : 
     280                 :     // with allocator, inherit stop token (use template to avoid void parameter)
     281                 :     template<class A>
     282                 :         requires (InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
     283               5 :     run_awaitable_ex(Ex ex, A alloc, Task inner)
     284               5 :         : ex_(std::move(ex))
     285               5 :         , resource_(std::move(alloc))
     286               5 :         , inner_(std::move(inner))
     287                 :     {
     288               5 :     }
     289                 : 
     290                 :     // with allocator, explicit stop token (use template to avoid void parameter)
     291                 :     template<class A>
     292                 :         requires (!InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
     293               2 :     run_awaitable_ex(Ex ex, A alloc, Task inner, std::stop_token st)
     294               2 :         : ex_(std::move(ex))
     295               2 :         , resource_(std::move(alloc))
     296               2 :         , st_(std::move(st))
     297               2 :         , inner_(std::move(inner))
     298                 :     {
     299               2 :     }
     300                 : 
     301              94 :     bool await_ready() const noexcept
     302                 :     {
     303              94 :         return inner_.await_ready();
     304                 :     }
     305                 : 
     306              81 :     decltype(auto) await_resume()
     307                 :     {
     308              81 :         if(std::exchange(holds_work_, false))
     309              81 :             caller_ex_.on_work_finished();
     310                 :         if constexpr(stops_by_abandonment<Task>)
     311                 :         {
     312               6 :             if(tr_.h_ && tr_.h_.promise().segment_.stopped())
     313               6 :                 return Task::promise_type::stopped_result();
     314                 :         }
     315              75 :         return inner_.await_resume();
     316                 :     }
     317                 : 
     318              95 :     std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, await_context const* caller_env)
     319                 :     {
     320              95 :         tr_ = make_boundary_trampoline();
     321              95 :         auto& tp = tr_.h_.promise();
     322              95 :         tp.owner_ = &tr_;
     323              95 :         tp.caller_ex_ = caller_env->executor;
     324              95 :         tp.parent_.h = cont;
     325              95 :         tp.parent_.owner = caller_env->owner;
     326              95 :         tp.destroy_inner_ = &run_awaitable_ex::destroy_inner;
     327              95 :         tp.awaitable_ = this;
     328              95 :         tp.segment_.bind(tr_.h_, ex_);
     329              95 :         tp.stoppable_ = stops_by_abandonment<Task>;
     330              95 :         caller_ex_ = caller_env->executor;
     331                 :         // Released once the parent resumes or is unwound. Not on the
     332                 :         // post back: the parent's chain may own the executor object and
     333                 :         // can be gone by the time post returns.
     334              95 :         caller_ex_.on_work_started();
     335              95 :         holds_work_ = true;
     336                 : 
     337              95 :         auto h = inner_.handle();
     338              95 :         auto& p = h.promise();
     339              95 :         p.set_continuation(tr_.h_);
     340                 : 
     341              95 :         env_.owner = &tp.segment_;
     342              95 :         env_.executor = ex_;
     343                 :         if constexpr (InheritStopToken)
     344              87 :             env_.stop_token = caller_env->stop_token;
     345                 :         else
     346               8 :             env_.stop_token = st_;
     347                 : 
     348                 :         if constexpr (!std::is_void_v<Alloc>)
     349               7 :             env_.frame_allocator = resource_.get();
     350                 :         else
     351              88 :             env_.frame_allocator = caller_env->frame_allocator;
     352                 : 
     353              95 :         p.set_environment(&env_);
     354                 : 
     355              95 :         task_cont_.h = h;
     356              95 :         task_cont_.owner = &tp.segment_;
     357                 :         try
     358                 :         {
     359                 :             // A quitter starts through its gate, which checks the stop
     360                 :             // when the body would start: a stopped quitter's segment is
     361                 :             // abandoned for the stop, and the parent receives the
     362                 :             // stopped value.
     363                 :             if constexpr(stops_by_abandonment<Task>)
     364               7 :                 task_cont_.h = p.launch_handle();
     365              95 :             ex_.post(task_cont_);
     366                 :         }
     367               2 :         catch(...)
     368                 :         {
     369                 :             // The parent resumes with the exception: nothing may still
     370                 :             // treat the task as lent or hold work for the parent.
     371               1 :             p.set_continuation(std::noop_coroutine());
     372               1 :             task_cont_.h = nullptr;
     373               1 :             tr_ = boundary_trampoline();
     374               1 :             holds_work_ = false;
     375               1 :             caller_ex_.on_work_finished();
     376               1 :             throw;
     377                 :         }
     378              94 :         return std::noop_coroutine();
     379                 :     }
     380                 : 
     381                 :     // Non-copyable
     382                 :     run_awaitable_ex(run_awaitable_ex const&) = delete;
     383                 :     run_awaitable_ex& operator=(run_awaitable_ex const&) = delete;
     384                 : 
     385                 :     // Movable (no noexcept - Task may throw)
     386              94 :     run_awaitable_ex(run_awaitable_ex&&) = default;
     387                 :     run_awaitable_ex& operator=(run_awaitable_ex&&) = default;
     388                 : };
     389                 : 
     390                 : /** Awaitable that runs a task with optional stop_token override.
     391                 : 
     392                 :     Does NOT store an executor - the task inherits the caller's executor
     393                 :     directly. Executors always match, so no dispatch trampoline is needed.
     394                 :     The inner task's `final_suspend` resumes the parent directly via
     395                 :     unconditional symmetric transfer.
     396                 : 
     397                 :     @tparam Task The ContextRunnable type
     398                 :     @tparam InheritStopToken If true, inherit caller's stop token
     399                 :     @tparam Alloc The allocator type (void for no allocator)
     400                 : */
     401                 : template<ContextRunnable Task, bool InheritStopToken, class Alloc = void>
     402                 : struct [[nodiscard]] run_awaitable
     403                 : {
     404                 :     frame_memory_resource<Alloc> resource_;
     405                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     406                 :     await_context env_;
     407                 :     // Set while the parent is suspended awaiting the inner task.
     408                 :     bool lent_ = false;
     409                 :     Task inner_;  // Last: destroyed first, while env_ is still valid
     410                 : 
     411              42 :     ~run_awaitable()
     412                 :     {
     413                 :         // Only the unwind of an abandoned chain destroys an inner task
     414                 :         // still awaited; checked here so it terminates whatever the
     415                 :         // task type does when destroyed while lent.
     416              42 :         auto h = inner_.handle();
     417              52 :         if(lent_ && h && !h.done() &&
     418              10 :             !(env_.owner && env_.owner->abandoned()))
     419                 :             std::terminate(); // LCOV_EXCL_LINE reached only in a death test's child process
     420              42 :     }
     421                 : 
     422                 :     // void allocator, inherit stop token
     423                 :     explicit run_awaitable(Task inner)
     424                 :         requires (InheritStopToken && std::is_void_v<Alloc>)
     425                 :         : inner_(std::move(inner))
     426                 :     {
     427                 :     }
     428                 : 
     429                 :     // void allocator, explicit stop token
     430               5 :     run_awaitable(Task inner, std::stop_token st)
     431                 :         requires (!InheritStopToken && std::is_void_v<Alloc>)
     432               5 :         : st_(std::move(st))
     433               5 :         , inner_(std::move(inner))
     434                 :     {
     435               5 :     }
     436                 : 
     437                 :     // with allocator, inherit stop token (use template to avoid void parameter)
     438                 :     template<class A>
     439                 :         requires (InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
     440              14 :     run_awaitable(A alloc, Task inner)
     441              14 :         : resource_(std::move(alloc))
     442              14 :         , inner_(std::move(inner))
     443                 :     {
     444              14 :     }
     445                 : 
     446                 :     // with allocator, explicit stop token (use template to avoid void parameter)
     447                 :     template<class A>
     448                 :         requires (!InheritStopToken && !std::is_void_v<Alloc> && std::same_as<A, Alloc>)
     449               2 :     run_awaitable(A alloc, Task inner, std::stop_token st)
     450               2 :         : resource_(std::move(alloc))
     451               2 :         , st_(std::move(st))
     452               2 :         , inner_(std::move(inner))
     453                 :     {
     454               2 :     }
     455                 : 
     456              21 :     bool await_ready() const noexcept
     457                 :     {
     458              21 :         return inner_.await_ready();
     459                 :     }
     460                 : 
     461              11 :     decltype(auto) await_resume()
     462                 :     {
     463              11 :         lent_ = false;
     464              11 :         return inner_.await_resume();
     465                 :     }
     466                 : 
     467              21 :     std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, await_context const* caller_env)
     468                 :     {
     469              21 :         env_.owner = caller_env->owner;
     470              21 :         env_.executor = caller_env->executor;
     471                 :         if constexpr (InheritStopToken)
     472              14 :             env_.stop_token = caller_env->stop_token;
     473                 :         else
     474               7 :             env_.stop_token = st_;
     475                 : 
     476                 :         if constexpr (!std::is_void_v<Alloc>)
     477              16 :             env_.frame_allocator = resource_.get();
     478                 :         else
     479               5 :             env_.frame_allocator = caller_env->frame_allocator;
     480                 : 
     481                 :         // A quitter opens a segment of its own, so a stop on this run's
     482                 :         // token never abandons the caller's segment.
     483                 :         if constexpr(stops_by_abandonment<Task>)
     484                 :         {
     485                 :             // Lent only once the segment is open: opening it can throw
     486                 :             // into a quitter's body, and a lent inner task terminates
     487                 :             // in our destructor.
     488               3 :             auto h = inner_.await_suspend(cont, &env_);
     489               3 :             lent_ = true;
     490               3 :             return h;
     491                 :         }
     492                 :         else
     493                 :         {
     494              18 :             auto h = inner_.handle();
     495              18 :             auto& p = h.promise();
     496              18 :             p.set_continuation(cont);
     497              18 :             p.set_environment(&env_);
     498              18 :             lent_ = true;
     499              18 :             return h;
     500                 :         }
     501                 :     }
     502                 : 
     503                 :     // Non-copyable
     504                 :     run_awaitable(run_awaitable const&) = delete;
     505                 :     run_awaitable& operator=(run_awaitable const&) = delete;
     506                 : 
     507                 :     // Movable (no noexcept - Task may throw)
     508              21 :     run_awaitable(run_awaitable&&) = default;
     509                 :     run_awaitable& operator=(run_awaitable&&) = default;
     510                 : };
     511                 : 
     512                 : /** Wrapper returned by run(ex, ...) that accepts a task for execution.
     513                 : 
     514                 :     @tparam Ex The executor type.
     515                 :     @tparam InheritStopToken If true, inherit caller's stop token.
     516                 :     @tparam Alloc The allocator type (void for no allocator).
     517                 : */
     518                 : template<Executor Ex, bool InheritStopToken, class Alloc>
     519                 : class [[nodiscard]] run_wrapper_ex
     520                 : {
     521                 :     Ex ex_;
     522                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     523                 :     frame_memory_resource<Alloc> resource_;
     524                 :     Alloc alloc_;  // Copy to pass to awaitable
     525                 : 
     526                 :     std::pmr::memory_resource* saved_tls_ =
     527                 :         get_current_frame_allocator();
     528                 :     bool restored_ = false;
     529                 : 
     530                 : public:
     531               3 :     run_wrapper_ex(Ex ex, Alloc alloc)
     532                 :         requires InheritStopToken
     533               3 :         : ex_(std::move(ex))
     534               3 :         , resource_(alloc)
     535               3 :         , alloc_(std::move(alloc))
     536                 :     {
     537               3 :         set_current_frame_allocator(&resource_);
     538               3 :     }
     539                 : 
     540               1 :     run_wrapper_ex(Ex ex, std::stop_token st, Alloc alloc)
     541                 :         requires (!InheritStopToken)
     542               1 :         : ex_(std::move(ex))
     543               1 :         , st_(std::move(st))
     544               1 :         , resource_(alloc)
     545               1 :         , alloc_(std::move(alloc))
     546                 :     {
     547               1 :         set_current_frame_allocator(&resource_);
     548               1 :     }
     549                 : 
     550                 :     // Restores the caller's allocator when evaluating the task argument
     551                 :     // threw. Not afterwards: a wrapper destroyed with a torn-down parent
     552                 :     // frame must not touch the tearing thread's allocator.
     553               4 :     ~run_wrapper_ex()
     554                 :     {
     555               4 :         if(!restored_)
     556               1 :             set_current_frame_allocator(saved_tls_);
     557               4 :     }
     558                 : 
     559                 :     // Non-copyable, non-movable (must be used immediately)
     560                 :     run_wrapper_ex(run_wrapper_ex const&) = delete;
     561                 :     run_wrapper_ex(run_wrapper_ex&&) = delete;
     562                 :     run_wrapper_ex& operator=(run_wrapper_ex const&) = delete;
     563                 :     run_wrapper_ex& operator=(run_wrapper_ex&&) = delete;
     564                 : 
     565                 :     template<ContextRunnable Task>
     566               3 :     [[nodiscard]] auto operator()(Task t) &&
     567                 :     {
     568                 :         // The task's frame is made; the caller's own frames are not.
     569               3 :         set_current_frame_allocator(saved_tls_);
     570               3 :         restored_ = true;
     571                 :         if constexpr (InheritStopToken)
     572                 :             return run_awaitable_ex<Task, Ex, true, Alloc>{
     573               2 :                 std::move(ex_), std::move(alloc_), std::move(t)};
     574                 :         else
     575                 :             return run_awaitable_ex<Task, Ex, false, Alloc>{
     576               1 :                 std::move(ex_), std::move(alloc_), std::move(t), std::move(st_)};
     577                 :     }
     578                 : };
     579                 : 
     580                 : /// Specialization for memory_resource* - stores pointer directly.
     581                 : template<Executor Ex, bool InheritStopToken>
     582                 : class [[nodiscard]] run_wrapper_ex<Ex, InheritStopToken, std::pmr::memory_resource*>
     583                 : {
     584                 :     Ex ex_;
     585                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     586                 :     std::pmr::memory_resource* mr_;
     587                 : 
     588                 :     std::pmr::memory_resource* saved_tls_ =
     589                 :         get_current_frame_allocator();
     590                 :     bool restored_ = false;
     591                 : 
     592                 : public:
     593               4 :     run_wrapper_ex(Ex ex, std::pmr::memory_resource* mr)
     594                 :         requires InheritStopToken
     595               4 :         : ex_(std::move(ex))
     596               4 :         , mr_(mr)
     597                 :     {
     598               4 :         set_current_frame_allocator(mr_);
     599               4 :     }
     600                 : 
     601               1 :     run_wrapper_ex(Ex ex, std::stop_token st, std::pmr::memory_resource* mr)
     602                 :         requires (!InheritStopToken)
     603               1 :         : ex_(std::move(ex))
     604               1 :         , st_(std::move(st))
     605               1 :         , mr_(mr)
     606                 :     {
     607               1 :         set_current_frame_allocator(mr_);
     608               1 :     }
     609                 : 
     610                 :     // Restores the caller's allocator when evaluating the task argument
     611                 :     // threw. Not afterwards: a wrapper destroyed with a torn-down parent
     612                 :     // frame must not touch the tearing thread's allocator.
     613               5 :     ~run_wrapper_ex()
     614                 :     {
     615               5 :         if(!restored_)
     616               1 :             set_current_frame_allocator(saved_tls_);
     617               5 :     }
     618                 : 
     619                 :     // Non-copyable, non-movable (must be used immediately)
     620                 :     run_wrapper_ex(run_wrapper_ex const&) = delete;
     621                 :     run_wrapper_ex(run_wrapper_ex&&) = delete;
     622                 :     run_wrapper_ex& operator=(run_wrapper_ex const&) = delete;
     623                 :     run_wrapper_ex& operator=(run_wrapper_ex&&) = delete;
     624                 : 
     625                 :     template<ContextRunnable Task>
     626               4 :     [[nodiscard]] auto operator()(Task t) &&
     627                 :     {
     628                 :         // The task's frame is made; the caller's own frames are not.
     629               4 :         set_current_frame_allocator(saved_tls_);
     630               4 :         restored_ = true;
     631                 :         if constexpr (InheritStopToken)
     632                 :             return run_awaitable_ex<Task, Ex, true, std::pmr::memory_resource*>{
     633               3 :                 std::move(ex_), mr_, std::move(t)};
     634                 :         else
     635                 :             return run_awaitable_ex<Task, Ex, false, std::pmr::memory_resource*>{
     636               1 :                 std::move(ex_), mr_, std::move(t), std::move(st_)};
     637                 :     }
     638                 : };
     639                 : 
     640                 : /// Specialization for no allocator (void).
     641                 : template<Executor Ex, bool InheritStopToken>
     642                 : class [[nodiscard]] run_wrapper_ex<Ex, InheritStopToken, void>
     643                 : {
     644                 :     Ex ex_;
     645                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     646                 : 
     647                 : public:
     648              82 :     explicit run_wrapper_ex(Ex ex)
     649                 :         requires InheritStopToken
     650              82 :         : ex_(std::move(ex))
     651                 :     {
     652              82 :     }
     653                 : 
     654               6 :     run_wrapper_ex(Ex ex, std::stop_token st)
     655                 :         requires (!InheritStopToken)
     656               6 :         : ex_(std::move(ex))
     657               6 :         , st_(std::move(st))
     658                 :     {
     659               6 :     }
     660                 : 
     661                 :     // Non-copyable, non-movable (must be used immediately)
     662                 :     run_wrapper_ex(run_wrapper_ex const&) = delete;
     663                 :     run_wrapper_ex(run_wrapper_ex&&) = delete;
     664                 :     run_wrapper_ex& operator=(run_wrapper_ex const&) = delete;
     665                 :     run_wrapper_ex& operator=(run_wrapper_ex&&) = delete;
     666                 : 
     667                 :     template<ContextRunnable Task>
     668              88 :     [[nodiscard]] auto operator()(Task t) &&
     669                 :     {
     670                 :         if constexpr (InheritStopToken)
     671                 :             return run_awaitable_ex<Task, Ex, true>{
     672              82 :                 std::move(ex_), std::move(t)};
     673                 :         else
     674                 :             return run_awaitable_ex<Task, Ex, false>{
     675               6 :                 std::move(ex_), std::move(t), std::move(st_)};
     676                 :     }
     677                 : };
     678                 : 
     679                 : /** Wrapper returned by run(st) or run(alloc) that accepts a task.
     680                 : 
     681                 :     @tparam InheritStopToken If true, inherit caller's stop token.
     682                 :     @tparam Alloc The allocator type (void for no allocator).
     683                 : */
     684                 : template<bool InheritStopToken, class Alloc>
     685                 : class [[nodiscard]] run_wrapper
     686                 : {
     687                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     688                 :     frame_memory_resource<Alloc> resource_;
     689                 :     Alloc alloc_;  // Copy to pass to awaitable
     690                 : 
     691                 :     std::pmr::memory_resource* saved_tls_ =
     692                 :         get_current_frame_allocator();
     693                 :     bool restored_ = false;
     694                 : 
     695                 : public:
     696               3 :     explicit run_wrapper(Alloc alloc)
     697                 :         requires InheritStopToken
     698               3 :         : resource_(alloc)
     699               3 :         , alloc_(std::move(alloc))
     700                 :     {
     701               3 :         set_current_frame_allocator(&resource_);
     702               3 :     }
     703                 : 
     704               1 :     run_wrapper(std::stop_token st, Alloc alloc)
     705                 :         requires (!InheritStopToken)
     706               1 :         : st_(std::move(st))
     707               1 :         , resource_(alloc)
     708               1 :         , alloc_(std::move(alloc))
     709                 :     {
     710               1 :         set_current_frame_allocator(&resource_);
     711               1 :     }
     712                 : 
     713                 :     // Restores the caller's allocator when evaluating the task argument
     714                 :     // threw. Not afterwards: a wrapper destroyed with a torn-down parent
     715                 :     // frame must not touch the tearing thread's allocator.
     716               4 :     ~run_wrapper()
     717                 :     {
     718               4 :         if(!restored_)
     719               1 :             set_current_frame_allocator(saved_tls_);
     720               4 :     }
     721                 : 
     722                 :     // Non-copyable, non-movable (must be used immediately)
     723                 :     run_wrapper(run_wrapper const&) = delete;
     724                 :     run_wrapper(run_wrapper&&) = delete;
     725                 :     run_wrapper& operator=(run_wrapper const&) = delete;
     726                 :     run_wrapper& operator=(run_wrapper&&) = delete;
     727                 : 
     728                 :     template<ContextRunnable Task>
     729               3 :     [[nodiscard]] auto operator()(Task t) &&
     730                 :     {
     731                 :         // The task's frame is made; the caller's own frames are not.
     732               3 :         set_current_frame_allocator(saved_tls_);
     733               3 :         restored_ = true;
     734                 :         if constexpr (InheritStopToken)
     735                 :             return run_awaitable<Task, true, Alloc>{
     736               2 :                 std::move(alloc_), std::move(t)};
     737                 :         else
     738                 :             return run_awaitable<Task, false, Alloc>{
     739               1 :                 std::move(alloc_), std::move(t), std::move(st_)};
     740                 :     }
     741                 : };
     742                 : 
     743                 : /// Specialization for memory_resource* - stores pointer directly.
     744                 : template<bool InheritStopToken>
     745                 : class [[nodiscard]] run_wrapper<InheritStopToken, std::pmr::memory_resource*>
     746                 : {
     747                 :     std::conditional_t<InheritStopToken, std::monostate, std::stop_token> st_;
     748                 :     std::pmr::memory_resource* mr_;
     749                 : 
     750                 :     std::pmr::memory_resource* saved_tls_ =
     751                 :         get_current_frame_allocator();
     752                 :     bool restored_ = false;
     753                 : 
     754                 : public:
     755              13 :     explicit run_wrapper(std::pmr::memory_resource* mr)
     756                 :         requires InheritStopToken
     757              13 :         : mr_(mr)
     758                 :     {
     759              13 :         set_current_frame_allocator(mr_);
     760              13 :     }
     761                 : 
     762               1 :     run_wrapper(std::stop_token st, std::pmr::memory_resource* mr)
     763                 :         requires (!InheritStopToken)
     764               1 :         : st_(std::move(st))
     765               1 :         , mr_(mr)
     766                 :     {
     767               1 :         set_current_frame_allocator(mr_);
     768               1 :     }
     769                 : 
     770                 :     // Restores the caller's allocator when evaluating the task argument
     771                 :     // threw. Not afterwards: a wrapper destroyed with a torn-down parent
     772                 :     // frame must not touch the tearing thread's allocator.
     773              14 :     ~run_wrapper()
     774                 :     {
     775              14 :         if(!restored_)
     776               1 :             set_current_frame_allocator(saved_tls_);
     777              14 :     }
     778                 : 
     779                 :     // Non-copyable, non-movable (must be used immediately)
     780                 :     run_wrapper(run_wrapper const&) = delete;
     781                 :     run_wrapper(run_wrapper&&) = delete;
     782                 :     run_wrapper& operator=(run_wrapper const&) = delete;
     783                 :     run_wrapper& operator=(run_wrapper&&) = delete;
     784                 : 
     785                 :     template<ContextRunnable Task>
     786              13 :     [[nodiscard]] auto operator()(Task t) &&
     787                 :     {
     788                 :         // The task's frame is made; the caller's own frames are not.
     789              13 :         set_current_frame_allocator(saved_tls_);
     790              13 :         restored_ = true;
     791                 :         if constexpr (InheritStopToken)
     792                 :             return run_awaitable<Task, true, std::pmr::memory_resource*>{
     793              12 :                 mr_, std::move(t)};
     794                 :         else
     795                 :             return run_awaitable<Task, false, std::pmr::memory_resource*>{
     796               1 :                 mr_, std::move(t), std::move(st_)};
     797                 :     }
     798                 : };
     799                 : 
     800                 : /// Specialization for stop_token only (no allocator).
     801                 : template<>
     802                 : class [[nodiscard]] run_wrapper<false, void>
     803                 : {
     804                 :     std::stop_token st_;
     805                 : 
     806                 : public:
     807               5 :     explicit run_wrapper(std::stop_token st)
     808               5 :         : st_(std::move(st))
     809                 :     {
     810               5 :     }
     811                 : 
     812                 :     // Non-copyable, non-movable (must be used immediately)
     813                 :     run_wrapper(run_wrapper const&) = delete;
     814                 :     run_wrapper(run_wrapper&&) = delete;
     815                 :     run_wrapper& operator=(run_wrapper const&) = delete;
     816                 :     run_wrapper& operator=(run_wrapper&&) = delete;
     817                 : 
     818                 :     template<ContextRunnable Task>
     819               5 :     [[nodiscard]] auto operator()(Task t) &&
     820                 :     {
     821               5 :         return run_awaitable<Task, false, void>{std::move(t), std::move(st_)};
     822                 :     }
     823                 : };
     824                 : 
     825                 : } // namespace boost::capy::detail
     826                 : 
     827                 : namespace boost::capy {
     828                 : 
     829                 : /** Bind a task to execute on a specific executor.
     830                 : 
     831                 :     Returns a wrapper that accepts a task and produces an awaitable.
     832                 :     When co_awaited, the task runs on the specified executor.
     833                 : 
     834                 :     If the inner task is abandoned before it completes, for example
     835                 :     because its execution context is torn down, the caller's chain is
     836                 :     abandoned too: it is unwound by its owner, on the owner's executor,
     837                 :     without the caller resuming. The awaitable holds work on the
     838                 :     caller's executor until the caller resumes or is unwound.
     839                 : 
     840                 :     A @ref quitter's stop is not such an abandon. When the inner task
     841                 :     is a quitter and a stop ends it, the caller resumes on its own
     842                 :     executor, and the `co_await` yields the quitter's stopped value, as
     843                 :     described in @ref quitter's Await-returns.
     844                 : 
     845                 :     @par Preconditions
     846                 :     The caller's execution context outlives the inner task and the
     847                 :     unwind of the caller's chain. This is not detected.
     848                 : 
     849                 :     @par Example
     850                 :     @par !example example_2
     851                 : 
     852                 : 
     853                 :     @param ex The executor on which the task should run.
     854                 : 
     855                 :     @return A wrapper that accepts a task for execution.
     856                 : 
     857                 :     @see @ref task
     858                 :     @see @ref Executor
     859                 : */
     860                 : template<Executor Ex>
     861                 : [[nodiscard]] auto
     862              82 : run(Ex ex)
     863                 : {
     864              82 :     return detail::run_wrapper_ex<Ex, true, void>{std::move(ex)};
     865                 : }
     866                 : 
     867                 : /** Bind a task to an executor with a stop token.
     868                 : 
     869                 :     @ref run(Ex) describes teardown and preconditions.
     870                 : 
     871                 :     @param ex The executor on which the task should run.
     872                 :     @param st The stop token for cooperative cancellation.
     873                 : 
     874                 :     @return A wrapper that accepts a task for execution.
     875                 : 
     876                 :     @see @ref run(Ex)
     877                 : */
     878                 : template<Executor Ex>
     879                 : [[nodiscard]] auto
     880               6 : run(Ex ex, std::stop_token st)
     881                 : {
     882                 :     return detail::run_wrapper_ex<Ex, false, void>{
     883               6 :         std::move(ex), std::move(st)};
     884                 : }
     885                 : 
     886                 : /** Bind a task to an executor with a memory resource.
     887                 : 
     888                 :     @ref run(Ex) describes teardown and preconditions.
     889                 : 
     890                 :     @param ex The executor on which the task should run.
     891                 :     @param mr The memory resource for frame allocation.
     892                 : 
     893                 :     @return A wrapper that accepts a task for execution.
     894                 : 
     895                 :     @see @ref run(Ex)
     896                 : */
     897                 : template<Executor Ex>
     898                 : [[nodiscard]] auto
     899               4 : run(Ex ex, std::pmr::memory_resource* mr)
     900                 : {
     901                 :     return detail::run_wrapper_ex<Ex, true, std::pmr::memory_resource*>{
     902               4 :         std::move(ex), mr};
     903                 : }
     904                 : 
     905                 : /** Bind a task to an executor with a standard allocator.
     906                 : 
     907                 :     Await the awaitable in the expression that creates it, as in
     908                 :     `co_await run(ex, alloc)(task())`: the task's frame is allocated from
     909                 :     memory the wrapper holds until the end of that expression.
     910                 : 
     911                 :     @ref run(Ex) describes teardown and preconditions.
     912                 : 
     913                 :     @param ex The executor on which the task should run.
     914                 :     @param alloc The allocator for frame allocation.
     915                 : 
     916                 :     @return A wrapper that accepts a task for execution.
     917                 : 
     918                 :     @see @ref run(Ex)
     919                 : */
     920                 : template<Executor Ex, detail::Allocator Alloc>
     921                 : [[nodiscard]] auto
     922               3 : run(Ex ex, Alloc alloc)
     923                 : {
     924                 :     return detail::run_wrapper_ex<Ex, true, Alloc>{
     925               3 :         std::move(ex), std::move(alloc)};
     926                 : }
     927                 : 
     928                 : /** Bind a task to an executor with stop token and memory resource.
     929                 : 
     930                 :     @ref run(Ex) describes teardown and preconditions.
     931                 : 
     932                 :     @param ex The executor on which the task should run.
     933                 :     @param st The stop token for cooperative cancellation.
     934                 :     @param mr The memory resource for frame allocation.
     935                 : 
     936                 :     @return A wrapper that accepts a task for execution.
     937                 : 
     938                 :     @see @ref run(Ex)
     939                 : */
     940                 : template<Executor Ex>
     941                 : [[nodiscard]] auto
     942               1 : run(Ex ex, std::stop_token st, std::pmr::memory_resource* mr)
     943                 : {
     944                 :     return detail::run_wrapper_ex<Ex, false, std::pmr::memory_resource*>{
     945               1 :         std::move(ex), std::move(st), mr};
     946                 : }
     947                 : 
     948                 : /** Bind a task to an executor with stop token and standard allocator.
     949                 : 
     950                 :     Await the awaitable in the expression that creates it, as in
     951                 :     `co_await run(ex, alloc)(task())`: the task's frame is allocated from
     952                 :     memory the wrapper holds until the end of that expression.
     953                 : 
     954                 :     @ref run(Ex) describes teardown and preconditions.
     955                 : 
     956                 :     @param ex The executor on which the task should run.
     957                 :     @param st The stop token for cooperative cancellation.
     958                 :     @param alloc The allocator for frame allocation.
     959                 : 
     960                 :     @return A wrapper that accepts a task for execution.
     961                 : 
     962                 :     @see @ref run(Ex)
     963                 : */
     964                 : template<Executor Ex, detail::Allocator Alloc>
     965                 : [[nodiscard]] auto
     966               1 : run(Ex ex, std::stop_token st, Alloc alloc)
     967                 : {
     968                 :     return detail::run_wrapper_ex<Ex, false, Alloc>{
     969               1 :         std::move(ex), std::move(st), std::move(alloc)};
     970                 : }
     971                 : 
     972                 : /** Run a task with a custom stop token.
     973                 : 
     974                 :     The task inherits the caller's executor. Only the stop token
     975                 :     is overridden.
     976                 : 
     977                 :     @par Example
     978                 :     @par !example example_1
     979                 : 
     980                 : 
     981                 :     @param st The stop token for cooperative cancellation.
     982                 : 
     983                 :     @return A wrapper that accepts a task for execution.
     984                 : */
     985                 : [[nodiscard]] inline auto
     986               5 : run(std::stop_token st)
     987                 : {
     988               5 :     return detail::run_wrapper<false, void>{std::move(st)};
     989                 : }
     990                 : 
     991                 : /** Run a task with a custom memory resource.
     992                 : 
     993                 :     The task inherits the caller's executor. The memory resource
     994                 :     is used for nested frame allocations.
     995                 : 
     996                 :     @param mr The memory resource for frame allocation.
     997                 : 
     998                 :     @return A wrapper that accepts a task for execution.
     999                 : */
    1000                 : [[nodiscard]] inline auto
    1001              13 : run(std::pmr::memory_resource* mr)
    1002                 : {
    1003              13 :     return detail::run_wrapper<true, std::pmr::memory_resource*>{mr};
    1004                 : }
    1005                 : 
    1006                 : // MrDocs 0.8.0 reports `run: Documented parameter 'alloc' does not exist` and
    1007                 : // pins it to the run(Ex) overload near the top of this namespace:
    1008                 : // `detail::Allocator` is implementation-defined, so the constraint is erased
    1009                 : // and the two single-parameter `run` overloads collapse onto one page. The
    1010                 : // finding is grandfathered in doc/lint/baseline.json. Deleting the `@param
    1011                 : // alloc` below clears it and produces no replacement finding, because MrDocs
    1012                 : // has no model of this overload at all -- so it would silently drop real
    1013                 : // documentation. Keep the `@param`.
    1014                 : /** Run a task with a custom standard allocator.
    1015                 : 
    1016                 :     Await the awaitable in the expression that creates it, as in
    1017                 :     `co_await run(alloc)(task())`: the task's frame is allocated from
    1018                 :     memory the wrapper holds until the end of that expression.
    1019                 : 
    1020                 :     The task inherits the caller's executor. The allocator is used
    1021                 :     for nested frame allocations.
    1022                 : 
    1023                 :     @param alloc The allocator for frame allocation.
    1024                 : 
    1025                 :     @return A wrapper that accepts a task for execution.
    1026                 : */
    1027                 : template<detail::Allocator Alloc>
    1028                 : [[nodiscard]] auto
    1029               3 : run(Alloc alloc)
    1030                 : {
    1031               3 :     return detail::run_wrapper<true, Alloc>{std::move(alloc)};
    1032                 : }
    1033                 : 
    1034                 : /** Run a task with stop token and memory resource.
    1035                 : 
    1036                 :     The task inherits the caller's executor.
    1037                 : 
    1038                 :     @param st The stop token for cooperative cancellation.
    1039                 :     @param mr The memory resource for frame allocation.
    1040                 : 
    1041                 :     @return A wrapper that accepts a task for execution.
    1042                 : */
    1043                 : [[nodiscard]] inline auto
    1044               1 : run(std::stop_token st, std::pmr::memory_resource* mr)
    1045                 : {
    1046                 :     return detail::run_wrapper<false, std::pmr::memory_resource*>{
    1047               1 :         std::move(st), mr};
    1048                 : }
    1049                 : 
    1050                 : /** Run a task with stop token and standard allocator.
    1051                 : 
    1052                 :     Await the awaitable in the expression that creates it, as in
    1053                 :     `co_await run(alloc)(task())`: the task's frame is allocated from
    1054                 :     memory the wrapper holds until the end of that expression.
    1055                 : 
    1056                 :     The task inherits the caller's executor.
    1057                 : 
    1058                 :     @param st The stop token for cooperative cancellation.
    1059                 :     @param alloc The allocator for frame allocation.
    1060                 : 
    1061                 :     @return A wrapper that accepts a task for execution.
    1062                 : */
    1063                 : template<detail::Allocator Alloc>
    1064                 : [[nodiscard]] auto
    1065               1 : run(std::stop_token st, Alloc alloc)
    1066                 : {
    1067                 :     return detail::run_wrapper<false, Alloc>{
    1068               1 :         std::move(st), std::move(alloc)};
    1069                 : }
    1070                 : 
    1071                 : } // namespace boost::capy
    1072                 : 
    1073                 : #endif
        

Generated by: LCOV version 2.3