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 % 271 271
Test Date: 2026-10-16 22:02:34 Functions: 88.4 % 319 282 37

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

Generated by: LCOV version 2.3