Skip to content

Commit 6f87de6

Browse files
committed
Doc comments in goal.hh
From feedback from @Ericson2314. Comments from goal.cc have been moved to goal.hh and made doc comments. Unnecessary lines of code that don't affect functionality have also been removed. Commented out assertion was uncommented-out.
1 parent f46a915 commit 6f87de6

File tree

2 files changed

+184
-55
lines changed

2 files changed

+184
-55
lines changed

src/libstore/build/goal.cc

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,7 @@ Co promise_type::get_return_object() {
2727
auto handle = handle_type::from_promise(*this);
2828
return Co{handle};
2929
};
30-
// Here we execute our continuation, by passing it back to the caller.
31-
// C++ compiler will create code that takes that and executes it promptly.
32-
// `h` is the handle for the coroutine that is finishing execution,
33-
// thus it must be destroyed.
30+
3431
std::coroutine_handle<> promise_type::final_awaiter::await_suspend(handle_type h) noexcept {
3532
auto& p = h.promise();
3633
assert(p.goal);
@@ -56,15 +53,6 @@ std::coroutine_handle<> promise_type::final_awaiter::await_suspend(handle_type h
5653
}
5754
}
5855

59-
// When "returning" another coroutine, what happens is that
60-
// we set it as our own continuation, thus once the final suspend
61-
// happens, we transfer control to it.
62-
// The original continuation we had is set as the continuation
63-
// of the coroutine passed in.
64-
// `final_suspend` is called after this, and `final_awaiter` will pass control off to `continuation`.
65-
// However, we also have to transfer the ownership of `next`, since it's an rvalue,
66-
// the handle to which is on our stack.
67-
// We thus give it to our previous continuation.
6856
void promise_type::return_value(Co&& next) {
6957
goal->trace("return_value(Co&&)");
7058
// we save our old continuation
@@ -81,15 +69,6 @@ void promise_type::return_value(Co&& next) {
8169
continuation->handle.promise().continuation = std::move(old_continuation);
8270
}
8371

84-
// When we `co_await` another `Co`-returning coroutine,
85-
// we tell the caller of `caller.resume()` to switch to our coroutine (`handle`).
86-
// To make sure we return to the original coroutine, we set it as the continuation of our
87-
// coroutine. In `final_awaiter` we check if it's set and if so we return to it.
88-
//
89-
// To explain in more understandable terms:
90-
// When we `co_await Co_returning_function()`, this function is called on the resultant Co of
91-
// the _called_ function, and C++ automatically passes the caller in.
92-
// We don't use this caller, because we make use of the invariant that top_co == caller.
9372
std::coroutine_handle<> nix::Goal::Co::await_suspend(handle_type caller) {
9473
assert(handle); // we must be a valid coroutine
9574
auto& p = handle.promise();

src/libstore/build/goal.hh

Lines changed: 183 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)