[NFC] Refactor delta debugging to use coroutines#8657
Conversation
Add a generator utility in a new support/coroutine.h header and use it to refactor away the callback in the delta debugging utility. Now the utility is a struct providing access to the test and working sets as well as `accept()` and `reject()` methods that cause the test and working sets to be updated appropriately. Rather than being refactored into an explicit state machine, the implementation of the delta debugging algorithm remains readable straight-line code the does a co_yield whenever it is ready to return control to the user. It co_yields a pointer to local state object that exposes all the information that the delta debugging utility exposes in its public API. This local object stays alive across suspend points. When the delta debugging algorithm is complete, we suspend the coroutine one final time and make sure never to resume it, which ensures the state remains alive and available after delta debugging has finished. It will ultimately be cleaned up when the outer `DeltaDebugger` struct is cleaned up.
|
This is an alternative to #8651. It took some iteration, but I'm pretty happy with how it turned out. The arcane C++ coroutine nonsense is pretty well encapsulated in coroutine.h and the delta debugging implementation is essentially just as readable as before. |
|
What about compiler support for coroutines - https://en.cppreference.com/cpp/compiler_support suggests clang on windows may not be done yet, but perhaps that page is out of date? |
|
Looks like this is still a problem :( https://clang.llvm.org/cxx_status.html#:~:text=Clang%2017-,Coroutines,-P0912R5. But I think the ABI problem only affects 32-bit x86, and it doesn't look like we do any 32-bit releases at all. Maybe this is good enough for us? Here's the relevant LLVM bug: llvm/llvm-project#59382 |
|
If it only affects 32-bit windows I think we are ok here. But the wording in those links is a little ambiguous to me if that is the case? |
|
The opening post on the LLVM issue says "The 32-bit Windows ABI passes objects of non-trivially-copyable class type by value on the stack" and never mentions any other problems, so I think we're good. (And I asked an expert internally and he confirmed this understanding.) |
kripken
left a comment
There was a problem hiding this comment.
Sounds good about compiler support!
| if (working.empty()) { | ||
| finished = true; | ||
| co_yield &state; | ||
| co_return; |
There was a problem hiding this comment.
Why does this need to yield before returning? Isn't the output in the right place already?
There was a problem hiding this comment.
A tricky thing here is that we need to prevent the coroutine from ever returning because we depend on its local state staying live for the lifetime of the outer DeltaDebugger. So we yield before returning here and below, then make sure we never resume the coroutine again.
There was a problem hiding this comment.
I see, thanks, that's what I was missing. Please document that, it is indeed tricky...
There was a problem hiding this comment.
Or, could we std::move the final state from the coroutine?
There was a problem hiding this comment.
Unfortunately there is not a great way to do that. This is by far the simplest approach I tried. Will add comments.
| return false; | ||
| } | ||
| PromiseType* await_resume() const noexcept { return promise; } | ||
| }; |
There was a problem hiding this comment.
Maybe add some docs for these classes? I'm not really sure what "GetPromise" means or does just from this code (which seems so generic as to do almost nothing but store a "promise"..?)
There was a problem hiding this comment.
All these methods are well-known to the compiler and configure the suspending and resuming behavior of our Generator utility. Unfortunately this is just a bunch of unavoidable boilerplate that doesn't do anything interesting (or comprehensible to non-experts). I'll document the interesting user-exposed methods, but for most of this there's not anything more to say than // Unavoidable boilerplate.
Add a generator utility in a new support/coroutine.h header and use it to refactor away the callback in the delta debugging utility. Now the utility is a struct providing access to the test and working sets as well as
accept()andreject()methods that cause the test and working sets to be updated appropriately. Rather than being refactored into an explicit state machine, the implementation of the delta debugging algorithm remains readable straight-line code the does a co_yield whenever it is ready to return control to the user. It co_yields a pointer to local state object that exposes all the information that the delta debugging utility exposes in its public API. This local object stays alive across suspend points. When the delta debugging algorithm is complete, we suspend the coroutine one final time and make sure never to resume it, which ensures the state remains alive and available after delta debugging has finished. It will ultimately be cleaned up when the outerDeltaDebuggerstruct is cleaned up.