diff --git a/src/support/coroutine.h b/src/support/coroutine.h new file mode 100644 index 00000000000..0fc18d89fae --- /dev/null +++ b/src/support/coroutine.h @@ -0,0 +1,136 @@ +/* + * Copyright 2026 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef wasm_support_coroutine_h +#define wasm_support_coroutine_h + +#include +#include + +namespace wasm { + +// A generator that yields T and receives U on resumption (if U is not +// void). The one-way generator (where U is void) provides the following +// methods: +// +// bool next() - Resume the coroutine and return whether it can be resumed +// again. +// T& get() - Return the current yielded value. +// +// The two-way generator (where U is not void) provides the following methods: +// + +// bool resume(U) - Resume the coroutine and return whether it can be resumed +// again. +// T& get() - Return the current yielded value. +// +// TODO: Make the one-way generator into a forward iterator. +template struct Generator; + +// One-way generator +template struct Generator { + struct promise_type { + T current_value; + + Generator get_return_object() { + return {std::coroutine_handle::from_promise(*this)}; + } + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void unhandled_exception() { std::terminate(); } + void return_void() {} + + std::suspend_always yield_value(T value) { + current_value = std::move(value); + return {}; + } + }; + + std::coroutine_handle handle; + + Generator(std::coroutine_handle h) : handle(h) {} + Generator(const Generator&) = delete; + Generator(Generator&& other) noexcept : handle(other.handle) { + other.handle = nullptr; + } + ~Generator() { + if (handle) { + handle.destroy(); + } + } + + bool next() { + handle.resume(); + return !handle.done(); + } + + T& get() { return handle.promise().current_value; } + const T& get() const { return handle.promise().current_value; } +}; + +// Two-way generator +template struct Generator { + struct promise_type { + T current_value; + U received_value; + + Generator get_return_object() { + return {std::coroutine_handle::from_promise(*this)}; + } + std::suspend_always initial_suspend() { return {}; } + std::suspend_always final_suspend() noexcept { return {}; } + void unhandled_exception() { std::terminate(); } + void return_void() {} + + auto yield_value(T value) { + current_value = std::move(value); + return YieldAwaiter{this}; + } + + struct YieldAwaiter { + promise_type* p; + bool await_ready() const noexcept { return false; } + void await_suspend(std::coroutine_handle) noexcept {} + U await_resume() const noexcept { return p->received_value; } + }; + }; + + std::coroutine_handle handle; + + Generator(std::coroutine_handle h) : handle(h) {} + Generator(const Generator&) = delete; + Generator(Generator&& other) noexcept : handle(other.handle) { + other.handle = nullptr; + } + ~Generator() { + if (handle) { + handle.destroy(); + } + } + + bool resume(U value) { + handle.promise().received_value = std::move(value); + handle.resume(); + return !handle.done(); + } + + T& get() { return handle.promise().current_value; } + const T& get() const { return handle.promise().current_value; } +}; + +} // namespace wasm + +#endif // wasm_support_coroutine_h diff --git a/src/support/delta_debugging.h b/src/support/delta_debugging.h index 9607d2011d0..5e754be27ac 100644 --- a/src/support/delta_debugging.h +++ b/src/support/delta_debugging.h @@ -21,100 +21,156 @@ #include #include +#include "support/coroutine.h" + namespace wasm { -// Use the delta debugging algorithm (Zeller 1999, -// https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of -// items necessary to preserve some property. Returns that minimal set of -// items, preserving their input order. `tryPartition` should have this -// signature: -// -// bool tryPartition(size_t partitionIndex, -// size_t numPartitions, -// const std::vector& partition) -// -// It should return true iff the property is preserved while keeping only -// `partition` items. -template -std::vector deltaDebugging(std::vector items, const F& tryPartition) { - if (items.empty()) { - return items; - } - // First try removing everything. - if (tryPartition(0, 1, {})) { - return {}; +// Use the delta debugging algorithm (Zeller 2002, +// https://dl.acm.org/doi/10.1109/32.988498) to find the minimal set of items +// necessary to preserve some property. `working` is the minimal set of items +// found so far and `test` is the smaller set of items that should be tested +// next. After testing, call `accept()`, `reject()`, or `resolve(bool accepted)` +// to update the working and test sets appropriately. +template struct DeltaDebugger { + DeltaDebugger(std::vector items) : task(run(std::move(items))) { + task.handle.resume(); } - size_t numPartitions = 2; - while (numPartitions <= items.size()) { - // Partition the items. - std::vector> partitions; - size_t size = items.size(); - size_t basePartitionSize = size / numPartitions; - size_t rem = size % numPartitions; - size_t idx = 0; - for (size_t i = 0; i < numPartitions; ++i) { - size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0); - if (partitionSize > 0) { - std::vector partition; - partition.reserve(partitionSize); - for (size_t j = 0; j < partitionSize; ++j) { - partition.push_back(items[idx++]); - } - partitions.emplace_back(std::move(partition)); - } + + bool finished() const { return task.get()->finished; } + + const std::vector& working() const { return task.get()->working; } + std::vector& test() { return task.get()->test; } + + size_t partitionCount() const { return task.get()->numPartitions; } + size_t partitionIndex() const { return task.get()->currPartition; } + + void resolve(bool success) { + // If the algorithm is finished, do not resume the coroutine and let it + // return; we depend on its local state remaining live. + if (finished()) { + return; } - assert(numPartitions == partitions.size()); + task.resume(success); + } - bool reduced = false; + void accept() { resolve(true); } + void reject() { resolve(false); } - // Try keeping only one partition. Try each partition in turn. - for (size_t i = 0; i < numPartitions; ++i) { - if (tryPartition(i, numPartitions, partitions[i])) { - items = std::move(partitions[i]); - numPartitions = 2; - reduced = true; - break; - } +private: + struct State { + std::vector working; + std::vector test; + size_t numPartitions = 1; + size_t currPartition = 0; + bool finished = false; + }; + + Generator task; + + static Generator run(std::vector items) { + State state; + auto& [working, test, numPartitions, currPartition, finished] = state; + + working = std::move(items); + + if (working.empty()) { + finished = true; + co_yield &state; + co_return; } - if (reduced) { - continue; + + // First try removing everything. + if (co_yield &state) { + working = {}; + finished = true; + // Yield the final results rather than returning because we need the local + // state to remain live for the lifetime of the DeltaDebugger. + co_yield &state; + co_return; } - // Otherwise, try keeping the complement of a partition. Do not do this with - // only two partitions because that would be no different from what we - // already tried. - if (numPartitions > 2) { + numPartitions = 2; + while (numPartitions <= working.size()) { + // Partition the items. + std::vector> partitions; + size_t size = working.size(); + size_t basePartitionSize = size / numPartitions; + size_t rem = size % numPartitions; + size_t idx = 0; for (size_t i = 0; i < numPartitions; ++i) { - std::vector complement; - complement.reserve(items.size() - partitions[i].size()); - for (size_t j = 0; j < numPartitions; ++j) { - if (j != i) { - complement.insert( - complement.end(), partitions[j].begin(), partitions[j].end()); + size_t partitionSize = basePartitionSize + (i < rem ? 1 : 0); + if (partitionSize > 0) { + std::vector partition; + partition.reserve(partitionSize); + for (size_t j = 0; j < partitionSize; ++j) { + partition.push_back(working[idx++]); } + partitions.emplace_back(std::move(partition)); } - if (tryPartition(i, numPartitions, complement)) { - items = std::move(complement); - numPartitions = std::max(numPartitions - 1, size_t(2)); + } + assert(numPartitions == partitions.size()); + + bool reduced = false; + + // Try keeping only one partition. Try each partition in turn. + for (currPartition = 0; currPartition < numPartitions; ++currPartition) { + test = std::move(partitions[currPartition]); + if (co_yield &state) { + working = std::move(test); + numPartitions = 2; reduced = true; break; + } else { + // Restore the partition since we failed and might need it for + // complement testing. + partitions[currPartition] = std::move(test); } } if (reduced) { continue; } - } - if (numPartitions == items.size()) { - // Cannot further refine the partitions. We're done. - break; + // Otherwise, try keeping the complement of a partition. Do not do this + // with only two partitions because that would be no different from what + // we already tried. + if (numPartitions > 2) { + for (currPartition = 0; currPartition < numPartitions; + ++currPartition) { + test.clear(); + test.reserve(working.size() - partitions[currPartition].size()); + for (size_t i = 0; i < numPartitions; ++i) { + if (i != currPartition) { + test.insert( + test.end(), partitions[i].begin(), partitions[i].end()); + } + } + if (co_yield &state) { + working = std::move(test); + numPartitions = std::max(numPartitions - 1, size_t(2)); + reduced = true; + break; + } + } + if (reduced) { + continue; + } + } + + if (numPartitions == working.size()) { + // Cannot further refine the partitions. We're done. + break; + } + + // Otherwise, make the partitions finer grained. + numPartitions = std::min(working.size(), 2 * numPartitions); } - // Otherwise, make the partitions finer grained. - numPartitions = std::min(items.size(), 2 * numPartitions); + // Yield final state + test = {}; + finished = true; + co_yield &state; } - return items; -} +}; } // namespace wasm diff --git a/src/tools/wasm-reduce/wasm-reduce.cpp b/src/tools/wasm-reduce/wasm-reduce.cpp index 6722cb45a03..fa3962debc8 100644 --- a/src/tools/wasm-reduce/wasm-reduce.cpp +++ b/src/tools/wasm-reduce/wasm-reduce.cpp @@ -918,78 +918,68 @@ struct Reducer } nontrivialFuncIndices.push_back(i); } - // TODO: Use something other than an exception to implement early return. - struct EarlyReturn {}; - try { - deltaDebugging( - nontrivialFuncIndices, - [&](Index partitionIndex, - Index numPartitions, - const std::vector& partition) { - // Stop early if the partition size is less than the square root of - // the remaining set. We don't want to waste time on very fine-grained - // partitions when we could switch to another reduction strategy - // instead. - if (size_t sqrtRemaining = std::sqrt(nontrivialFuncIndices.size()); - partition.size() > 0 && partition.size() < sqrtRemaining) { - throw EarlyReturn{}; - } + DeltaDebugger dd(std::move(nontrivialFuncIndices)); + while (!dd.finished()) { + // Stop early if the partition size is less than the square root of + // the remaining set. We don't want to waste time on very fine-grained + // partitions when we could switch to another reduction strategy + // instead. + if (size_t sqrtRemaining = std::sqrt(dd.working().size()); + dd.test().size() > 0 && dd.test().size() < sqrtRemaining) { + break; + } - std::cerr << "| try partition " << partitionIndex + 1 << " / " - << numPartitions << " (size " << partition.size() << ")\n"; - Index removedSize = nontrivialFuncIndices.size() - partition.size(); - std::vector oldBodies(removedSize); - - // We first need to remove each non-kept function body, and later we - // might need to restore the same function bodies. Abstract the logic - // for iterating over these function bodies. `f` takes a Function* and - // Expression*& for the stashed body. - auto forEachRemovedFuncBody = [&](auto f) { - Index bodyIndex = 0; - Index nontrivialIndex = 0; - Index partitionIndex = 0; - while (nontrivialIndex < nontrivialFuncIndices.size()) { - if (partitionIndex < partition.size() && - nontrivialFuncIndices[nontrivialIndex] == - partition[partitionIndex]) { - // Kept, skip it. - nontrivialIndex++; - partitionIndex++; - } else { - // Removed, process it - Index funcIndex = nontrivialFuncIndices[nontrivialIndex++]; - f(module->functions[funcIndex].get(), oldBodies[bodyIndex++]); - } - } - assert(bodyIndex == removedSize); - assert(partitionIndex == partition.size()); - }; - - // Stash the bodies. - forEachRemovedFuncBody([&](Function* func, Expression*& oldBody) { - oldBody = func->body; - Builder builder(*module); - if (func->getResults() == Type::none) { - func->body = builder.makeNop(); - } else { - func->body = builder.makeUnreachable(); - } - }); - - if (!writeAndTestReduction()) { - // Failure. Restore the bodies. - forEachRemovedFuncBody([](Function* func, Expression*& oldBody) { - func->body = oldBody; - }); - return false; + std::cerr << "| try partition " << dd.partitionIndex() + 1 << " / " + << dd.partitionCount() << " (size " << dd.test().size() + << ")\n"; + Index removedSize = dd.working().size() - dd.test().size(); + std::vector oldBodies(removedSize); + + // We first need to remove each non-kept function body, and later we + // might need to restore the same function bodies. Abstract the logic + // for iterating over these function bodies. `f` takes a Function* and + // Expression*& for the stashed body. + auto forEachRemovedFuncBody = [&](auto f) { + Index bodyIndex = 0; + Index workingIndex = 0; + Index testIndex = 0; + while (workingIndex < dd.working().size()) { + if (testIndex < dd.test().size() && + dd.working()[workingIndex] == dd.test()[testIndex]) { + // Kept, skip it. + workingIndex++; + testIndex++; + } else { + // Removed, process it + Index funcIndex = dd.working()[workingIndex++]; + f(module->functions[funcIndex].get(), oldBodies[bodyIndex++]); } + } + assert(bodyIndex == removedSize); + assert(testIndex == dd.test().size()); + }; + + // Stash the bodies. + forEachRemovedFuncBody([&](Function* func, Expression*& oldBody) { + oldBody = func->body; + Builder builder(*module); + if (func->getResults() == Type::none) { + func->body = builder.makeNop(); + } else { + func->body = builder.makeUnreachable(); + } + }); - // Success! - noteReduction(removedSize); - nontrivialFuncIndices = partition; - return true; - }); - } catch (EarlyReturn) { + if (!writeAndTestReduction()) { + // Failure. Restore the bodies. + forEachRemovedFuncBody( + [](Function* func, Expression*& oldBody) { func->body = oldBody; }); + dd.reject(); + } else { + // Success! + noteReduction(removedSize); + dd.accept(); + } } } diff --git a/test/gtest/delta_debugging.cpp b/test/gtest/delta_debugging.cpp index 7e4c8ad4db5..dea2cfbc1e0 100644 --- a/test/gtest/delta_debugging.cpp +++ b/test/gtest/delta_debugging.cpp @@ -8,90 +8,189 @@ using namespace wasm; TEST(DeltaDebuggingTest, EmptyInput) { std::vector items; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector&) { return false; }); - EXPECT_TRUE(result.empty()); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(false); + } + EXPECT_TRUE(dd.working().empty()); +} + +TEST(DeltaDebuggingTest, SingleInputEmptySetWorks) { + std::vector items = {42}; + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(dd.test().empty()); + } + EXPECT_TRUE(dd.working().empty()); +} + +TEST(DeltaDebuggingTest, SingleInputEmptySetFails) { + std::vector items = {42}; + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(!dd.test().empty()); + } + std::vector expected = {42}; + EXPECT_EQ(dd.working(), expected); } TEST(DeltaDebuggingTest, SingleItem) { std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - return std::find(partition.begin(), partition.end(), 3) != - partition.end(); - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(std::find(dd.test().begin(), dd.test().end(), 3) != + dd.test().end()); + } std::vector expected = {3}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working(), expected); } TEST(DeltaDebuggingTest, MultipleItemsAdjacent) { std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool has2 = - std::find(partition.begin(), partition.end(), 2) != partition.end(); - bool has3 = - std::find(partition.begin(), partition.end(), 3) != partition.end(); - return has2 && has3; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has2 = + std::find(dd.test().begin(), dd.test().end(), 2) != dd.test().end(); + bool has3 = + std::find(dd.test().begin(), dd.test().end(), 3) != dd.test().end(); + dd.resolve(has2 && has3); + } std::vector expected = {2, 3}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working(), expected); } TEST(DeltaDebuggingTest, MultipleItemsNonAdjacent) { std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool has2 = - std::find(partition.begin(), partition.end(), 2) != partition.end(); - bool has5 = - std::find(partition.begin(), partition.end(), 5) != partition.end(); - return has2 && has5; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has2 = + std::find(dd.test().begin(), dd.test().end(), 2) != dd.test().end(); + bool has5 = + std::find(dd.test().begin(), dd.test().end(), 5) != dd.test().end(); + dd.resolve(has2 && has5); + } std::vector expected = {2, 5}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working(), expected); } TEST(DeltaDebuggingTest, OrderMaintained) { std::vector items = {3, 1, 4, 2}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool has3 = - std::find(partition.begin(), partition.end(), 3) != partition.end(); - bool has2 = - std::find(partition.begin(), partition.end(), 2) != partition.end(); - return has3 && has2; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has3 = + std::find(dd.test().begin(), dd.test().end(), 3) != dd.test().end(); + bool has2 = + std::find(dd.test().begin(), dd.test().end(), 2) != dd.test().end(); + dd.resolve(has3 && has2); + } std::vector expected = {3, 2}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working(), expected); } TEST(DeltaDebuggingTest, DifferentTypes) { std::vector items = {"apple", "banana", "cherry", "date"}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector& partition) { - bool hasBanana = - std::find(partition.begin(), partition.end(), "banana") != - partition.end(); - bool hasDate = std::find(partition.begin(), partition.end(), "date") != - partition.end(); - return hasBanana && hasDate; - }); + DeltaDebugger dd(items); + while (!dd.finished()) { + bool hasBanana = std::find(dd.test().begin(), dd.test().end(), "banana") != + dd.test().end(); + bool hasDate = + std::find(dd.test().begin(), dd.test().end(), "date") != dd.test().end(); + dd.resolve(hasBanana && hasDate); + } std::vector expected = {"banana", "date"}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working(), expected); } TEST(DeltaDebuggingTest, UnconditionallyTrue) { std::vector items = {0, 1, 2, 3}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector&) { return true; }); - EXPECT_TRUE(result.empty()); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(true); + } + EXPECT_TRUE(dd.working().empty()); } TEST(DeltaDebuggingTest, UnconditionallyFalse) { std::vector items = {0, 1, 2, 3}; - auto result = deltaDebugging( - items, [](size_t, size_t, const std::vector&) { return false; }); + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(false); + } std::vector expected = {0, 1, 2, 3}; - EXPECT_EQ(result, expected); + EXPECT_EQ(dd.working(), expected); +} + +TEST(DeltaDebuggingTest, StructBasic) { + std::vector items = {0, 1, 2, 3, 4, 5, 6, 7}; + DeltaDebugger dd(items); + while (!dd.finished()) { + bool has3 = + std::find(dd.test().begin(), dd.test().end(), 3) != dd.test().end(); + dd.resolve(has3); + } + std::vector expected = {3}; + EXPECT_EQ(dd.working(), expected); +} + +TEST(DeltaDebuggingTest, HierarchicalExample) { + std::vector> items = {{1, 10}, {2, 10}, {3, 10}}; + + auto testProperty = [](const std::vector>& lists) { + int sum = 0; + for (const auto& list : lists) { + for (int x : list) { + sum += x; + } + } + return sum >= 20; + }; + + DeltaDebugger> dd(items); + while (!dd.finished()) { + dd.resolve(testProperty(dd.test())); + } + + std::vector> currentLists = dd.working(); + + for (size_t i = 0; i < currentLists.size(); ++i) { + std::vector currentList = currentLists[i]; + DeltaDebugger subDd(currentList); + + while (!subDd.finished()) { + std::vector> fullTestSet; + for (size_t j = 0; j < currentLists.size(); ++j) { + if (j == i) { + fullTestSet.push_back(subDd.test()); + } else { + fullTestSet.push_back(currentLists[j]); + } + } + subDd.resolve(testProperty(fullTestSet)); + } + currentLists[i] = subDd.working(); + } + + std::vector> expected = {{10}, {10}}; + EXPECT_EQ(currentLists, expected); +} + +TEST(DeltaDebuggingTest, ResolveAfterFinished) { + std::vector items = {0, 1, 2, 3}; + DeltaDebugger dd(items); + while (!dd.finished()) { + dd.resolve(false); + } + + std::vector expected = {0, 1, 2, 3}; + EXPECT_EQ(dd.working(), expected); + EXPECT_TRUE(dd.finished()); + + // Call resolve again + dd.resolve(true); + EXPECT_EQ(dd.working(), expected); + EXPECT_TRUE(dd.finished()); + + dd.resolve(false); + EXPECT_EQ(dd.working(), expected); + EXPECT_TRUE(dd.finished()); }