Jlm
TarjanSccTests.cpp
Go to the documentation of this file.
1 /*
2  * Copyright 2024 HÃ¥vard Krogstie <krogstie.havard@gmail.com>
3  * See COPYING for terms of redistribution.
4  */
5 
6 #include <gtest/gtest.h>
7 
8 #include <jlm/util/HashSet.hpp>
9 #include <jlm/util/TarjanScc.hpp>
10 
11 #include <iostream>
12 #include <optional>
13 #include <tuple>
14 #include <vector>
15 
16 // Used to represent graphs with no node unification
17 static size_t
18 Identity(size_t i)
19 {
20  return i;
21 }
22 
30 template<typename UnificationRootFunctor, typename SuccessorFunctor>
31 static void
33  size_t numNodes,
34  UnificationRootFunctor & unificationRoot,
35  SuccessorFunctor & successors,
36  size_t numSccs,
37  const std::vector<size_t> & sccIndex,
38  const std::vector<size_t> & reverseTopologicalOrder)
39 {
40  EXPECT_EQ(numNodes, sccIndex.size());
41 
42  // Check that all sccIndex are valid, and each SCC has at least one node
43  std::vector<size_t> numNodesInScc(numSccs, 0);
44  for (size_t i = 0; i < numNodes; i++)
45  {
46  auto node = unificationRoot(i);
47  EXPECT_LT(sccIndex[node], numSccs);
48  numNodesInScc[sccIndex[node]]++;
49  }
50  for (size_t i = 0; i < numSccs; i++)
51  EXPECT_GT(numNodesInScc[i], 0u);
52 
53  // Check that no edge in the graph points to an earlier SCC
54  for (size_t i = 0; i < numNodes; i++)
55  {
56  // Only consider unification roots
57  if (unificationRoot(i) != i)
58  continue;
59 
60  for (auto next : successors(i))
61  {
62  next = unificationRoot(next);
63 
64  // successor SCCs must have lower scc index
65  EXPECT_LE(sccIndex[next], sccIndex[i]);
66  }
67  }
68 
69  // Check that all unification roots appear exactly once in the topological order
70  jlm::util::HashSet<size_t> nodeInTopologicalOrder(
71  reverseTopologicalOrder.begin(),
72  reverseTopologicalOrder.end());
73  EXPECT_EQ(nodeInTopologicalOrder.Size(), reverseTopologicalOrder.size());
74 
75  for (size_t i = 0; i < numNodes; i++)
76  {
77  if (unificationRoot(i) == i)
78  EXPECT_TRUE(nodeInTopologicalOrder.Contains(i));
79  else
80  EXPECT_TRUE(!nodeInTopologicalOrder.Contains(i));
81  }
82 
83  // Check that the reverse topological order contains nodes with ascending sccIndex
84  for (size_t i = 1; i < reverseTopologicalOrder.size(); i++)
85  {
86  EXPECT_LE(sccIndex[reverseTopologicalOrder[i - 1]], sccIndex[reverseTopologicalOrder[i]]);
87  }
88 }
89 
90 TEST(TarjanSccTests, TestDag)
91 {
92  // Create a DAG, where each node is its own SCC
93  const size_t numNodes = 8;
94  std::vector<std::vector<size_t>> successors{
95  { 2 }, // 0's successors
96  { 2 }, // 1's successors
97  { 3, 4 }, // 2's successors
98  { 5, 6 }, // 3's successors
99  { 5 }, // 4's successors
100  { 6 }, // 5's successors
101  {}, // 6's successors
102  { 7 }, // 7's successors (fully disconnected, just a self-loop)
103  };
104 
105  auto GetSuccessors = [&](size_t node)
106  {
107  return successors[node];
108  };
109 
110  std::vector<size_t> sccIndex;
111  std::vector<size_t> reverseTopologicalOrder;
113  numNodes,
114  GetSuccessors,
115  sccIndex,
116  reverseTopologicalOrder);
118  numNodes,
119  Identity,
120  GetSuccessors,
121  numSccs,
122  sccIndex,
123  reverseTopologicalOrder);
124 
125  EXPECT_EQ(numSccs, numNodes);
126 }
127 
128 // Test a graph with some cycles, ensuring they become SCCs
129 TEST(TarjanSccTests, TestCycles)
130 {
131  const size_t numNodes = 7;
132  std::vector<std::vector<size_t>> successors{
133  { 1, 3 }, // 0's successors
134  { 2 }, // 1's successors
135  { 0 }, // 2's successors
136  { 4, 5 }, // 3's successors
137  { 0, 5 }, // 4's successors
138  {}, // 5's successors
139  { 2, 4, 5 }, // 6's successors
140  };
141 
142  auto GetSuccessors = [&](size_t node)
143  {
144  return successors[node];
145  };
146 
147  std::vector<size_t> sccIndex;
148  std::vector<size_t> reverseTopologicalOrder;
150  numNodes,
151  GetSuccessors,
152  sccIndex,
153  reverseTopologicalOrder);
155  numNodes,
156  Identity,
157  GetSuccessors,
158  numSccs,
159  sccIndex,
160  reverseTopologicalOrder);
161 
162  EXPECT_EQ(numSccs, 3u);
163  // 5 has to be at the end
164  EXPECT_EQ(sccIndex[5], 0u);
165  EXPECT_EQ(reverseTopologicalOrder[0], 5u);
166  // 6 has to be at the beginning
167  EXPECT_EQ(sccIndex[6], 2u);
168  EXPECT_EQ(reverseTopologicalOrder[numNodes - 1], 6u);
169  // The rest belong to the middle SCC
170  for (size_t i = 0; i < 5; i++)
171  EXPECT_EQ(sccIndex[i], 1u);
172 }
173 
194 static std::tuple<size_t, size_t, std::vector<size_t>>
195 CreateDiamondChain(size_t knots, std::optional<std::pair<size_t, size_t>> extraEdge)
196 {
197  EXPECT_GE(knots, 2u);
198  const size_t numNodes = 3 * knots - 2;
199  std::vector<std::vector<size_t>> successors(numNodes);
200 
201  // Use a permutation to "randomize" the node indices
202  std::vector<size_t> perm(numNodes);
203  for (size_t i = 0; i < numNodes; i++)
204  {
205  // 3 and numNodes is always coprime, so this will assign each node a unique new index
206  perm[i] = (i * 3) % numNodes;
207  }
208 
209  for (size_t knot = 0; knot + 3 < numNodes; knot += 3)
210  {
211  successors[perm[knot]].push_back(perm[knot + 1]);
212  successors[perm[knot]].push_back(perm[knot + 2]);
213  successors[perm[knot + 1]].push_back(perm[knot + 3]);
214  successors[perm[knot + 2]].push_back(perm[knot + 3]);
215  }
216 
217  if (extraEdge)
218  {
219  EXPECT_LT(extraEdge->first, numNodes);
220  EXPECT_LT(extraEdge->second, numNodes);
221  // Leave 5 nodes on the end, and 5 at the beginning, out of the big loop
222  successors[perm[extraEdge->first]].push_back(perm[extraEdge->second]);
223  }
224 
225  auto GetSuccessors = [&](size_t node)
226  {
227  return successors[node];
228  };
229 
230  std::vector<size_t> sccIndex;
231  std::vector<size_t> reverseTopologicalOrder;
233  numNodes,
234  GetSuccessors,
235  sccIndex,
236  reverseTopologicalOrder);
238  numNodes,
239  Identity,
240  GetSuccessors,
241  numSccs,
242  sccIndex,
243  reverseTopologicalOrder);
244 
245  std::vector<size_t> unshuffledNodeIndex(numNodes);
246  for (size_t i = 0; i < numNodes; i++)
247  {
248  unshuffledNodeIndex[i] = sccIndex[perm[i]];
249  }
250 
251  return { numNodes, numSccs, std::move(unshuffledNodeIndex) };
252 }
253 
254 TEST(TarjanSccTests, TestSimpleDiamondChain)
255 {
256  auto [numNodes, numSccs, sccIndex] = CreateDiamondChain(100, std::nullopt);
257  EXPECT_EQ(numNodes, numSccs);
258 }
259 
260 TEST(TarjanSccTests, TestDiamondChainWithForwardEdge)
261 {
262  // Forward edges do not create any cycles
263  std::pair<size_t, size_t> forwardEdge{ 5, 200 };
264  auto [numNodes, numSccs, sccIndex] = CreateDiamondChain(100, forwardEdge);
265  EXPECT_EQ(numNodes, numSccs);
266 }
267 
268 TEST(TarjanSccTests, TestDiamondChainWithBackEdge)
269 {
270  // Back edges create one big SCC, the rest are single node SCCs
271  std::pair<size_t, size_t> backEdge{ 3 * 50, 3 * 3 };
272  auto [numNodes, numSccs, sccIndex] = CreateDiamondChain(100, backEdge);
273 
274  std::cerr << "numSccs: " << numSccs << std::endl;
275  std::cerr << "numNodes: " << numNodes << std::endl;
276  EXPECT_EQ(numSccs, backEdge.second + numNodes - backEdge.first);
277 
278  auto largeScc = sccIndex[backEdge.second];
279 
280  // All nodes before the back edge head have a higher SCC index
281  for (size_t i = 0; i < backEdge.second; i++)
282  EXPECT_GT(sccIndex[i], largeScc);
283 
284  // All nodes after the back edge tail have a lower SCC index
285  for (size_t i = backEdge.first + 1; i < numNodes; i++)
286  EXPECT_LT(sccIndex[i], largeScc);
287 
288  // All nodes between the back edge tail and head have same SCC index
289  for (size_t i = backEdge.second; i <= backEdge.first; i++)
290  EXPECT_EQ(sccIndex[i], largeScc);
291 }
292 
293 // During SCC creation, the function should query a node for its successors at most twice
294 TEST(TarjanSccTests, TestVisitEachNodeTwice)
295 {
296  const size_t numNodes = 5;
297  std::vector<std::vector<size_t>> successors{
298  { 1, 2 }, // 0's successors
299  { 2, 3 }, // 1's successors
300  { 1, 3 }, // 2's successors
301  {}, // 3's successors
302  { 4 } // 4's successors
303  };
304  // The graph looks like a 0->(12)->3 diamond with edges between 1 and 2, and a lone node 4
305 
306  std::vector<size_t> successorsQueried(numNodes, 0);
307  auto GetSuccessors = [&](size_t node)
308  {
309  EXPECT_LT(node, numNodes);
310  successorsQueried[node]++;
311  return successors[node];
312  };
313 
314  std::vector<size_t> sccIndex;
315  std::vector<size_t> reverseTopologicalOrder;
317  numNodes,
318  GetSuccessors,
319  sccIndex,
320  reverseTopologicalOrder);
321 
322  EXPECT_EQ(numSccs, 4u);
323  for (size_t timesQueried : successorsQueried)
324  EXPECT_LE(timesQueried, 2u);
325 
326  // Validate the produced SCC DAG as well, but do it last, as this function calls GetSuccessors.
328  numNodes,
329  Identity,
330  GetSuccessors,
331  numSccs,
332  sccIndex,
333  reverseTopologicalOrder);
334 }
335 
336 TEST(TarjanSccTests, TestUnifiedNodes)
337 {
338  // Each node with index >= 5 has a unification root equal to index - 5
339  const size_t numNodes = 10;
340  std::vector<std::vector<size_t>> successors{
341  { 1, 1 + 5, 2 }, // 0's successors
342  { 2 + 5, 3 }, // 1's successors
343  { 1 + 5, 3 + 5 }, // 2's successors
344  {}, // 3's successors
345  { 4, 4 + 5 } // 4's successors
346  };
347  // The graph looks like a 0->(12)->3 diamond with edges between 1 and 2, and a lone node 4
348 
349  auto GetUnificationRoot = [&](size_t node)
350  {
351  if (node >= 5)
352  return node - 5;
353  return node;
354  };
355 
356  auto GetSuccessors = [&](size_t node)
357  {
358  EXPECT_LT(node, 5u);
359  return successors[node];
360  };
361 
362  std::vector<size_t> sccIndex;
363  std::vector<size_t> reverseTopologicalOrder;
365  numNodes,
366  GetUnificationRoot,
367  GetSuccessors,
368  sccIndex,
369  reverseTopologicalOrder);
370 
371  EXPECT_EQ(numSccs, 4u);
372 
373  // Validate the produced SCC DAG as well, but do it last, as this function calls GetSuccessors.
375  numNodes,
376  GetUnificationRoot,
377  GetSuccessors,
378  numSccs,
379  sccIndex,
380  reverseTopologicalOrder);
381 }
static std::tuple< size_t, size_t, std::vector< size_t > > CreateDiamondChain(size_t knots, std::optional< std::pair< size_t, size_t >> extraEdge)
static void ValidateTopologicalOrderAndSccIndices(size_t numNodes, UnificationRootFunctor &unificationRoot, SuccessorFunctor &successors, size_t numSccs, const std::vector< size_t > &sccIndex, const std::vector< size_t > &reverseTopologicalOrder)
TEST(TarjanSccTests, TestDag)
static size_t Identity(size_t i)
std::size_t Size() const noexcept
Definition: HashSet.hpp:187
bool Contains(const ItemType &item) const noexcept
Definition: HashSet.hpp:150
NodeType FindStronglyConnectedComponents(NodeType numNodes, UnificationRootFunctor unificationRoot, SuccessorFunctor &successors, std::vector< NodeType > &sccIndex, std::vector< NodeType > &reverseTopologicalOrder)
Definition: TarjanScc.hpp:52