-
Notifications
You must be signed in to change notification settings - Fork 831
Add a utility for iterating over all topological orders #6801
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| /* | ||
| * Copyright 2024 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. | ||
| */ | ||
|
|
||
| #include <cassert> | ||
|
|
||
| #include "topological_orders.h" | ||
|
|
||
| namespace wasm { | ||
|
|
||
| TopologicalOrders::Selector | ||
| TopologicalOrders::Selector::select(TopologicalOrders& ctx) { | ||
| assert(count >= 1); | ||
| assert(start + count <= ctx.buf.size()); | ||
| auto selection = ctx.buf[start]; | ||
| // The next selector will select the next index and will not be able to choose | ||
| // the vertex we just selected. | ||
| Selector next = {start + 1, count - 1, 0}; | ||
| // Append any child that this selection makes available to the choices for the | ||
| // next selector. | ||
| for (auto child : ctx.graph[selection]) { | ||
| assert(ctx.indegrees[child] > 0); | ||
| if (--ctx.indegrees[child] == 0) { | ||
| ctx.buf[next.start + next.count++] = child; | ||
| } | ||
| } | ||
| return next; | ||
| } | ||
|
|
||
| std::optional<TopologicalOrders::Selector> | ||
| TopologicalOrders::Selector::advance(TopologicalOrders& ctx) { | ||
| assert(count >= 1); | ||
| // Undo the current selection. Backtrack by incrementing the in-degree for | ||
| // each child of the vertex we are unselecting. No need to remove the newly | ||
| // unavailable children from the buffer; they will be overwritten with valid | ||
| // selections. | ||
| auto unselected = ctx.buf[start]; | ||
| for (auto child : ctx.graph[unselected]) { | ||
| ++ctx.indegrees[child]; | ||
| } | ||
| if (index == count - 1) { | ||
| // We are wrapping back to the original configuration. The current selection | ||
| // element needs to go back on the end and everything else needs to be | ||
| // shifted back to its original location. This ensures that we leave | ||
| // everything how we found it so the previous selector can make its next | ||
| // selection without observing anything having changed in the meantime. | ||
| for (size_t i = 1; i < count; ++i) { | ||
| ctx.buf[start + i - 1] = ctx.buf[start + i]; | ||
| } | ||
| ctx.buf[start + count - 1] = unselected; | ||
| return std::nullopt; | ||
| } | ||
| // Otherwise, just swap the next selection into the first position and | ||
| // finalize the selection. | ||
| std::swap(ctx.buf[start], ctx.buf[start + ++index]); | ||
| return select(ctx); | ||
| } | ||
|
|
||
| TopologicalOrders::TopologicalOrders( | ||
| const std::vector<std::vector<size_t>>& graph) | ||
| : graph(graph), indegrees(graph.size()), buf(graph.size()) { | ||
| if (graph.size() == 0) { | ||
| return; | ||
| } | ||
| // Find the in-degree of each vertex. | ||
| for (const auto& vertex : graph) { | ||
| for (auto child : vertex) { | ||
| ++indegrees[child]; | ||
| } | ||
| } | ||
| // Set up the first selector with its possible selections. | ||
| selectors.reserve(graph.size()); | ||
| selectors.push_back({0, 0, 0}); | ||
| auto& first = selectors.back(); | ||
| for (size_t i = 0; i < graph.size(); ++i) { | ||
| if (indegrees[i] == 0) { | ||
| buf[first.count++] = i; | ||
| } | ||
| } | ||
| // Initialize the full stack of selectors. | ||
| while (selectors.size() < graph.size()) { | ||
| selectors.push_back(selectors.back().select(*this)); | ||
| } | ||
| } | ||
|
|
||
| void TopologicalOrders::advance() { | ||
| // Find the last selector that can be advanced, popping any that cannot. | ||
| std::optional<Selector> next; | ||
| while (!selectors.empty() && !(next = selectors.back().advance(*this))) { | ||
| selectors.pop_back(); | ||
| } | ||
| if (!next) { | ||
| // No selector could be advanced, so we've seen every possible ordering. | ||
| assert(selectors.empty()); | ||
| return; | ||
| } | ||
| // We've advanced the last selector on the stack, so initialize the | ||
| // subsequent selectors. | ||
| assert(selectors.size() < graph.size()); | ||
| selectors.push_back(*next); | ||
| while (selectors.size() < graph.size()) { | ||
| selectors.push_back(selectors.back().select(*this)); | ||
| } | ||
| } | ||
|
|
||
| } // namespace wasm | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| /* | ||
| * Copyright 2024 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_topological_orders_h | ||
| #define wasm_support_topological_orders_h | ||
|
|
||
| #include <cstddef> | ||
| #include <optional> | ||
| #include <vector> | ||
|
|
||
| namespace wasm { | ||
|
|
||
| // A utility for iterating through all possible topological orders in a graph | ||
| // using an extension of Kahn's algorithm (see | ||
| // https://en.wikipedia.org/wiki/Topological_sorting) that iteratively makes all | ||
| // possible choices for each position of the output order. | ||
| struct TopologicalOrders { | ||
| // Takes an adjacency list, where the list for each vertex is a sorted list of | ||
| // the indices of its children, which will appear after it in the order. | ||
| TopologicalOrders(const std::vector<std::vector<size_t>>& graph); | ||
|
|
||
| struct Iterator { | ||
| using value_type = const std::vector<size_t>; | ||
| using difference_type = std::ptrdiff_t; | ||
| using reference = const std::vector<size_t>&; | ||
| using pointer = const std::vector<size_t>*; | ||
| using iterator_category = std::input_iterator_tag; | ||
|
|
||
| TopologicalOrders* parent; | ||
|
|
||
| bool isEnd() const { return !parent || parent->selectors.empty(); } | ||
| bool operator==(const Iterator& other) const { | ||
| return isEnd() == other.isEnd(); | ||
| } | ||
| bool operator!=(const Iterator& other) const { return !(*this == other); } | ||
| const std::vector<size_t>& operator*() { return parent->buf; } | ||
| const std::vector<size_t>* operator->() { return &parent->buf; } | ||
| Iterator& operator++() { | ||
| parent->advance(); | ||
| return *this; | ||
| } | ||
| Iterator operator++(int) { return ++(*this); } | ||
| }; | ||
|
|
||
| Iterator begin() { return {this}; } | ||
| Iterator end() { return {nullptr}; } | ||
|
|
||
| private: | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can all the private parts be moved out of the header? I think it is easier to read in either a single file (no separate cpp; makes sense for short things), or two files with the implementation in one and the interface in the other. E.g. I think LocalGraph does a good job of this. The header has just the API, and all the flow analysis to compute the graph is in the cpp.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not without introducing a layer of indirection, unfortunately. We could add a unique_ptr to an opaque implementation object, but I'd rather not. Do you think it would be worth it?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair enough as it is, I guess. It's reasonably short, and the iteration makes it hard to separate. |
||
| // The input graph given as an adjacency list with edges from vertices to | ||
| // their dependent children. | ||
| const std::vector<std::vector<size_t>>& graph; | ||
| // The current in-degrees for each vertex. When a vertex is appended to our | ||
| // permutation, the in-degrees of its children are decremented and those that | ||
| // go to zero become available for the next selection. | ||
| std::vector<size_t> indegrees; | ||
| // The buffer in which we are constructing a permutation. It contains a | ||
| // sequence of selected vertices followed by a sequence of possible choices | ||
| // for the next vertex. | ||
| std::vector<size_t> buf; | ||
|
|
||
| // The state for tracking the possible choices for a single vertex in the | ||
| // output order. | ||
| struct Selector { | ||
| // The start index of the sequence of available choices. Also the index | ||
| // where we place the current choice. | ||
| size_t start; | ||
| // The number of choices we have. | ||
| size_t count; | ||
| // The index of the current choice in the original order. | ||
| size_t index; | ||
|
|
||
| // Select the next available vertex, decrement in-degrees, and update the | ||
| // sequence of available vertices. Return the Selector for the next vertex. | ||
| Selector select(TopologicalOrders& ctx); | ||
|
|
||
| // Undo the current selection, move the next selection into the first | ||
| // position and return the new selector for the next position. Returns | ||
| // nullopt if advancing wraps back around to the original configuration. | ||
| std::optional<Selector> advance(TopologicalOrders& ctx); | ||
| }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I read Kahn's Algorithm on wikipedia but didn't see mention of "selectors" there. Is this a generally familiar term I am unaware of?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Kahn's algorithm finds a single topological sort by selecting one of the next available vertices arbitrarily. Here we systematically select each of the next available vertices in sequence to enumerate all the possible topological sorts. |
||
|
|
||
| // A stack of selectors, one for each vertex in a complete topological order. | ||
| // Empty if we've already seen every possible ordering. | ||
| std::vector<Selector> selectors; | ||
|
|
||
| void advance(); | ||
| }; | ||
|
|
||
| } // namespace wasm | ||
|
|
||
| #endif // wasm_support_topological_orders_h | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| /* | ||
| * Copyright 2024 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. | ||
| */ | ||
|
|
||
| #include <cstddef> | ||
| #include <optional> | ||
| #include <vector> | ||
|
|
||
| #include "support/topological_orders.h" | ||
| #include "gtest/gtest.h" | ||
|
|
||
| using namespace wasm; | ||
|
|
||
| using Graph = std::vector<std::vector<size_t>>; | ||
|
|
||
| TEST(TopologicalOrdersTest, Empty) { | ||
| Graph graph; | ||
| TopologicalOrders orders(graph); | ||
| EXPECT_EQ(orders.begin(), orders.end()); | ||
| } | ||
|
|
||
| TEST(TopologicalOrdersTest, Singleton) { | ||
| Graph graph(1); | ||
| TopologicalOrders orders(graph); | ||
| auto it = orders.begin(); | ||
| ASSERT_NE(it, orders.end()); | ||
| EXPECT_EQ(*it, std::vector<size_t>{0}); | ||
| ++it; | ||
| EXPECT_EQ(it, orders.end()); | ||
| } | ||
|
|
||
| TEST(TopologicalOrdersTest, Permutations) { | ||
| Graph graph(3); | ||
| TopologicalOrders orders(graph); | ||
| std::set<std::vector<size_t>> results(orders.begin(), orders.end()); | ||
| std::set<std::vector<size_t>> expected{ | ||
| {0, 1, 2}, | ||
| {0, 2, 1}, | ||
| {1, 0, 2}, | ||
| {1, 2, 0}, | ||
| {2, 0, 1}, | ||
| {2, 1, 0}, | ||
| }; | ||
| EXPECT_EQ(results, expected); | ||
| } | ||
|
|
||
| TEST(TopologicalOrdersTest, Chain) { | ||
| constexpr size_t n = 10; | ||
| Graph graph(n); | ||
| for (size_t i = 1; i < n; ++i) { | ||
| graph[i].push_back(i - 1); | ||
| } | ||
| TopologicalOrders orders(graph); | ||
| std::set<std::vector<size_t>> results(orders.begin(), orders.end()); | ||
| std::set<std::vector<size_t>> expected{{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}}; | ||
| EXPECT_EQ(results, expected); | ||
| } | ||
|
|
||
| TEST(TopologicalOrdersTest, TwoChains) { | ||
| Graph graph(4); | ||
| graph[0].push_back(2); | ||
| graph[1].push_back(3); | ||
| TopologicalOrders orders(graph); | ||
| std::set<std::vector<size_t>> results(orders.begin(), orders.end()); | ||
| std::set<std::vector<size_t>> expected{ | ||
| {0, 1, 2, 3}, | ||
| {0, 1, 3, 2}, | ||
| {0, 2, 1, 3}, | ||
| {1, 0, 2, 3}, | ||
| {1, 0, 3, 2}, | ||
| {1, 3, 0, 2}, | ||
| }; | ||
| EXPECT_EQ(results, expected); | ||
| } | ||
|
|
||
| TEST(TopologicalOrdersTest, Diamond) { | ||
| Graph graph(4); | ||
| graph[0].push_back(1); | ||
| graph[0].push_back(2); | ||
| graph[1].push_back(3); | ||
| graph[2].push_back(3); | ||
| TopologicalOrders orders(graph); | ||
| std::set<std::vector<size_t>> results(orders.begin(), orders.end()); | ||
| std::set<std::vector<size_t>> expected{ | ||
| {0, 1, 2, 3}, | ||
| {0, 2, 1, 3}, | ||
| }; | ||
| EXPECT_EQ(results, expected); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once again I wonder why
++x;instead ofx++;, when there is nothing reading the old value... 😄Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For iterators with non-trivial state, preincrement is more efficient, so should clearly be preferred. (Optimizers can't necessarily optimize out copies even if there are no readers because the copy constructors might have arbitrary side effects.) For other iterators it doesn't matter, so we should still choose preincrement for the sake of consistency.
https://stackoverflow.com/questions/30036749/is-it-still-better-to-prefer-pre-increment-over-post-increment