Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 33 additions & 5 deletions src/support/topological_orders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@
* limitations under the License.
*/

#include <algorithm>
#include <cassert>

#include "topological_orders.h"

namespace wasm {

TopologicalOrders::Selector
TopologicalOrders::Selector::select(TopologicalOrders& ctx) {
TopologicalOrders::Selector::select(TopologicalOrders& ctx,
SelectionMethod method = InPlace) {
assert(count >= 1);
assert(start + count <= ctx.buf.size());
if (method == MinHeap) {
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.
Expand All @@ -33,7 +38,12 @@ TopologicalOrders::Selector::select(TopologicalOrders& ctx) {
for (auto child : ctx.graph[selection]) {
assert(ctx.indegrees[child] > 0);
if (--ctx.indegrees[child] == 0) {
ctx.buf[next.start + next.count++] = child;
if (method == MinHeap) {
ctx.pushChoice(child);
} else {
ctx.buf[next.start + next.count] = child;
}
++next.count;
}
}
return next;
Expand Down Expand Up @@ -69,7 +79,7 @@ TopologicalOrders::Selector::advance(TopologicalOrders& ctx) {
}

TopologicalOrders::TopologicalOrders(
const std::vector<std::vector<size_t>>& graph)
const std::vector<std::vector<size_t>>& graph, SelectionMethod method)
: graph(graph), indegrees(graph.size()), buf(graph.size()) {
if (graph.size() == 0) {
return;
Expand All @@ -86,13 +96,19 @@ TopologicalOrders::TopologicalOrders(
auto& first = selectors.back();
for (size_t i = 0; i < graph.size(); ++i) {
if (indegrees[i] == 0) {
buf[first.count++] = i;
if (method == MinHeap) {
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.push_back(selectors.back().select(*this, method));
}
selectors.back().select(*this, method);
}

TopologicalOrders& TopologicalOrders::operator++() {
Expand All @@ -117,4 +133,16 @@ TopologicalOrders& TopologicalOrders::operator++() {
return *this;
}

void TopologicalOrders::pushChoice(size_t choice) {
choiceHeap.push_back(choice);
std::push_heap(choiceHeap.begin(), choiceHeap.end(), std::greater<size_t>{});
}

size_t TopologicalOrders::popChoice() {
std::pop_heap(choiceHeap.begin(), choiceHeap.end(), std::greater<size_t>{});
auto choice = choiceHeap.back();
choiceHeap.pop_back();
return choice;
}

} // namespace wasm
86 changes: 82 additions & 4 deletions src/support/topological_orders.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
#ifndef wasm_support_topological_orders_h
#define wasm_support_topological_orders_h

#include <cassert>
#include <cstddef>
#include <optional>
#include <unordered_map>
#include <vector>

namespace wasm {
Expand All @@ -36,7 +38,8 @@ 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);
TopologicalOrders(const std::vector<std::vector<size_t>>& graph)
: TopologicalOrders(graph, InPlace) {}

TopologicalOrders begin() { return TopologicalOrders(graph); }
TopologicalOrders end() { return TopologicalOrders({}); }
Expand All @@ -47,11 +50,16 @@ struct TopologicalOrders {
bool operator!=(const TopologicalOrders& other) const {
return !(*this == other);
}
const std::vector<size_t>& operator*() { return buf; }
const std::vector<size_t>* operator->() { return &buf; }
const std::vector<size_t>& operator*() const { return buf; }
const std::vector<size_t>* operator->() const { return &buf; }
TopologicalOrders& operator++();
TopologicalOrders operator++(int) { return ++(*this); }

protected:
enum SelectionMethod { InPlace, MinHeap };
TopologicalOrders(const std::vector<std::vector<size_t>>& graph,
SelectionMethod method);

private:
// The input graph given as an adjacency list with edges from vertices to
// their dependent children.
Expand All @@ -64,6 +72,9 @@ struct TopologicalOrders {
// sequence of selected vertices followed by a sequence of possible choices
// for the next vertex.
std::vector<size_t> buf;
// When we are finding the minimal topological order, store the possible
// choices in this separate min-heap instead of directly in `buf`.
std::vector<size_t> choiceHeap;

// The state for tracking the possible choices for a single vertex in the
// output order.
Expand All @@ -78,19 +89,86 @@ struct TopologicalOrders {

// 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);
Selector select(TopologicalOrders& ctx, SelectionMethod method);

// 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);
};

void pushChoice(size_t);
size_t popChoice();

// 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;
};

// A utility for finding a single topological order of a graph.
struct TopologicalSort : private TopologicalOrders {
TopologicalSort(const std::vector<std::vector<size_t>>& graph)
: TopologicalOrders(graph) {}

const std::vector<size_t>& operator*() const {
return TopologicalOrders::operator*();
}
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor is identical to the parent, and this operator doesn't seem to modify anything either - I am sure I'm missing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, this is providing a small subset of the functionality of the superclass, packaged under a different name that is more appropriate for the common case of needing just one topological sort.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks... I was missing the word "private" on line 108. Makes sense now.


// A utility for finding the topological order that is as close as possible to
// the original order of elements. Internally uses a min-heap to choose the best
// available next element.
struct MinTopologicalSort : private TopologicalOrders {
MinTopologicalSort(const std::vector<std::vector<size_t>>& graph)
: TopologicalOrders(graph, MinHeap) {}

const std::vector<size_t>& operator*() const {
return TopologicalOrders::operator*();
}
};

// A utility that finds a topological sort of a graph with arbitrary element
// types.
template<typename T, typename TopoSort = TopologicalSort>
struct TopologicalSortOf {
std::vector<T> order;

// The value of the iterators must be a pair of an element and an iterable of
// its children.
template<typename It> TopologicalSortOf(It begin, It end) {
std::unordered_map<T, size_t> indices;
std::vector<T> 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.
std::vector<std::vector<size_t>> 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.
order.reserve(elements.size());
auto indexOrder = *TopoSort(indexGraph);
for (auto i : indexOrder) {
order.emplace_back(std::move(elements[i]));
}
}

const std::vector<T>& operator*() const { return order; }
};

// A utility that finds the minimum topological sort of a graph with arbitrary
// element types.
template<typename T>
using MinTopologicalSortOf = TopologicalSortOf<T, MinTopologicalSort>;

} // namespace wasm

#endif // wasm_support_topological_orders_h
55 changes: 55 additions & 0 deletions test/gtest/topological-orders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,58 @@ TEST(TopologicalOrdersTest, Diamond) {
};
EXPECT_EQ(results, expected);
}

TEST(MinTopologicalSortTest, Empty) {
Graph graph(0);
EXPECT_EQ(*MinTopologicalSort(graph), std::vector<size_t>{});
}

TEST(MinTopologicalSortTest, Unconstrained) {
Graph graph(3);
MinTopologicalSort order(graph);
std::vector<size_t> expected{0, 1, 2};
EXPECT_EQ(*MinTopologicalSort(graph), expected);
}

TEST(MinTopologicalSortTest, Reversed) {
Graph graph(3);
graph[2].push_back(1);
graph[1].push_back(0);
std::vector<size_t> expected{2, 1, 0};
EXPECT_EQ(*MinTopologicalSort(graph), expected);
}

TEST(MinTopologicalSortTest, OneBeforeZero) {
Graph graph(3);
graph[1].push_back(0);
// 2 last because it is greater than 1 and 0
std::vector<size_t> expected{1, 0, 2};
EXPECT_EQ(*MinTopologicalSort(graph), expected);
}

TEST(MinTopologicalSortTest, TwoBeforeOne) {
Graph graph(3);
graph[2].push_back(1);
// 0 first because it is less than 2 and 1
std::vector<size_t> expected{0, 2, 1};
EXPECT_EQ(*MinTopologicalSort(graph), expected);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add comments to these, e.g. for this one IIUC the point is to show that 2 is before 1, and the unconstrained 0 is smaller so it appears before them both.

}

TEST(MinTopologicalSortTest, TwoBeforeZero) {
Graph graph(3);
graph[2].push_back(0);
// 1 first because it is less than 2 and zero is not eligible.
std::vector<size_t> expected{1, 2, 0};
EXPECT_EQ(*MinTopologicalSort(graph), expected);
}

TEST(MinTopologicalSortTest, Strings) {
std::map<std::string, std::vector<std::string>> graph{
{"animal", {"mammal"}},
{"cat", {}},
{"dog", {}},
{"mammal", {"cat", "dog"}}};
std::vector<std::string> expected{"animal", "mammal", "cat", "dog"};
EXPECT_EQ(*MinTopologicalSortOf<std::string>(graph.begin(), graph.end()),
expected);
}