/* * 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_sort_h #define wasm_support_topological_sort_h #include #include #include #include #include #include #include #include #include #include "support/index.h" namespace wasm { namespace TopologicalSort { // An adjacency list containing edges from vertices to their successors. Uses // `Index` because we are primarily sorting elements of Wasm modules. If we ever // need to sort signficantly larger objects, we might need to switch to // `size_t` or make this a template parameter. using Graph = std::vector>; // Return a topological sort of the vertices in the given adjacency graph. inline std::vector sort(const Graph& graph); // Return the topological sort of the vertices in the given adjacency graph that // is lexicographically minimal with respect to the provided comparator on // vertex indices. Implemented using a min-heap internally. template> std::vector minSort(const Graph& graph, F cmp = std::less{}); // A utility that finds a topological sort of a graph with arbitrary element // types. The provided iterators must be to pairs of elements and collections of // their children. template decltype(auto) sortOfImpl(It begin, It end, Args... args) { using T = std::remove_cv_t; std::unordered_map indices; std::vector elements; // Assign indices to each element. for (auto it = begin; it != end; ++it) { auto inserted = indices.insert({it->first, elements.size()}); assert(inserted.second && "unexpected repeat element"); elements.push_back(inserted.first->first); } // Collect the graph in terms of indices. Graph indexGraph; indexGraph.reserve(elements.size()); for (auto it = begin; it != end; ++it) { indexGraph.emplace_back(); for (const auto& child : it->second) { indexGraph.back().push_back(indices.at(child)); } } // Compute the topological order and convert back to original elements. std::vector order; order.reserve(elements.size()); for (auto i : Sort(indexGraph, std::forward(args)...)) { order.emplace_back(std::move(elements[i])); } return order; } template decltype(auto) sortOf(It begin, It end) { return sortOfImpl (&)(const Graph&), sort>(begin, end); } template decltype(auto) minSortOf(It begin, It end, Cmp cmp) { return sortOfImpl (&)(const Graph&, Cmp), minSort, Cmp>(begin, end, cmp); } } // namespace TopologicalSort template struct TopologicalOrdersImpl; // 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. using TopologicalOrders = TopologicalOrdersImpl; // The template parameter `Cmp` is only used as in internal implementation // detail of `minSort`. template struct TopologicalOrdersImpl { using Graph = TopologicalSort::Graph; using value_type = const std::vector; using difference_type = std::ptrdiff_t; using reference = const std::vector&; using pointer = const std::vector*; using iterator_category = std::input_iterator_tag; // 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. TopologicalOrdersImpl(const Graph& graph) : TopologicalOrdersImpl(graph, {}) {} TopologicalOrdersImpl begin() { return TopologicalOrdersImpl(graph); } TopologicalOrdersImpl end() { return TopologicalOrdersImpl({}); } bool operator==(const TopologicalOrdersImpl& other) const { return selectors.empty() == other.selectors.empty(); } bool operator!=(const TopologicalOrdersImpl& other) const { return !(*this == other); } const std::vector& operator*() const { return buf; } const std::vector* operator->() const { return &buf; } TopologicalOrdersImpl& operator++() { // Find the last selector that can be advanced, popping any that cannot. std::optional 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 *this; } // 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)); } return *this; } TopologicalOrdersImpl operator++(int) { return ++(*this); } private: TopologicalOrdersImpl(const Graph& graph, Cmp cmp) : graph(graph), indegrees(graph.size()), buf(graph.size()), cmp(cmp) { 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 (Index i = 0; i < graph.size(); ++i) { if (indegrees[i] == 0) { if constexpr (useMinHeap) { pushChoice(i); } else { buf[first.count] = i; } ++first.count; } } // Initialize the full stack of selectors. while (selectors.size() < graph.size()) { selectors.push_back(selectors.back().select(*this)); } selectors.back().select(*this); } // The input graph given as an adjacency list with edges from vertices to // their dependent children. const Graph& 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 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 buf; // When we are finding a minimal topological order, store the possible // choices in this separate min-heap instead of directly in `buf`. std::vector choiceHeap; // When we are finding a minimal topological order, use this function to // compare possible choices. Empty iff we are not finding a minimal // topological order. Cmp cmp; static constexpr bool useMinHeap = !std::is_same_v; // 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. Index start; // The number of choices we have. Index count; // The index of the current choice in the original order. Index 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(TopologicalOrdersImpl& ctx) { assert(count >= 1); assert(start + count <= ctx.buf.size()); if constexpr (TopologicalOrdersImpl::useMinHeap) { ctx.buf[start] = ctx.popChoice(); } 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) { if constexpr (TopologicalOrdersImpl::useMinHeap) { ctx.pushChoice(child); } else { ctx.buf[next.start + next.count] = child; } ++next.count; } } return next; } // 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 advance(TopologicalOrdersImpl& 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 (Index 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); } }; void pushChoice(Index choice) { choiceHeap.push_back(choice); std::push_heap(choiceHeap.begin(), choiceHeap.end(), [&](Index a, Index b) { return cmp(b, a); }); } Index popChoice() { std::pop_heap(choiceHeap.begin(), choiceHeap.end(), [&](Index a, Index b) { return cmp(b, a); }); auto choice = choiceHeap.back(); choiceHeap.pop_back(); return choice; } // A stack of selectors, one for each vertex in a complete topological order. // Empty if we've already seen every possible ordering. std::vector selectors; friend std::vector TopologicalSort::minSort(const Graph&, Cmp); }; namespace TopologicalSort { std::vector sort(const Graph& graph) { return *TopologicalOrders(graph); } template std::vector minSort(const Graph& graph, Cmp cmp) { return *TopologicalOrdersImpl(graph, cmp); } } // namespace TopologicalSort } // namespace wasm #endif // wasm_support_topological_sort_h