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