100.00% Lines (25/25) 100.00% Functions (7/7)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2026 Steve Gerbino 2   // Copyright (c) 2026 Steve Gerbino
  3 + // Copyright (c) 2026 Michael Vandeberg
3   // 4   //
4   // Distributed under the Boost Software License, Version 1.0. (See accompanying 5   // Distributed under the Boost Software License, Version 1.0. (See accompanying
5   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6   // 7   //
7   // Official repository: https://github.com/cppalliance/capy 8   // Official repository: https://github.com/cppalliance/capy
8   // 9   //
9   10  
10   #ifndef BOOST_CAPY_EX_ABANDON_HPP 11   #ifndef BOOST_CAPY_EX_ABANDON_HPP
11   #define BOOST_CAPY_EX_ABANDON_HPP 12   #define BOOST_CAPY_EX_ABANDON_HPP
12   13  
13   #include <boost/capy/detail/config.hpp> 14   #include <boost/capy/detail/config.hpp>
14   #include <boost/capy/continuation.hpp> 15   #include <boost/capy/continuation.hpp>
15   #include <boost/capy/ex/executor_ref.hpp> 16   #include <boost/capy/ex/executor_ref.hpp>
16   17  
17   #include <atomic> 18   #include <atomic>
18   #include <coroutine> 19   #include <coroutine>
19   20  
20   namespace boost { 21   namespace boost {
21   namespace capy { 22   namespace capy {
22   23  
23   /** The owner of one segment of a coroutine chain. 24   /** The owner of one segment of a coroutine chain.
24   25  
25   A segment is a run of coroutines started together: the task 26   A segment is a run of coroutines started together: the task
26   `run_async` launches and everything it awaits, a task `run` starts 27   `run_async` launches and everything it awaits, a task `run` starts
27 - on another executor, or one branch of `when_all` or `when_any`. Its 28 + on another executor, one branch of `when_all` or `when_any`, or a
  29 + @ref quitter awaited by a coroutine that is not a quitter. Its
28   owner is the coroutine that started it, suspended above the 30   owner is the coroutine that started it, suspended above the
29   segment's top frame until that frame completes. Every coroutine in 31   segment's top frame until that frame completes. Every coroutine in
30   the segment reaches the owner through its @ref await_context. 32   the segment reaches the owner through its @ref await_context.
31   33  
32   When the holder of a suspended coroutine in the segment gives it up 34   When the holder of a suspended coroutine in the segment gives it up
33   with @ref abandon, the owner posts its own continuation to its 35   with @ref abandon, the owner posts its own continuation to its
34   executor. There it unwinds the segment from the top: it destroys 36   executor. There it unwinds the segment from the top: it destroys
35 - the frame it awaits, which destroys the frames below it in turn, and 37 + the frame it awaits, which destroys the frames below it in turn.
36 - finishes without delivering a result. An executor that destroys the 38 + After this teardown, the owner delivers no result. An executor that
37 - posted continuation instead, as a closed one does, unwinds the 39 + destroys the posted continuation instead, as a closed one does,
38 - segment the same way. 40 + unwinds the segment the same way.
  41 +
  42 + The owner also records why the segment was abandoned. Teardown
  43 + calls @ref abandon. A stopped @ref quitter calls
  44 + @ref abandon_for_stop instead. Its owner unwinds the segment the
  45 + same way, then reports the stop to its parent. An owner that never
  46 + asks @ref stopped treats a stop as teardown: it delivers nothing. A
  47 + launcher written by a user must therefore check @ref stopped if it
  48 + runs quitters and has to report their stops.
39   49  
40   @par Thread Safety 50   @par Thread Safety
41 - @ref abandon may be called from any thread. Only one holder can 51 + @ref abandon and @ref abandon_for_stop may be called from any
42 - abandon a segment, since it is suspended at one point; a repeated 52 + thread. Only the first call takes effect, and decides the cause; a
43 - call before the unwind runs does nothing. 53 + later call of either before the unwind runs does nothing.
44   */ 54   */
45   class chain_owner 55   class chain_owner
46   { 56   {
47 - std::atomic<bool> abandoned_{false}; 57 + enum : unsigned char { not_abandoned, by_teardown, by_stop };
  58 +
  59 + std::atomic<unsigned char> cause_{not_abandoned};
48   continuation self_; 60   continuation self_;
49   executor_ref ex_; 61   executor_ref ex_;
50   62  
HITGNC   63 + 126211 void abandon_as(unsigned char cause) noexcept
  64 + {
HITGNC   65 + 126211 unsigned char expected = not_abandoned;
HITGNC   66 + 252422 if(!cause_.compare_exchange_strong(
  67 + expected, cause,
  68 + std::memory_order_acq_rel,
  69 + std::memory_order_acquire))
HITGNC   70 + 10002 return;
HITGNC   71 + 116209 ex_.post(self_);
  72 + }
  73 +
51   public: 74   public:
52   /** Bind the owner to its coroutine and executor. 75   /** Bind the owner to its coroutine and executor.
53   76  
54   Called by the owner before it starts the segment. 77   Called by the owner before it starts the segment.
55   78  
56   @param self The owner's coroutine, resumed or destroyed to 79   @param self The owner's coroutine, resumed or destroyed to
57   unwind the segment. 80   unwind the segment.
58   81  
59   @param ex The executor the segment runs on. 82   @param ex The executor the segment runs on.
60   */ 83   */
HITCBC 61   106453 void bind(std::coroutine_handle<> self, executor_ref ex) noexcept 84   136552 void bind(std::coroutine_handle<> self, executor_ref ex) noexcept
62   { 85   {
HITCBC 63   106453 self_.h = self; 86   136552 self_.h = self;
HITCBC 64   106453 ex_ = ex; 87   136552 ex_ = ex;
HITCBC 65   106453 } 88   136552 }
66   89  
67   /// Return true once the segment has been abandoned. 90   /// Return true once the segment has been abandoned.
HITCBC 68   7854 bool abandoned() const noexcept 91   32947 bool abandoned() const noexcept
69   { 92   {
HITCBC 70 - 7854 return abandoned_.load(std::memory_order_acquire); 93 + 65894 return cause_.load(std::memory_order_acquire) != not_abandoned;
  94 + }
  95 +
  96 + /** Return true if the segment was abandoned because of a stop.
  97 +
  98 + @return `true` only if the first abandon of the segment was
  99 + @ref abandon_for_stop.
  100 + */
HITGNC   101 + 30802 bool stopped() const noexcept
  102 + {
HITGNC   103 + 61604 return cause_.load(std::memory_order_acquire) == by_stop;
71   } 104   }
72   105  
73   /** Unwind the segment on the owner's executor. 106   /** Unwind the segment on the owner's executor.
74   107  
75   Does nothing if the segment was already abandoned. Otherwise 108   Does nothing if the segment was already abandoned. Otherwise
76   posts the owner's continuation; nothing of the owner is touched 109   posts the owner's continuation; nothing of the owner is touched
77   afterward, since the post may unwind and free it at once. 110   afterward, since the post may unwind and free it at once.
78   111  
79   @note A post that throws calls `std::terminate`. 112   @note A post that throws calls `std::terminate`.
80   */ 113   */
HITCBC 81   101381 void abandon() noexcept 114   111375 void abandon() noexcept
82   { 115   {
HITCBC 83 - 101381 if(abandoned_.exchange(true, std::memory_order_acq_rel)) 116 + 111375 abandon_as(by_teardown);
HITCBC 84 - 1 return; 117 + 111375 }
ECB 85 - 101380 ex_.post(self_); 118 +
  119 + /** Unwind the segment on the owner's executor, as stopped.
  120 +
  121 + The same as @ref abandon, but records the cause as a stop, so
  122 + @ref stopped returns `true` if this call was the first. Called
  123 + when a @ref quitter in the segment is stopped: at a `co_await`
  124 + in its body, when an operation completes into its segment's
  125 + gate, or when the gate through which `run_async` or `run`
  126 + starts a quitter does not start it.
  127 +
  128 + @note A post that throws calls `std::terminate`.
  129 + */
HITGNC   130 + 14836 void abandon_for_stop() noexcept
  131 + {
HITGNC   132 + 14836 abandon_as(by_stop);
HITGIC 86   } 133   14836 }
87   }; 134   };
88   135  
89   /** Give up a suspended coroutine for good. 136   /** Give up a suspended coroutine for good.
90   137  
91   Called by the holder of a suspended coroutine that will never resume 138   Called by the holder of a suspended coroutine that will never resume
92   it, such as an execution context, strand or primitive being torn 139   it, such as an execution context, strand or primitive being torn
93   down. The coroutine's segment is unwound from its owner: on the 140   down. The coroutine's segment is unwound from its owner: on the
94   owner's executor, from the top frame down to this coroutine, without 141   owner's executor, from the top frame down to this coroutine, without
95 - any frame delivering a result. Ordinary cancellation does not use 142 + any frame delivering a result. Ordinary cancellation of a @ref task
96 - this; it resumes the coroutine with an error instead. 143 + does not use this; it resumes the coroutine with an error instead.
  144 + A @ref quitter's stop does use abandonment, through
  145 + @ref chain_owner::abandon_for_stop.
97   146  
98   A continuation with no owner, such as an owner's own continuation, 147   A continuation with no owner, such as an owner's own continuation,
99   is destroyed directly. 148   is destroyed directly.
100   149  
101   @par Preconditions 150   @par Preconditions
102   `c` refers to a suspended coroutine that the caller holds and that 151   `c` refers to a suspended coroutine that the caller holds and that
103   nothing else will resume or abandon. If `c` has no owner, nothing 152   nothing else will resume or abandon. If `c` has no owner, nothing
104   awaits the coroutine: destroying its frame alone would leave any 153   awaits the coroutine: destroying its frame alone would leave any
105   awaiting frame holding a destroyed one. 154   awaiting frame holding a destroyed one.
106   155  
107   @par Postconditions 156   @par Postconditions
108   The caller must not touch the coroutine, or anything in its frame, 157   The caller must not touch the coroutine, or anything in its frame,
109   again. The frames may already be destroyed when this returns, if the 158   again. The frames may already be destroyed when this returns, if the
110   owner's executor destroys posted work at once. 159   owner's executor destroys posted work at once.
111   160  
112   @note The unwind runs on the owner's executor, which may belong to 161   @note The unwind runs on the owner's executor, which may belong to
113   another execution context than the caller's, and may run after the 162   another execution context than the caller's, and may run after the
114   caller's context is destroyed. A chain that holds objects of the 163   caller's context is destroyed. A chain that holds objects of the
115   caller's context must therefore have finished before that context 164   caller's context must therefore have finished before that context
116   is torn down. 165   is torn down.
117   166  
118   @note Each frame is destroyed from the destructor of the frame 167   @note Each frame is destroyed from the destructor of the frame
119   awaiting it, so the unwind uses stack in proportion to how deeply 168   awaiting it, so the unwind uses stack in proportion to how deeply
120   the chain's coroutines are nested. 169   the chain's coroutines are nested.
121   170  
122   @param c The continuation of the coroutine given up. 171   @param c The continuation of the coroutine given up.
123   */ 172   */
124   inline void 173   inline void
HITCBC 125   203405 abandon(continuation& c) noexcept 174   203389 abandon(continuation& c) noexcept
126   { 175   {
HITCBC 127   203405 if(c.owner) 176   203389 if(c.owner)
HITCBC 128   101379 c.owner->abandon(); 177   101372 c.owner->abandon();
HITCBC 129   102026 else if(c.h) 178   102017 else if(c.h)
HITCBC 130   102026 c.h.destroy(); 179   102017 c.h.destroy();
HITCBC 131   203405 } 180   203389 }
132   181  
133   } // namespace capy 182   } // namespace capy
134   } // namespace boost 183   } // namespace boost
135   184  
136   #endif 185   #endif