@@ -99,43 +99,110 @@ struct Goal : public std::enable_shared_from_this<Goal>
9999 */
100100 ExitCode exitCode = ecBusy;
101101
102- public :
102+ protected :
103103 /* *
104104 * Build result.
105105 */
106106 BuildResult buildResult;
107+ public:
107108
108- /*
109- * Suspend our goal and wait until we get work()-ed again.
109+ /* *
110+ * Suspend our goal and wait until we get @ref work()-ed again.
111+ * `co_await`-able by @ref Co.
110112 */
111113 struct SuspendGoal {};
114+
115+ /* *
116+ * Return from the current coroutine and suspend our goal
117+ * if we're not busy anymore, or jump to the next coroutine
118+ * set to be executed/resumed.
119+ */
112120 struct Return {};
121+
122+ // forward declaration of promise_type, see below
113123 struct promise_type ;
124+
125+ /* *
126+ * Handle to coroutine using @ref Co and @ref promise_type.
127+ */
114128 using handle_type = std::coroutine_handle<promise_type>;
115- // FIXME: Allocate explicitly on stack since HALO thing doesn't really work,
116- // specifically, there's no way to uphold the requirements when trying to do
117- // tail-calls without using a trampoline AFAICT.
118- // NOTES:
119- // These are good resources for understanding how coroutines work:
120- // https://lewissbaker.github.io/
121- // https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/coroutines-c++20/
122- // https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html
129+
130+ /* *
131+ * C++20 coroutine wrapper for use in goal logic.
132+ * Coroutines are functions that use `co_await`/`co_return` (and `co_yield`, but not supported by @ref Co).
133+ *
134+ * @ref Co is meant to be used by methods of subclasses of @ref Goal.
135+ * The main functionality provided by `Co` is
136+ * - `co_await SuspendGoal{}`: Suspends the goal.
137+ * - `co_await f()`: Waits until `f()` finishes.
138+ * - `co_return f()`: Tail-calls `f()`.
139+ * - `co_return Return{}`: Ends coroutine.
140+ *
141+ * The idea is that you implement the goal logic using coroutines,
142+ * and do the core thing a goal can do, suspension, when you have
143+ * children you're waiting for.
144+ * Coroutines allow you to resume the work cleanly.
145+ *
146+ * @note Below follows a brief explanation of C++20 coroutines.
147+ * When you `Co f()`, a `std::coroutine_handle<promise_type>` is created,
148+ * alongside its @ref promise_type.
149+ * There are suspension points at the beginning of the coroutine,
150+ * at every `co_await`, and at the final (possibly implicit) `co_return`.
151+ * Once suspended, you can resume the `std::coroutine_handle` by doing `coroutine_handle.resume()`.
152+ * Suspension points are implemented by passing a struct to the compiler
153+ * that implements `await_sus`pend.
154+ * `await_suspend` can either say "cancel suspension", in which case execution resumes,
155+ * "suspend", in which case control is passed back to the caller of `coroutine_handle.resume()`
156+ * or the place where the coroutine function is initially executed in the case of the initial
157+ * suspension, or `await_suspend` can specify another coroutine to jump to, which is
158+ * how tail calls are implemented.
159+ *
160+ * @note Resources:
161+ * - https://lewissbaker.github.io/
162+ * - https://www.chiark.greenend.org.uk/~sgtatham/quasiblog/coroutines-c++20/
163+ * - https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html
164+ *
165+ * @todo Allocate explicitly on stack since HALO thing doesn't really work,
166+ * specifically, there's no way to uphold the requirements when trying to do
167+ * tail-calls without using a trampoline AFAICT.
168+ */
123169 struct [[nodiscard]] Co {
170+ /* *
171+ * The underlying handle.
172+ */
124173 handle_type handle;
174+
125175 explicit Co (handle_type handle) : handle (handle) {};
126- Co (const Co&) = delete ;
127- Co &operator =(const Co&) = delete ;
128176 void operator =(Co&&);
129177 Co (Co&& rhs);
130178 ~Co ();
131179
132180 bool await_ready () { return false ; };
181+ /* *
182+ * When we `co_await` another @ref Co-returning coroutine,
183+ * we tell the caller of `caller_coroutine.resume()` to switch to our coroutine (@ref handle).
184+ * To make sure we return to the original coroutine, we set it as the continuation of our
185+ * coroutine. In @ref promise_type::final_awaiter we check if it's set and if so we return to it.
186+ *
187+ * To explain in more understandable terms:
188+ * When we `co_await Co_returning_function()`, this function is called on the resultant @ref Co of
189+ * the _called_ function, and C++ automatically passes the caller in.
190+ *
191+ * `goal` field of @ref promise_type is also set here by copying it from the caller.
192+ */
133193 std::coroutine_handle<> await_suspend (handle_type handle);
134194 void await_resume () {};
135195 };
136- // Used on initial suspend, doesn't do anything useful,
137- // but asserts that everything has been set correctly.
196+
197+ /* *
198+ * Used on initial suspend, does the same as @ref std::suspend_always,
199+ * but asserts that everything has been set correctly.
200+ */
138201 struct InitialSuspend {
202+ /* *
203+ * Handle of coroutine that does the
204+ * initial suspend
205+ */
139206 handle_type handle;
140207
141208 bool await_ready () { return false ; };
@@ -144,51 +211,132 @@ public:
144211 }
145212 void await_resume () {
146213 assert (handle);
147- assert (handle.promise ().goal ); // Caller must have set our goal
148- assert (handle.promise ().goal ->top_co ); // Caller must have set top_co
149- assert (handle.promise ().goal ->top_co ->handle == handle); // top_co must be us
214+ assert (handle.promise ().goal ); // goal must be set
215+ assert (handle.promise ().goal ->top_co ); // top_co of goal must be set
216+ assert (handle.promise ().goal ->top_co ->handle == handle); // top_co of goal must be us
150217 }
151- };;
218+ };
219+
220+ /* *
221+ * Promise type for coroutines defined using @ref Co.
222+ * Attached to coroutine handle.
223+ */
152224 struct promise_type {
153- // Either this is who called us, or it is who we will tail-call.
154- // It is what we "jump" to once we are done.
225+ /* *
226+ * Either this is who called us, or it is who we will tail-call.
227+ * It is what we "jump" to once we are done.
228+ */
155229 std::optional<Co> continuation;
230+
231+ /* *
232+ * The goal that we're a part of.
233+ * Set either in @ref Co::await_suspend or in constructor of @ref Goal.
234+ */
156235 Goal* goal = nullptr ;
157- bool alive = true ;
158236
159- promise_type () {}
237+ /* *
238+ * Is set to false when destructed to ensure we don't use a
239+ * destructed coroutine by accident
240+ */
241+ bool alive = true ;
160242
243+ /* *
244+ * The awaiter used by @ref final_suspend.
245+ */
161246 struct final_awaiter {
162- bool await_ready () noexcept { return false ; };;
163- std::coroutine_handle<> await_suspend (handle_type) noexcept ;
247+ bool await_ready () noexcept { return false ; };
248+ /* *
249+ * Here we execute our continuation, by passing it back to the caller.
250+ * C++ compiler will create code that takes that and executes it promptly.
251+ * `h` is the handle for the coroutine that is finishing execution,
252+ * thus it must be destroyed.
253+ */
254+ std::coroutine_handle<> await_suspend (handle_type h) noexcept ;
164255 void await_resume () noexcept { assert (false ); };
165256 };
257+
258+ /* *
259+ * Called by compiler generated code to construct the @ref Co
260+ * that is returned from a @ref Co-returning coroutine.
261+ */
166262 Co get_return_object ();
167- InitialSuspend initial_suspend () {
168- // top_co isn't set to us yet,
169- // we've merely constructed the frame and now the
170- // caller is free to do whatever they wish to us.
171- return {};
172- };
263+
264+ /* *
265+ * Called by compiler generated code before body of coroutine.
266+ * We use this opportunity to set the @ref goal field
267+ * and `top_co` field of @ref Goal.
268+ */
269+ InitialSuspend initial_suspend () { return {}; };
270+
271+ /* *
272+ * Called on `co_return`. Creates @ref final_awaiter which
273+ * either jumps to continuation or suspends goal.
274+ */
173275 final_awaiter final_suspend () noexcept { return {}; };
276+
277+ /* *
278+ * Does nothing, but provides an opportunity for
279+ * @ref final_suspend to happen.
280+ */
174281 void return_value (Return) {}
282+
283+ /* *
284+ * When "returning" another coroutine, what happens is that
285+ * we set it as our own continuation, thus once the final suspend
286+ * happens, we transfer control to it.
287+ * The original continuation we had is set as the continuation
288+ * of the coroutine passed in.
289+ * @ref final_suspend is called after this, and @ref final_awaiter will
290+ * pass control off to @ref continuation.
291+ *
292+ * If we already have a continuation, that continuation is set as
293+ * the continuation of the new continuation. Thus, the continuation
294+ * passed to @ref return_value must not have a continuation set.
295+ */
175296 void return_value (Co&&);
297+
298+ /* *
299+ * If an exception is thrown inside a coroutine,
300+ * we re-throw it in the context of the "resumer" of the continuation.
301+ */
176302 void unhandled_exception () { throw ; };
177303
304+ /* *
305+ * Allows awaiting a @ref Co.
306+ */
178307 Co&& await_transform(Co&& co) { return static_cast <Co&&>(co); }
308+
309+ /* *
310+ * Allows awaiting a @ref SuspendGoal.
311+ * Always suspends.
312+ */
179313 std::suspend_always await_transform (SuspendGoal) { return {}; };
180314 };
315+
181316 /* *
182317 * The coroutine being currently executed.
183- * You MUST update this when switching the coroutine being executed!
318+ * MUST be updated when switching the coroutine being executed.
184319 * This is used both for memory management and to resume the last
185320 * coroutine executed.
321+ * Destroying this should destroy all coroutines created for this goal.
186322 */
187323 std::optional<Co> top_co;
188324
325+ /* *
326+ * The entry point for the goal
327+ */
189328 virtual Co init () = 0;
329+
330+ /* *
331+ * Wrapper around @ref init since virtual functions
332+ * can't be used in constructors.
333+ */
190334 inline Co init_wrapper ();
191335
336+ /* *
337+ * Signals that the goal is done.
338+ * The coroutine that is returned will suspend eternally.
339+ */
192340 Co amDone (ExitCode result, std::optional<Error> ex = {});
193341
194342 virtual void cleanup () { }
@@ -213,7 +361,9 @@ public:
213361 Goal (Worker & worker, DerivedPath path)
214362 : worker(worker), top_co(init_wrapper())
215363 {
216- // assert(!top_co->handle.promise().goal);
364+ // top_co shouldn't have a goal already, should be nullptr.
365+ assert (!top_co->handle .promise ().goal );
366+ // we set it such that top_co can pass it down to its subcoroutines.
217367 top_co->handle .promise ().goal = this ;
218368 }
219369
0 commit comments