Skip to content

Commit fa7dd47

Browse files
committed
[NFC] Refactor delta debugging to use coroutines
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.
1 parent df8b79d commit fa7dd47

4 files changed

Lines changed: 463 additions & 192 deletions

File tree

src/support/coroutine.h

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright 2026 WebAssembly Community Group participants
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#ifndef wasm_support_coroutine_h
18+
#define wasm_support_coroutine_h
19+
20+
#include <coroutine>
21+
#include <exception>
22+
23+
namespace wasm {
24+
25+
template<typename PromiseType> struct GetPromise {
26+
PromiseType* promise = nullptr;
27+
bool await_ready() const noexcept { return false; }
28+
bool await_suspend(std::coroutine_handle<PromiseType> h) noexcept {
29+
promise = &h.promise();
30+
return false;
31+
}
32+
PromiseType* await_resume() const noexcept { return promise; }
33+
};
34+
35+
template<typename T, typename U = void> struct Generator;
36+
37+
// One-way generator
38+
template<typename T> struct Generator<T, void> {
39+
struct promise_type {
40+
T current_value;
41+
42+
Generator get_return_object() {
43+
return {std::coroutine_handle<promise_type>::from_promise(*this)};
44+
}
45+
std::suspend_always initial_suspend() { return {}; }
46+
std::suspend_always final_suspend() noexcept { return {}; }
47+
void unhandled_exception() { std::terminate(); }
48+
void return_void() {}
49+
50+
std::suspend_always yield_value(T value) {
51+
current_value = std::move(value);
52+
return {};
53+
}
54+
};
55+
56+
std::coroutine_handle<promise_type> handle;
57+
58+
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
59+
Generator(const Generator&) = delete;
60+
Generator(Generator&& other) noexcept : handle(other.handle) {
61+
other.handle = nullptr;
62+
}
63+
~Generator() {
64+
if (handle) {
65+
handle.destroy();
66+
}
67+
}
68+
69+
bool next() {
70+
handle.resume();
71+
return !handle.done();
72+
}
73+
74+
T& get() { return handle.promise().current_value; }
75+
const T& get() const { return handle.promise().current_value; }
76+
};
77+
78+
// Two-way generator
79+
template<typename T, typename U> struct Generator {
80+
struct promise_type {
81+
T current_value;
82+
U received_value;
83+
84+
Generator get_return_object() {
85+
return {std::coroutine_handle<promise_type>::from_promise(*this)};
86+
}
87+
std::suspend_always initial_suspend() { return {}; }
88+
std::suspend_always final_suspend() noexcept { return {}; }
89+
void unhandled_exception() { std::terminate(); }
90+
void return_void() {}
91+
92+
auto yield_value(T value) {
93+
current_value = std::move(value);
94+
return YieldAwaiter{this};
95+
}
96+
97+
struct YieldAwaiter {
98+
promise_type* p;
99+
bool await_ready() const noexcept { return false; }
100+
void await_suspend(std::coroutine_handle<promise_type>) noexcept {}
101+
U await_resume() const noexcept { return p->received_value; }
102+
};
103+
};
104+
105+
std::coroutine_handle<promise_type> handle;
106+
107+
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
108+
Generator(const Generator&) = delete;
109+
Generator(Generator&& other) noexcept : handle(other.handle) {
110+
other.handle = nullptr;
111+
}
112+
~Generator() {
113+
if (handle) {
114+
handle.destroy();
115+
}
116+
}
117+
118+
bool resume(U value) {
119+
handle.promise().received_value = std::move(value);
120+
handle.resume();
121+
return !handle.done();
122+
}
123+
124+
T& get() { return handle.promise().current_value; }
125+
const T& get() const { return handle.promise().current_value; }
126+
};
127+
128+
} // namespace wasm
129+
130+
#endif // wasm_support_coroutine_h

src/support/delta_debugging.h

Lines changed: 123 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,100 +21,152 @@
2121
#include <cassert>
2222
#include <vector>
2323

