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-20 22:20:34 Functions: 88.4 % 319 282 37

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

Generated by: LCOV version 2.3