24+
#include "support/coroutine.h"
25+
2426
namespace wasm {
2527

26-
// Use the delta debugging algorithm (Zeller 1999,
27-
// https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of
28-
// items necessary to preserve some property. Returns that minimal set of
29-
// items, preserving their input order. `tryPartition` should have this
30-
// signature:
31-
//
32-
// bool tryPartition(size_t partitionIndex,
33-
// size_t numPartitions,
34-
// const std::vector<T>& partition)
35-
//
36-
// It should return true iff the property is preserved while keeping only
37-
// `partition` items.
38-
template<typename T, typename F>
39-
std::vector<T> deltaDebugging(std::vector<T> items, const F& tryPartition) {
40-
if (items.empty()) {
41-
return items;
42-
}
43-
// First try removing everything.
44-
if (tryPartition(0, 1, {})) {
45-
return {};
28+
// Use the delta debugging algorithm (Zeller 2002,
29+
// https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of items
30+
// necessary to preserve some property. `working` is the minimal set of items
31+
// found so far and `test` is the smaller set of items that should be tested
32+
// next. After testing, call `accept()`, `reject()`, or `resolve(bool accepted)`
33+
// to update the working and test sets appropriately.
34+
template<typename T> struct DeltaDebugger {
35+
DeltaDebugger(std::vector<T> items) : task(run(std::move(items))) {
36+
task.handle.resume();
4637
}
47-
size_t numPartitions = 2;
48-
while (numPartitions <= items.size()) {
49-
// Partition the items.
50-
std::vector<std::vector<T>> partitions;
51-
size_t size = items.size();
52-
size_t basePartitionSize = size / numPartitions;
53-
size_t rem = size % numPartitions;
54-
size_t idx = 0;
55-
for (size_t i = 0; i < numPartitions; ++i) {
56-
size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0);
57-
if (partitionSize > 0) {
58-
std::vector<T> partition;
59-
partition.reserve(partitionSize);
60-
for (size_t j = 0; j < partitionSize; ++j) {
61-
partition.push_back(items[idx++]);
62-
}
63-
partitions.emplace_back(std::move(partition));
64-
}
38+
39+
bool finished() const { return task.get()->finished; }
40+
41+
const std::vector<T>& working() const { return task.get()->working; }
42+
std::vector<T>& test() { return task.get()->test; }
43+
44+
size_t partitionCount() const { return task.get()->numPartitions; }
45+
size_t partitionIndex() const { return task.get()->currPartition; }
46+
47+
void resolve(bool success) {
48+
if (finished()) {
49+
return;
6550
}
66-
assert(numPartitions == partitions.size());
51+
task.resume(success);
52+
}
6753

68-
bool reduced = false;
54+
void accept() { resolve(true); }
55+
void reject() { resolve(false); }
6956

70-
// Try keeping only one partition. Try each partition in turn.
71-
for (size_t i = 0; i < numPartitions; ++i) {
72-
if (tryPartition(i, numPartitions, partitions[i])) {
73-
items = std::move(partitions[i]);
74-
numPartitions = 2;
75-
reduced = true;
76-
break;
77-
}
57+
private:
58+
struct State {
59+
std::vector<T> working;
60+
std::vector<T> test;
61+
size_t numPartitions = 1;
62+
size_t currPartition = 0;
63+
bool finished = false;
64+
};
65+
66+
Generator<State*, bool> task;
67+
68+
static Generator<State*, bool> run(std::vector<T> items) {
69+
State state;
70+
auto& [working, test, numPartitions, currPartition, finished] = state;
71+
72+
working = std::move(items);
73+
74+
if (working.empty()) {
75+
finished = true;
76+
co_yield &state;
77+
co_return;
7878
}
79-
if (reduced) {
80-
continue;
79+
80+
// First try removing everything.
81+
if (co_yield &state) {
82+
working = {};
83+
finished = true;
84+
co_yield &state;
85+
co_return;
8186
}
8287

83-
// Otherwise, try keeping the complement of a partition. Do not do this with
84-
// only two partitions because that would be no different from what we
85-
// already tried.
86-
if (numPartitions > 2) {
88+
numPartitions = 2;
89+
while (numPartitions <= working.size()) {
90+
// Partition the items.
91+
std::vector<std::vector<T>> partitions;
92+
size_t size = working.size();
93+
size_t basePartitionSize = size / numPartitions;
94+
size_t rem = size % numPartitions;
95+
size_t idx = 0;
8796
for (size_t i = 0; i < numPartitions; ++i) {
88-
std::vector<T> complement;
89-
complement.reserve(items.size() - partitions[i].size());
90-
for (size_t j = 0; j < numPartitions; ++j) {
91-
if (j != i) {
92-
complement.insert(
93-
complement.end(), partitions[j].begin(), partitions[j].end());
97+
size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0);
98+
if (partitionSize > 0) {
99+
std::vector<T> partition;
100+
partition.reserve(partitionSize);
101+
for (size_t j = 0; j < partitionSize; ++j) {
102+
partition.push_back(working[idx++]);
94103
}
104+
partitions.emplace_back(std::move(partition));
95105
}
96-
if (tryPartition(i, numPartitions, complement)) {
97-
items = std::move(complement);
98-
numPartitions = std::max(numPartitions - 1, size_t(2));
106+
}
107+
assert(numPartitions == partitions.size());
108+
109+
bool reduced = false;
110+
111+
// Try keeping only one partition. Try each partition in turn.
112+
for (currPartition = 0; currPartition < numPartitions; ++currPartition) {
113+
test = std::move(partitions[currPartition]);
114+
if (co_yield &state) {
115+
working = std::move(test);
116+
numPartitions = 2;
99117
reduced = true;
100118
break;
119+
} else {
120+
// Restore the partition since we failed and might need it for
121+
// complement testing.
122+
partitions[currPartition] = std::move(test);
101123
}
102124
}
103125
if (reduced) {
104126
continue;
105127
}
106-
}
107128

108-
if (numPartitions == items.size()) {
109-
// Cannot further refine the partitions. We're done.
110-
break;
129+
// Otherwise, try keeping the complement of a partition. Do not do this
130+
// with only two partitions because that would be no different from what
131+
// we already tried.
132+
if (numPartitions > 2) {
133+
for (currPartition = 0; currPartition < numPartitions;
134+
++currPartition) {
135+
test.clear();
136+
test.reserve(working.size() - partitions[currPartition].size());
137+
for (size_t i = 0; i < numPartitions; ++i) {
138+
if (i != currPartition) {
139+
test.insert(
140+
test.end(), partitions[i].begin(), partitions[i].end());
141+
}
142+
}
143+
if (co_yield &state) {
144+
working = std::move(test);
145+
numPartitions = std::max(numPartitions - 1, size_t(2));
146+
reduced = true;
147+
break;
148+
}
149+
}
150+
if (reduced) {
151+
continue;
152+
}
153+
}
154+
155+
if (numPartitions == working.size()) {
156+
// Cannot further refine the partitions. We're done.
157+
break;
158+
}
159+
160+
// Otherwise, make the partitions finer grained.
161+
numPartitions = std::min(working.size(), 2 * numPartitions);
111162
}
112163

113-
// Otherwise, make the partitions finer grained.
114-
numPartitions = std::min(items.size(), 2 * numPartitions);
164+
// Yield final state
165+
test = {};
166+
finished = true;
167+
co_yield &state;
115168
}
116-
return items;
117-
}
169+
};
118170

119171
} // namespace wasm
120172

0 commit comments

Comments
 (0)