mirror of
https://forge.sourceware.org/marek/gcc.git
synced 2026-02-22 12:00:11 -05:00
2051 lines
61 KiB
C++
2051 lines
61 KiB
C++
/* Find prime paths
|
|
Copyright (C) 2024-2026 Free Software Foundation, Inc.
|
|
|
|
This file is part of GCC.
|
|
|
|
GCC is free software; you can redistribute it and/or modify it under
|
|
the terms of the GNU General Public License as published by the Free
|
|
Software Foundation; either version 3, or (at your option) any later
|
|
version.
|
|
|
|
GCC is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with GCC; see the file COPYING3. If not see
|
|
<http://www.gnu.org/licenses/>. */
|
|
|
|
#include "config.h"
|
|
#include "system.h"
|
|
#include "coretypes.h"
|
|
#include "obstack.h"
|
|
#include "sbitmap.h"
|
|
#include "vec.h"
|
|
#include "graphds.h"
|
|
#include "selftest.h"
|
|
|
|
namespace
|
|
{
|
|
|
|
/* Counter for the number of candidate paths to generate before giving up. It
|
|
is neater to use a global because it has to be checked deep in helper
|
|
functions, which may also suffer under path explosion. It is a heuristic
|
|
guaranteed to overshoot the number of actual paths (which is difficult to
|
|
estimate), and is intended to give up on (absurdly) large functions with
|
|
millions of paths, not be a high fidelity rejection mechanism. This is
|
|
essentially an exception. */
|
|
size_t approx_limit;
|
|
|
|
/* Reset the threshold to APPROX when a function is too complex and finding
|
|
paths should give up. */
|
|
void
|
|
limit_reset (size_t approx)
|
|
{
|
|
approx_limit = approx;
|
|
}
|
|
|
|
/* Record approximately APPROX new paths. Returns true if the limit is
|
|
exceeded and coverage should give up. */
|
|
bool
|
|
limit_checked_add (size_t approx)
|
|
{
|
|
approx_limit -= approx < approx_limit ? approx : approx_limit;
|
|
return approx_limit == 0;
|
|
}
|
|
|
|
/* Check if adding APPROX would exceed the path limit. This is necessary when
|
|
(pessimistically counted) trie insertions would exceed the limit and yields
|
|
a partial result, when the path count would drop below the limit again once
|
|
redundancies are eliminated. */
|
|
bool
|
|
limit_exceed_p (size_t approx)
|
|
{
|
|
return approx > approx_limit;
|
|
}
|
|
|
|
/* A silly RAII wrapper for struct graph. The prime_paths function has multiple
|
|
returns, and this helps reliably clean up. */
|
|
struct auto_graph
|
|
{
|
|
auto_graph (struct graph *graph) : ptr (graph) {}
|
|
auto_graph (const auto_graph &) = delete;
|
|
~auto_graph () { free_graph (ptr); }
|
|
operator struct graph* () { return ptr; }
|
|
struct graph* operator -> () { return ptr; }
|
|
graph *ptr;
|
|
};
|
|
|
|
/* A silly RAII wrapper for an sbitmap vector. The prime_paths function has
|
|
multiple returns, and this helps reliably clean up. */
|
|
struct auto_sbitmap_vector
|
|
{
|
|
auto_sbitmap_vector (sbitmap *s) : ptr (s) {}
|
|
auto_sbitmap_vector (const auto_sbitmap_vector &) = delete;
|
|
~auto_sbitmap_vector () { sbitmap_vector_free (ptr); }
|
|
operator sbitmap* () { return ptr; }
|
|
sbitmap* ptr;
|
|
};
|
|
|
|
/* A silly RAII wrpaper for automatically releasing a vec<vec<int>>. */
|
|
struct auto_vec_vec : vec<vec<int>>
|
|
{
|
|
auto_vec_vec () = default;
|
|
auto_vec_vec (vec<vec<int>> v) : vec<vec<int>>(v) {}
|
|
~auto_vec_vec () { release_vec_vec (*this); }
|
|
};
|
|
|
|
/* A silly RAII wrpaper for automatically releasing a vec<vec<vec<int>>>. */
|
|
struct auto_vec_vec_vec : vec<vec<vec<int>>>
|
|
{
|
|
~auto_vec_vec_vec ()
|
|
{
|
|
for (vec<vec<int>> &v : *this)
|
|
release_vec_vec (v);
|
|
release ();
|
|
}
|
|
};
|
|
|
|
/* A trivial key/value pair for a short linear map type. */
|
|
struct xpair
|
|
{
|
|
int key;
|
|
unsigned val;
|
|
};
|
|
|
|
/* A node in a trie, optimized for mid-sized alphabets possibly larger than 256
|
|
but not much more. Finding the prime paths ends up creating a large amount
|
|
of these nodes so space and access costs matters a lot.
|
|
|
|
The node does not explicitly store its own key (CFG vertex ID/basic block
|
|
index), nor does it store pointers to its successors. Rather, it stores the
|
|
key+offset pairs for its successors the root trie object, and in a sense
|
|
behaves like near pointers. This makes the trie vertices small and
|
|
reloctable, and removes the need for pointer chasing when releasing the trie.
|
|
|
|
The union of near/far is essentially a short-vector optimization, switching
|
|
to a heap-allocated vector when necessary. This happens relatively rarely
|
|
(usually maxes out at 1-2%), and the vertices that have more than 2 sucessors
|
|
also tend to have more than 4. The root vertex tends to use the dynamic
|
|
vector because the subpaths are recorded as the successors of the root.
|
|
|
|
Conceptually, this is a small map from vertex-id -> index and the API is
|
|
modelled as such. The insert and search functions are unrolled by hand when
|
|
using the small vector. This has a noticable performance impact on insert in
|
|
particular, and is not too complex since we know we are limited to 2
|
|
elements.
|
|
|
|
Vertices are tagged with endofpath and inserted. If endofpath is set, the
|
|
path from the root to this vertex is a complete path. If inserted is set
|
|
then the vertex is a part of proper path (one given to insert) and not
|
|
generated as a suffix. For example:
|
|
|
|
insert ([2 4 6])
|
|
insert ([9 7 2 4 6])
|
|
insert ([2 4 6 8])
|
|
|
|
The inserted flags for [2 4 6] are not cleared, because otherwise [2 4 6 8]
|
|
would be dropped when only following inserted vertices. The endofpath flag
|
|
in [2 4 6] is cleared when the suffixes of [9 7 2 4 6] are inserted.
|
|
|
|
The node will be inserted into a vec, and should be trivial. Instances
|
|
should be value-initialized to zero-intialized state. */
|
|
struct trie_node
|
|
{
|
|
unsigned length () const
|
|
{ return !heaped ? len : far.length (); }
|
|
|
|
const xpair *begin () const
|
|
{ return !heaped ? near : far.begin (); }
|
|
|
|
const xpair *end () const
|
|
{ return !heaped ? (near + len) : far.end (); }
|
|
|
|
/* Get the ith successor. This is used for traversal and not lookup, and
|
|
should only be used by the iterator. */
|
|
const xpair &at (unsigned i) const
|
|
{ return !heaped ? near[i] : far[i]; }
|
|
|
|
const xpair *get (int key) const;
|
|
void put (int key, unsigned val);
|
|
|
|
unsigned near_lower_bound (int key) const;
|
|
|
|
/* Experimentally I found that using a union with 2 elements in the near array
|
|
to be faster than 4 or without the union (very slightly). A lot of trie
|
|
vertices will be created, and vast majority of vertices will have 1 or 2
|
|
successors (straight line or if-then), and the cost of size and copying
|
|
adds up. */
|
|
union
|
|
{
|
|
xpair near[2];
|
|
vec<xpair> far;
|
|
};
|
|
unsigned len : 8;
|
|
unsigned endofpath : 1;
|
|
unsigned inserted : 1;
|
|
unsigned heaped : 1;
|
|
};
|
|
|
|
/* Compare LHS.key < RHS.key, for use with vec.lower_bound. */
|
|
bool
|
|
xpair_less (const xpair& lhs, const xpair& rhs)
|
|
{
|
|
return lhs.key < rhs.key;
|
|
}
|
|
|
|
/* Compare LHS.key to RHS.key, for use with vec.bsearch. */
|
|
int
|
|
xpair_cmp (const void *lhs, const void *rhs)
|
|
{
|
|
return ((const xpair*)lhs)->key - ((const xpair*)rhs)->key;
|
|
}
|
|
|
|
/* Get a pointer to the element at KEY if it exists, otherwise NULL. */
|
|
const xpair*
|
|
trie_node::get (int key) const
|
|
{
|
|
if (!heaped)
|
|
{
|
|
if (len == 0) return NULL;
|
|
if (len >= 1 && key == near[0].key) return near + 0;
|
|
if (len >= 2 && key == near[1].key) return near + 1;
|
|
return NULL;
|
|
}
|
|
else
|
|
{
|
|
xpair kv;
|
|
kv.key = key;
|
|
return const_cast <vec<xpair>&> (far).bsearch (&kv, xpair_cmp);
|
|
}
|
|
}
|
|
|
|
/* Put ("emplace") VAL at KEY, extending the paths that pass through this
|
|
vertex. This function assumes that KEY is not already a successor, and does
|
|
not perform this check. get () should be called and checked for NULL putting
|
|
with this function. Put maintains the order of the successors. */
|
|
void
|
|
trie_node::put (int key, unsigned val)
|
|
{
|
|
xpair kv;
|
|
kv.key = key;
|
|
kv.val = val;
|
|
if (!heaped)
|
|
{
|
|
const unsigned i = near_lower_bound (key);
|
|
if (len < 2)
|
|
{
|
|
near[1] = near[0];
|
|
near[i] = kv;
|
|
len += 1;
|
|
}
|
|
else
|
|
{
|
|
/* This insert is the 3rd element, which does not fit in the embedded
|
|
storage, so we must create a vector and convert to a far node. */
|
|
vec<xpair> xs {};
|
|
xs.reserve (13);
|
|
xs.quick_grow (3);
|
|
gcc_checking_assert (i <= 2);
|
|
if (i == 0)
|
|
{
|
|
xs[0] = kv;
|
|
xs[1] = near[0];
|
|
xs[2] = near[1];
|
|
}
|
|
else if (i == 1)
|
|
{
|
|
xs[0] = near[0];
|
|
xs[1] = kv;
|
|
xs[2] = near[1];
|
|
}
|
|
else
|
|
{
|
|
xs[0] = near[0];
|
|
xs[1] = near[1];
|
|
xs[2] = kv;
|
|
}
|
|
|
|
far = xs;
|
|
heaped = 1;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
const unsigned i = far.lower_bound (kv, xpair_less);
|
|
far.safe_insert (i, kv);
|
|
}
|
|
}
|
|
|
|
/* Get the index to the last element that compares less than KEY, similar to
|
|
vec.lower_bound. This assumes the near vector is active, and is for internal
|
|
use. */
|
|
unsigned
|
|
trie_node::near_lower_bound (int key) const
|
|
{
|
|
gcc_checking_assert (!heaped);
|
|
if (len == 0) return 0;
|
|
if (len >= 1 && key < near[0].key) return 0;
|
|
if (len >= 2 && key < near[1].key) return 1;
|
|
return len;
|
|
}
|
|
|
|
/* The trie is a major workhorse for this algorithm. It has two key properties
|
|
- set-like subpath elimination and sorted output.
|
|
|
|
Many evaluated paths will be non-prime, that is, a sequence of vertices that
|
|
is also fully embedded in a longer sequence of vertices. For example the
|
|
path [3 4 5 8] is a subpath of both [2 3 4 5 8] and [3 4 5 8 10]. The
|
|
insert_with_suffix function maintains this property so that inserting a
|
|
subpath into the trie is effectively a no-op, and inserting a superpath will
|
|
effectively remove (unmark) the subpath. Sometimes it can be guaranteed that
|
|
no redundant (subpaths) will be generated, in which case the insert function
|
|
can be used. The insert function is really only set insert, only becoming a
|
|
no-op for identical paths, which will be a lot faster.
|
|
|
|
Paths can be extracted with an iterator, which will output paths in
|
|
lexicographically sorted order. This is an important property because the
|
|
index of a path in the sorted set will be used by the coverage to record when
|
|
a path is taken and completed. The iterator has different behavior than the
|
|
standard C++ iterators, and to avoid mixups the interface is deliberately
|
|
different. The iterator has a (large) stack which is not cheap to copy, and
|
|
if the stack is shallow copied it would mean iterator copies have non-local
|
|
effects. */
|
|
struct trie
|
|
{
|
|
struct iter;
|
|
trie ();
|
|
trie (const trie &o);
|
|
trie (trie &&o);
|
|
~trie ();
|
|
|
|
bool insert (const vec<int>&);
|
|
bool insert (const array_slice<const int>);
|
|
bool hard_insert (const array_slice<const int>);
|
|
bool insert_with_suffix (const array_slice<const int>);
|
|
bool insert_suffix (const array_slice<const int>);
|
|
|
|
void merge (const trie&);
|
|
|
|
iter paths (vec<int>&) const;
|
|
iter paths (vec<int>&, int from) const;
|
|
|
|
vec<vec<int>> paths () const;
|
|
|
|
size_t size () const { return len; }
|
|
|
|
vec<trie_node> vertices;
|
|
size_t len;
|
|
|
|
/* An iterator for the paths of the trie. The iterator yields all paths in
|
|
lexicographical order. The iterator will be invalidated on any insertion
|
|
into the trie. The iterator should not be constructed directly, but
|
|
through the paths functions on the trie. It is essentially an explicit
|
|
stack depth-first traversal.
|
|
|
|
The iter fills a user-provided buffer which should only be read as a when
|
|
the iter is active. Whenever next returns true the buffer is filled with
|
|
the current path. Uses will generally look like this:
|
|
|
|
vec<int> path {};
|
|
auto iter = trie.paths (path);
|
|
while (iter.next ())
|
|
use_path (path);
|
|
*/
|
|
struct iter
|
|
{
|
|
iter (vec<int>&, const vec<trie_node>&);
|
|
iter (int first, vec<int>& path, const vec<trie_node> &vertices);
|
|
~iter ()
|
|
{ stack.release (); }
|
|
|
|
bool next ();
|
|
bool next (int);
|
|
bool next (bool);
|
|
|
|
|
|
/* This is the analog of the stack frame when implementing a recursive
|
|
depth-first path traversal and collecting paths to the leafs:
|
|
|
|
for (auto successor : vertex[ix])
|
|
{
|
|
path.push (successor.value);
|
|
collect (successor.ix, successor.begin, successor.end, path)
|
|
path.pop ();
|
|
}
|
|
|
|
Using size_t + 2x unsigned helped make the frame more compact and faster
|
|
than pointers. */
|
|
struct frame
|
|
{
|
|
/* The index of this frame's vertex, so that vertices[ix]. */
|
|
size_t ix;
|
|
/* The index of the current active successor of vertices[ix]. */
|
|
unsigned itr;
|
|
/* The end of vertices[ix] successors. When itr == end, vertex[ix] is
|
|
exhausted. */
|
|
unsigned end;
|
|
};
|
|
|
|
/* User provided buffer to fill with the paths. */
|
|
vec<int> &path;
|
|
/* Direct reference to the trie vertices vector. */
|
|
const vec<trie_node> &vertices;
|
|
/* The call stack. */
|
|
vec<frame> stack;
|
|
/* Yield flag. If this is true then next () is permitted to and return a
|
|
new value. If this is false, a value has already been yielded and next
|
|
must first reset the state before building the next value. */
|
|
bool yield = true;
|
|
|
|
iter (const iter& o) : path (o.path), vertices (o.vertices),
|
|
stack (o.stack.copy ()), yield (o.yield)
|
|
{
|
|
}
|
|
|
|
/* Delete the copy assignment as the iter stores references and would cause
|
|
bad bugs. It is not necessary for the iterator to work well. To support
|
|
these the references would need to be (explicit) pointers. */
|
|
iter& operator = (const iter& o) = delete;
|
|
};
|
|
};
|
|
|
|
/* Construct an iterator filling BUFFER. */
|
|
trie::iter
|
|
trie::paths (vec<int> &buffer) const
|
|
{
|
|
buffer.truncate (0);
|
|
return iter (buffer, vertices);
|
|
}
|
|
|
|
/* Construct an iterator filling BUFFER for paths starting at FROM. */
|
|
trie::iter
|
|
trie::paths (vec<int>& buffer, int from) const
|
|
{
|
|
buffer.truncate (0);
|
|
return iter (from, buffer, vertices);
|
|
}
|
|
|
|
/* Default construct a new trie. */
|
|
trie::trie () : vertices (vec<trie_node> {}), len (0)
|
|
{
|
|
vertices.safe_push (trie_node {});
|
|
vertices[0].inserted = true;
|
|
}
|
|
|
|
/* Copy construct a new trie. */
|
|
trie::trie (const trie &o) : vertices (o.vertices.copy ()), len (o.len)
|
|
{
|
|
}
|
|
|
|
/* Move construct a new trie. */
|
|
trie::trie (trie &&o) : vertices (o.vertices), len (o.len)
|
|
{
|
|
o.vertices = {};
|
|
o.len = 0;
|
|
}
|
|
|
|
/* Destroy a trie and release all the heaped resources. */
|
|
trie::~trie ()
|
|
{
|
|
for (trie_node &v : vertices)
|
|
if (v.heaped)
|
|
v.far.release ();
|
|
vertices.release ();
|
|
}
|
|
|
|
/* Insert PATH into the trie. */
|
|
bool
|
|
trie::insert (const vec<int>& path)
|
|
{
|
|
return insert (array_slice <const int> (path));
|
|
}
|
|
|
|
/* Insert the PATH into the trie. Duplicate entries will not be entered twice.
|
|
If PATH is a subpath of another path this will not be detected or if there is
|
|
a path previously inserted that is a subpath of PATH then this redundancy
|
|
will not be eliminated. For that behavior, call insert_with_suffix. */
|
|
bool
|
|
trie::insert (array_slice<const int> path)
|
|
{
|
|
size_t index = 0;
|
|
size_t partition = 0;
|
|
for (const int v : path)
|
|
{
|
|
trie_node ¤t = vertices[index];
|
|
current.inserted = true;
|
|
partition++;
|
|
|
|
const auto *xp = current.get (v);
|
|
if (xp)
|
|
{
|
|
index = xp->val;
|
|
}
|
|
else
|
|
{
|
|
/* A new vertex on this path has been created, which means the rest of
|
|
the path will also have to be created. Drain the path and create
|
|
the remaining vertices in a single operation. */
|
|
unsigned ix = vertices.length ();
|
|
current.put (v, ix);
|
|
current.endofpath = false;
|
|
|
|
array_slice<const int> tail (path.begin () + partition,
|
|
path.size () - partition);
|
|
vertices.safe_grow_cleared (1 + ix + tail.size ());
|
|
|
|
for (const int v : tail)
|
|
{
|
|
trie_node &last = vertices[ix];
|
|
ix += 1;
|
|
last.put (v, ix);
|
|
last.inserted = true;
|
|
}
|
|
|
|
vertices.last ().endofpath = true;
|
|
vertices.last ().inserted = true;
|
|
len += 1;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* hard_insert is like insert, except it does not overwrite any endofpath flags,
|
|
and records the endofpath flag even when a superpath of PATH has been
|
|
inserted previously. This effectively disables subpath elimination. */
|
|
bool
|
|
trie::hard_insert (array_slice<const int> path)
|
|
{
|
|
size_t index = 0;
|
|
size_t partition = 0;
|
|
for (const int v : path)
|
|
{
|
|
trie_node ¤t = vertices[index];
|
|
current.inserted = true;
|
|
partition++;
|
|
|
|
const auto *xp = current.get (v);
|
|
if (xp)
|
|
{
|
|
index = xp->val;
|
|
}
|
|
else
|
|
{
|
|
unsigned ix = vertices.length ();
|
|
current.put (v, ix);
|
|
|
|
array_slice<const int> tail (path.begin () + partition,
|
|
path.size () - partition);
|
|
vertices.safe_grow_cleared (1 + ix + tail.size ());
|
|
|
|
for (const int v : tail)
|
|
{
|
|
trie_node &last = vertices[ix];
|
|
ix += 1;
|
|
last.put (v, ix);
|
|
last.inserted = true;
|
|
}
|
|
|
|
vertices.last ().endofpath = true;
|
|
vertices.last ().inserted = true;
|
|
len += 1;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
vertices[index].endofpath = true;
|
|
return false;
|
|
}
|
|
|
|
/* Insert a suffixes of PATH. This is identical to insert except that it is
|
|
assumed that PATH is a subpath, and that the inserted path should clear the
|
|
inserted and endofpath flags. This function should only be called by
|
|
insert_with_suffix. */
|
|
bool
|
|
trie::insert_suffix (array_slice<const int> path)
|
|
{
|
|
size_t index = 0;
|
|
size_t partition = 0;
|
|
for (const int v : path)
|
|
{
|
|
trie_node ¤t = vertices[index];
|
|
current.endofpath = false;
|
|
partition++;
|
|
|
|
const auto *xp = current.get (v);
|
|
if (xp)
|
|
{
|
|
index = xp->val;
|
|
}
|
|
else
|
|
{
|
|
/* A new vertex on this path has been created, which means the rest of
|
|
the path will also have to be created. Drain the path and create
|
|
the remaining vertices in a single operation. */
|
|
unsigned ix = vertices.length ();
|
|
current.put (v, ix);
|
|
|
|
array_slice<const int> tail (path.begin () + partition,
|
|
path.size () - partition);
|
|
vertices.safe_grow_cleared (1 + ix + tail.size ());
|
|
|
|
for (const int v : tail)
|
|
{
|
|
vertices[ix].put (v, ix + 1);
|
|
ix += 1;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
vertices[index].endofpath = false;
|
|
return false;
|
|
}
|
|
|
|
/* Insert the paths from OTHER into this trie. */
|
|
void
|
|
trie::merge (const trie& other)
|
|
{
|
|
auto_vec<int, 32> p {};
|
|
iter itr = other.paths (p);
|
|
while (itr.next ())
|
|
insert_with_suffix (p);
|
|
}
|
|
|
|
/* Insert PATH and all its subpaths into the trie. This function implements the
|
|
redundancy property of the trie - if an inserted path is either a subpath or
|
|
superpath of some other path then only the longest will keep its inserted
|
|
flag. */
|
|
bool
|
|
trie::insert_with_suffix (array_slice<const int> path)
|
|
{
|
|
bool inserted = insert (path);
|
|
path = array_slice<const int> (path.begin () + 1, path.size () - 1);
|
|
while (inserted && !path.empty ())
|
|
{
|
|
inserted = insert_suffix (path);
|
|
path = array_slice<const int> (path.begin () + 1, path.size () - 1);
|
|
}
|
|
return inserted;
|
|
}
|
|
|
|
/* Convert the paths of a trie to a vec-of-vec. */
|
|
vec<vec<int>>
|
|
trie::paths () const
|
|
{
|
|
auto_vec<int> path {};
|
|
vec<vec<int>> all {};
|
|
auto iter = paths (path);
|
|
while (iter.next ())
|
|
all.safe_push (path.copy ());
|
|
return all;
|
|
}
|
|
|
|
/* Create an iterator over VERTICES with the caller-provided buffer PATH. */
|
|
trie::iter::iter (vec<int> &path, const vec<trie_node> &vertices) : path (path),
|
|
vertices (vertices), stack (vec<frame> {})
|
|
{
|
|
gcc_checking_assert (!vertices.is_empty ());
|
|
stack.reserve (13);
|
|
frame f;
|
|
f.ix = 0;
|
|
f.itr = 0;
|
|
f.end = vertices[0].length ();
|
|
stack.quick_push (f);
|
|
}
|
|
|
|
/* Create an iterator over VERTICES with the caller-provided buffer PATH for all
|
|
paths and subpaths (suffixes) starting in FROM. Note that FROM will not be
|
|
in the output buffer PATH, mainly because non-rooted paths are used when
|
|
splicing with paths that end in FROM. */
|
|
trie::iter::iter (int from, vec<int> &path, const vec<trie_node> &vertices) :
|
|
path (path), vertices (vertices), stack (vec<frame> {})
|
|
{
|
|
gcc_checking_assert (!vertices.is_empty ());
|
|
stack.reserve (13);
|
|
|
|
auto *xp = vertices[0].get (from);
|
|
if (!xp)
|
|
{
|
|
/* No paths start with FROM, so construct an iterator where next () always
|
|
returns false. */
|
|
frame f;
|
|
f.ix = 0;
|
|
f.itr = 0;
|
|
f.end = 0;
|
|
stack.safe_push (f);
|
|
return;
|
|
}
|
|
|
|
frame f;
|
|
f.ix = xp->val;
|
|
f.itr = 0;
|
|
f.end = vertices[f.ix].length ();
|
|
stack.safe_push (f);
|
|
}
|
|
|
|
/* Find the next complete prime path in the trie and write it to the caller's
|
|
buffer. Returns true if a path is written and false if the iterator is
|
|
exhausted, in which case no path is written and the contents of the buffer is
|
|
garbage. */
|
|
bool
|
|
trie::iter::next ()
|
|
{
|
|
while (true)
|
|
{
|
|
frame &top = stack.last ();
|
|
const trie_node &vertex = vertices[top.ix];
|
|
|
|
if (vertex.endofpath && yield
|
|
&& (top.itr != top.end || vertex.length () == 0))
|
|
{
|
|
yield = false;
|
|
return true;
|
|
}
|
|
|
|
yield = true;
|
|
|
|
if (top.itr != top.end)
|
|
{
|
|
const xpair succ = vertex.at (top.itr);
|
|
const trie_node &next = vertices[succ.val];
|
|
top.itr++;
|
|
|
|
if (!next.inserted)
|
|
continue;
|
|
|
|
frame f {};
|
|
f.ix = succ.val;
|
|
f.itr = 0;
|
|
f.end = next.length ();
|
|
path.safe_push (succ.key);
|
|
stack.safe_push (f);
|
|
}
|
|
else
|
|
{
|
|
stack.pop ();
|
|
if (stack.is_empty ())
|
|
return false;
|
|
path.pop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Find the next path in the trie that would continue (but does not include)
|
|
LIMIT. If the trie contains the paths [2 4 6 8 9] [2 4 6 8 10] and [2 4 5
|
|
8], iter.next (8) would yield [2 4 6] and [2 4 5]. Returns true if a path is
|
|
written and false if the iterator is exhausted, in which case no path is
|
|
written and the contents of the buffer is garbage. */
|
|
bool
|
|
trie::iter::next (int limit)
|
|
{
|
|
while (true)
|
|
{
|
|
frame &top = stack.last ();
|
|
const trie_node &vertex = vertices[top.ix];
|
|
|
|
if (yield && top.itr != top.end)
|
|
{
|
|
const xpair succ = vertex.at (top.itr);
|
|
const trie_node &next = vertices[succ.val];
|
|
const int key = succ.key;
|
|
const int val = succ.val;
|
|
top.itr++;
|
|
|
|
if (!next.inserted)
|
|
continue;
|
|
|
|
if (key == limit)
|
|
{
|
|
if (path.is_empty ())
|
|
continue;
|
|
yield = false;
|
|
return true;
|
|
}
|
|
|
|
frame f {};
|
|
f.ix = val;
|
|
f.itr = 0;
|
|
f.end = next.length ();
|
|
path.safe_push (key);
|
|
stack.safe_push (f);
|
|
}
|
|
else
|
|
{
|
|
yield = true;
|
|
stack.pop ();
|
|
if (stack.is_empty ())
|
|
return false;
|
|
path.pop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Find the next path in among all paths including subpaths/suffixes. This is
|
|
mainly useful in combination with trie.paths (from) for finding the paths
|
|
that go through some vertex. */
|
|
bool
|
|
trie::iter::next (bool)
|
|
{
|
|
while (true)
|
|
{
|
|
frame &top = stack.last ();
|
|
const trie_node &vertex = vertices[top.ix];
|
|
|
|
if (yield && vertex.length () == 0)
|
|
{
|
|
yield = false;
|
|
return true;
|
|
}
|
|
|
|
yield = true;
|
|
|
|
if (top.itr != top.end)
|
|
{
|
|
const xpair succ = vertex.at (top.itr);
|
|
const trie_node &next = vertices[succ.val];
|
|
top.itr++;
|
|
|
|
frame f {};
|
|
f.ix = succ.val;
|
|
f.itr = 0;
|
|
f.end = next.length ();
|
|
path.safe_push (succ.key);
|
|
stack.safe_push (f);
|
|
}
|
|
else
|
|
{
|
|
stack.pop ();
|
|
if (stack.is_empty ())
|
|
return false;
|
|
path.pop ();
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Return the index of NEEDLE in HAYSTACK, or (size_t)-1 if not found. */
|
|
template <typename T>
|
|
size_t
|
|
index_of (T needle, const vec <T> &haystack)
|
|
{
|
|
size_t len = haystack.length ();
|
|
for (size_t i = 0; i != len; ++i)
|
|
if (haystack[i] == needle)
|
|
return i;
|
|
return (size_t)-1;
|
|
}
|
|
|
|
/* Check if there is an edge in GRAPH from SRC to DST. */
|
|
bool
|
|
edge_p (const struct graph *graph, int src, int dest)
|
|
{
|
|
for (struct graph_edge *e = graph->vertices[src].succ; e; e = e->succ_next)
|
|
if (e->dest == dest)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/* Check if PATH is a cycle starting (and ending) with V. */
|
|
bool
|
|
cycle_p (const vec<int>& path, int v)
|
|
{
|
|
return path[0] == v && path[path.length ()-1] == v;
|
|
}
|
|
|
|
/* Find the SCC entry-exit paths, the simple paths from ENTRY to EXIT, and add
|
|
them to OUT. PRIME_PATHS is the prime paths of the SCC. Paths are hard
|
|
inserted into OUT, which disables subpath eliminiation and essentially makes
|
|
OUT a compact set. This is important to not eliminate paths from ENTRY to
|
|
EXIT which are traversed by other ENTRY/EXIT pairs. Duplicated entries are
|
|
removed. */
|
|
void
|
|
scc_entry_exit_paths (const vec<vec<int>> &internal_pp, int entry, int exit,
|
|
trie &out)
|
|
{
|
|
if (entry == exit)
|
|
{
|
|
out.hard_insert (array_slice <const int> (&entry, 1));
|
|
return;
|
|
}
|
|
|
|
for (const vec<int> &path : internal_pp)
|
|
{
|
|
const size_t Ven = index_of (entry, path);
|
|
const size_t Vex = index_of (exit, path);
|
|
|
|
if (Ven == (size_t)-1 || Vex == (size_t)-1 || Vex <= Ven)
|
|
continue;
|
|
|
|
const size_t len = (Vex + 1) - Ven;
|
|
array_slice <const int> p (path.begin () + Ven, len);
|
|
out.hard_insert (p);
|
|
}
|
|
}
|
|
|
|
/* Find the SCC exit paths, the simple paths that starts in a non-entry vertex
|
|
in the SCC and ends in EXIT and are not a cycles. INTERNAL_PP are the
|
|
internal prime paths for the SCC with EXIT as an exit vertex.
|
|
|
|
Fazli claims the path must not be a subpath of another exit path in the SCC,
|
|
but this is only half true: see gcov-29.c/pathcov005a. Subpaths must survive
|
|
if they end in a different exit vertex than the superpath, so the hard_insert
|
|
is important. */
|
|
void
|
|
scc_exit_paths (const vec<vec<int>> &prime_paths, int exit, trie &out)
|
|
{
|
|
trie trie;
|
|
for (const vec<int> &path : prime_paths)
|
|
{
|
|
const size_t Vex = index_of (exit, path);
|
|
if (Vex == (size_t)-1 || cycle_p (path, exit))
|
|
continue;
|
|
array_slice <const int> p (path.begin (), Vex + 1);
|
|
trie.insert_with_suffix (p);
|
|
}
|
|
|
|
auto_vec<int> path {};
|
|
auto iter = trie.paths (path);
|
|
while (iter.next ())
|
|
out.hard_insert (path);
|
|
}
|
|
|
|
/* Find the SCC entry paths, the simple paths that start in the entry vertex
|
|
ENTRY and are not cycles. INTERNAL_PP are the internal prime paths for the
|
|
SCC with ENTRY as an entry vertex. The paths are inserted into OUT. */
|
|
void
|
|
scc_entry_paths (const vec<vec<int>> &internal_pp, int entry, trie &trie)
|
|
{
|
|
for (const vec<int> &path : internal_pp)
|
|
{
|
|
const size_t Ven = index_of (entry, path);
|
|
if (Ven == (size_t)-1 || cycle_p (path, entry))
|
|
continue;
|
|
array_slice <const int> p (path.begin () + Ven, path.length () - Ven);
|
|
trie.insert (p);
|
|
}
|
|
}
|
|
|
|
/* Worker for cfg_complete_prime_paths. ITR is the current id for the current
|
|
path. END is end of the path so that when ITR == END, the walk is completed.
|
|
EDGES is the matrix of edges where EDGES[src][dst] is set if there is an edge
|
|
from src to dest. PATH is the vertices that make up this walk so far. TRIE
|
|
is the output trie where paths are inserted. SCC_ENEX_PATHS are the
|
|
entry-exit paths found by the scc_entry_exit_paths function. */
|
|
void
|
|
cfg_complete_prime_paths1 (const int *itr, const int *end,
|
|
const sbitmap *edges,
|
|
const vec<vec<vec<int>>> &scc_enex_paths,
|
|
vec<int> &path, trie &trie)
|
|
{
|
|
if (itr == end)
|
|
{
|
|
trie.insert_with_suffix (path);
|
|
return;
|
|
}
|
|
|
|
const unsigned pathlen = path.length ();
|
|
const sbitmap succs = edges[path.last ()];
|
|
for (const vec<int> &enex : scc_enex_paths[*itr])
|
|
{
|
|
if (!bitmap_bit_p (succs, enex[0]))
|
|
continue;
|
|
|
|
path.safe_splice (enex);
|
|
cfg_complete_prime_paths1 (itr + 1, end, edges, scc_enex_paths,
|
|
path, trie);
|
|
path.truncate (pathlen);
|
|
if (limit_exceed_p (trie.size ()))
|
|
return;
|
|
}
|
|
}
|
|
|
|
/* Find the complete prime paths of the CFG, the prime paths that start in the
|
|
entry vertex and end in the exit vertex. */
|
|
trie
|
|
cfg_complete_prime_paths (const sbitmap *edges,
|
|
const vec<trie> &scc_entry_exit_paths,
|
|
const trie &ccfg_prime_paths)
|
|
{
|
|
trie trie;
|
|
auto_vec<int, 16> path {};
|
|
auto_vec<int, 16> cfgpp {};
|
|
auto_vec_vec_vec scc_enex {};
|
|
scc_enex.reserve (scc_entry_exit_paths.length ());
|
|
|
|
for (size_t i = 0; i != scc_entry_exit_paths.length (); ++i)
|
|
{
|
|
scc_enex.quick_push (vec<vec<int>> {});
|
|
auto iter = scc_entry_exit_paths[i].paths (path);
|
|
while (iter.next ())
|
|
scc_enex[i].safe_push (path.copy ());
|
|
}
|
|
|
|
auto iter = ccfg_prime_paths.paths (cfgpp);
|
|
while (!limit_exceed_p (trie.size ()) && iter.next ())
|
|
for (const vec<int> &enex : scc_enex[cfgpp[0]])
|
|
{
|
|
path.truncate (0);
|
|
path.safe_splice (enex);
|
|
cfg_complete_prime_paths1 (cfgpp.begin () + 1, cfgpp.end (), edges,
|
|
scc_enex, path, trie);
|
|
if (limit_exceed_p (trie.size ()))
|
|
return trie;
|
|
}
|
|
|
|
return trie;
|
|
}
|
|
|
|
/* Find the SCC exit prime paths, the prime paths that start from a strongly
|
|
connected component and end in the end vertex. SCCS is a vector where
|
|
SCCS[i] = SCC (vertex_i) so that if vertex[2].component == 5 then SCCS[2] ==
|
|
5. SCC_EXIT_PATHS is the output of scc_exit_paths (). COMPLETE_PRIME_PATHS
|
|
is the output of cfg_complete_prime_paths ().
|
|
|
|
This function can suffer under path explosion and will terminate early if
|
|
the number of inserts in COMPLETE_PRIME_PATHS exceeds approx_limit. */
|
|
trie
|
|
scc_exit_prime_paths (const struct graph *cfg, const trie &scc_exit_paths,
|
|
const trie &complete_prime_paths)
|
|
{
|
|
trie trie;
|
|
auto_vec<int, 8> path {};
|
|
auto_vec<int, 8> r {};
|
|
auto_vec<int, 8> q {};
|
|
|
|
auto exiter = scc_exit_paths.paths (q);
|
|
while (exiter.next ())
|
|
{
|
|
const int Vex = q.last ();
|
|
auto iter = complete_prime_paths.paths (r, Vex);
|
|
while (iter.next (true))
|
|
{
|
|
/* There could be multiple Vex in the SCC. Even if scc_exit_paths
|
|
did not kill the subpaths, this trie probably would. It can be
|
|
assumed that all vertices in q are in the same SCC.
|
|
|
|
This is an important note, as the Fazli and Afsharchi paper does
|
|
not properly capture this subtlety. */
|
|
const int p0 = Vex;
|
|
const int p1 = r[0];
|
|
|
|
if (cfg->vertices[p0].component == cfg->vertices[p1].component)
|
|
continue;
|
|
|
|
path.truncate (0);
|
|
path.reserve (q.length () + r.length ());
|
|
path.splice (q);
|
|
path.splice (r);
|
|
/* This can probably insert without subpath elimination because:
|
|
1. Conflicts are *really* rare (see patmatch in tree.c), but they
|
|
do happen.
|
|
2. The output of this function is "filtered" through another trie
|
|
anyway so the redundant paths generated here will be eliminated
|
|
in the consumers at a very low extra cost. */
|
|
trie.insert (path);
|
|
if (limit_exceed_p (trie.size ()))
|
|
return trie;
|
|
}
|
|
}
|
|
|
|
return trie;
|
|
}
|
|
|
|
/* Check if PATH in CFG enters the VERTEX's SCC through VERTEX. */
|
|
bool
|
|
enters_through_p (const struct graph *cfg, const vec<int> &path, int vertex)
|
|
{
|
|
gcc_checking_assert (!path.is_empty ());
|
|
const int last = path.address()[path.length ()-1];
|
|
if (cfg->vertices[last].component == cfg->vertices[vertex].component)
|
|
return false;
|
|
return edge_p (cfg, last, vertex);
|
|
}
|
|
|
|
/* Worker for scc_entry_prime_paths. CFG is the CFG for the function,
|
|
SCC_ENTRY_PATHS the accumulated scc_entry_paths for all the SCCs, PRIME_PATHS
|
|
is either the result of cfg_complete_prime_paths or exit_prime_paths, and OUT
|
|
the output trie.
|
|
|
|
This function can suffer under path explosion and will terminate early if
|
|
the number of inserts in OUT exceeds approx_limit. */
|
|
void
|
|
scc_entry_prime_paths1 (const struct graph *cfg, const trie &scc_entry_paths,
|
|
const trie &prime_paths, trie &out)
|
|
{
|
|
auto_vec<int, 8> p {};
|
|
auto_vec<int, 8> q {};
|
|
auto_vec<int, 8> path {};
|
|
auto itr = scc_entry_paths.paths (q);
|
|
while (itr.next ())
|
|
{
|
|
const int Ven = q[0];
|
|
/* TODO: This might benefit from a reversed trie lookup. */
|
|
auto xitr = prime_paths.paths (p);
|
|
while (xitr.next (Ven))
|
|
{
|
|
if (!enters_through_p (cfg, p, Ven))
|
|
continue;
|
|
|
|
path.truncate (0);
|
|
path.reserve (p.length () + q.length ());
|
|
path.splice (p);
|
|
path.splice (q);
|
|
out.insert_with_suffix (path);
|
|
if (limit_exceed_p (out.size ()))
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Find the entry prime paths - the prime paths that start in the root and end
|
|
in a strongly connected component. CFG is the CFG for this function,
|
|
SCC_ENTRY_PATHS the accumulated scc_entry_paths for all the SCCs,
|
|
COMPLETE_PRIME_PATHS the result of cfg_complete_prime_paths, and
|
|
EXIT_PRIME_PATHS result of exit_prime_paths.
|
|
|
|
This function can suffer under path explosion and will terminate early if
|
|
the return value grows beyond approx_limit. */
|
|
trie
|
|
scc_entry_prime_paths (const struct graph *cfg,
|
|
const trie &scc_entry_paths,
|
|
const trie &complete_prime_paths,
|
|
const trie &exit_prime_paths)
|
|
{
|
|
trie trie;
|
|
scc_entry_prime_paths1 (cfg, scc_entry_paths, complete_prime_paths, trie);
|
|
scc_entry_prime_paths1 (cfg, scc_entry_paths, exit_prime_paths, trie);
|
|
return trie;
|
|
}
|
|
|
|
/* Build a new control flow graph from the strongly connected components, so
|
|
that every node in the CCFG is a strongly connected component in the original
|
|
CFG. NSSC is the number of vertices in the new graph, and the return value
|
|
of graphds_ssc. */
|
|
struct graph*
|
|
build_ccfg (struct graph *cfg, int nscc)
|
|
{
|
|
struct graph *ccfg = new_graph (nscc);
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
{
|
|
struct vertex v = cfg->vertices[i];
|
|
for (struct graph_edge *e = v.succ; e; e = e->succ_next)
|
|
{
|
|
int src = v.component;
|
|
int dest = cfg->vertices[e->dest].component;
|
|
if (src != dest && !edge_p (ccfg, src, dest))
|
|
add_edge (ccfg, src, dest);
|
|
}
|
|
}
|
|
|
|
return ccfg;
|
|
}
|
|
|
|
/* Create a new graph from CFG where the edges between strongly connected
|
|
components removed. */
|
|
struct graph*
|
|
disconnect_sccs (struct graph *cfg)
|
|
{
|
|
struct graph *ccfg = new_graph (cfg->n_vertices);
|
|
const struct vertex *vertices = cfg->vertices;
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
{
|
|
ccfg->vertices[i].data = &cfg->vertices[i];
|
|
for (struct graph_edge *e = vertices[i].succ; e; e = e->succ_next)
|
|
if (vertices[e->src].component == vertices[e->dest].component)
|
|
add_edge (ccfg, e->src, e->dest)->data = e;
|
|
}
|
|
return ccfg;
|
|
}
|
|
|
|
/* Check if vertex I in CFG is the entry vertex of a strongly connected
|
|
component. A vertex is an entry vertex if 1) there are no predecessors
|
|
(i.e. the root vertex is always an entry vertex) or 2) a predecessor belongs
|
|
to a different SCC. */
|
|
bool
|
|
scc_entry_vertex_p (struct graph *cfg, size_t i)
|
|
{
|
|
if (!cfg->vertices[i].pred)
|
|
return true;
|
|
const int scc = cfg->vertices[i].component;
|
|
for (struct graph_edge *e = cfg->vertices[i].pred; e; e = e->pred_next)
|
|
if (cfg->vertices[e->src].component != scc)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/* Check if vertex I in CFG is an exit vertex of a strongly connected component.
|
|
A vertex is an exit vertex if 1) there are no successors (i.e. the sink is
|
|
always an exit vertex) or 2) if a successor belongs to a different SCC. */
|
|
bool
|
|
scc_exit_vertex_p (struct graph *cfg, size_t i)
|
|
{
|
|
if (!cfg->vertices[i].succ)
|
|
return true;
|
|
const int scc = cfg->vertices[i].component;
|
|
for (struct graph_edge *e = cfg->vertices[i].succ; e; e = e->succ_next)
|
|
if (cfg->vertices[e->dest].component != scc)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/* Worker for simple_paths. Find all the simple paths in CFG starting at NODE
|
|
and insert into OUT. This is a DFS where the search stops when entering a
|
|
vertex already in SEEN. PATH is the sequence of ids for the vertices taken
|
|
from the from the root to NODE. When the number of inserts reaches LIMIT
|
|
the function aborts and returns so the caller can report that it is giving
|
|
up because the function is too complex.
|
|
|
|
This function can suffer under path explosion and will terminate early if
|
|
the number of inserts in OUT exceeds approx_limit. */
|
|
void
|
|
simple_paths1 (const struct graph *cfg, int node, sbitmap seen, vec<int> &path,
|
|
trie &out)
|
|
{
|
|
if (limit_exceed_p (out.size ()))
|
|
return;
|
|
|
|
if (!bitmap_set_bit (seen, node))
|
|
{
|
|
if (path[0] == node)
|
|
path.quick_push (node);
|
|
out.insert (path);
|
|
if (path[0] == node)
|
|
path.pop ();
|
|
return;
|
|
}
|
|
path.quick_push (node);
|
|
|
|
struct graph_edge *succs = cfg->vertices[node].succ;
|
|
if (!succs)
|
|
{
|
|
out.insert (path);
|
|
bitmap_clear_bit (seen, node);
|
|
path.pop ();
|
|
return;
|
|
}
|
|
|
|
for (struct graph_edge *e = succs; e; e = e->succ_next)
|
|
simple_paths1 (cfg, e->dest, seen, path, out);
|
|
|
|
bitmap_clear_bit (seen, node);
|
|
path.pop ();
|
|
}
|
|
|
|
/* Find all the simple paths in CFG starting at ROOT and insert into OUT. A
|
|
simple path is a sequence of vertices without any duplicated vertices (i.e.
|
|
no loops). SEEN should be an sbitmap of CFG->n_vertices size. PATH and
|
|
SEEN will be cleared entry and is for buffer reuse between calls. When the
|
|
number of inserts reaches LIMIT the function aborts and returns so the
|
|
caller can report that it is giving up because the function is too complex.
|
|
Note that there might be fewer prime paths than inserts, but if the number
|
|
of inserts alone is larger than LIMIT the function is very complex and would
|
|
take too long to compile in later stages.
|
|
|
|
This function can suffer under path explosion and will terminate early if
|
|
the number of inserts in OUT exceeds approx_limit. Since OUT is often
|
|
shared between calls it is ok to use in a loop, and only check the size of
|
|
OUT after the loop terminates. */
|
|
void
|
|
simple_paths (struct graph *cfg, int root, sbitmap seen, vec<int> &path,
|
|
trie &out)
|
|
{
|
|
bitmap_clear (seen);
|
|
path.reserve (cfg->n_vertices);
|
|
path.truncate (0);
|
|
simple_paths1 (cfg, root, seen, path, out);
|
|
}
|
|
|
|
/* Merge the tries T1, T2, T3, and set of paths VECS into the larges trie.
|
|
Returns a reference to the trie merged into. Merging tries and resolving
|
|
redundant paths is the slowest step (at least in the sense it works on the
|
|
largest input), and merging into a partial result reduces the work
|
|
accordingly. For large problems this is a massive improvement, which in the
|
|
worst cases (where all tries but one are empty or almost empty) speed up
|
|
30-40%. */
|
|
trie&
|
|
merge (trie &t1, trie &t2, trie &t3, vec<vec<vec<int>>> &vecs)
|
|
{
|
|
trie *dst = nullptr;
|
|
const size_t s1 = t1.size ();
|
|
const size_t s2 = t2.size ();
|
|
const size_t s3 = t3.size ();
|
|
|
|
if (s1 >= s2 && s1 >= s3)
|
|
{
|
|
dst = &t1;
|
|
t1.merge (t2);
|
|
t1.merge (t3);
|
|
}
|
|
else if (s2 >= s1 && s2 >= s3)
|
|
{
|
|
dst = &t2;
|
|
t2.merge (t1);
|
|
t2.merge (t3);
|
|
}
|
|
else
|
|
{
|
|
dst = &t3;
|
|
t3.merge (t1);
|
|
t3.merge (t2);
|
|
}
|
|
|
|
gcc_checking_assert (dst);
|
|
for (const vec<vec<int>> &v2 : vecs)
|
|
for (const vec<int> &v1 : v2)
|
|
dst->insert_with_suffix (v1);
|
|
return *dst;
|
|
}
|
|
|
|
/* Store the edges of CFG in a matrix of bitmaps so that bit_p (edges[src],
|
|
dest) is true if there is an edge from src to dest. This is faster and more
|
|
convenient than walking the linked list of successors in hot loops. The
|
|
vector will have N bitmaps of N bits where N is the number of vertices in
|
|
CFG. */
|
|
sbitmap*
|
|
edge_matrix (const struct graph *cfg)
|
|
{
|
|
sbitmap *edges = sbitmap_vector_alloc (cfg->n_vertices, cfg->n_vertices);
|
|
bitmap_vector_clear (edges, cfg->n_vertices);
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
for (graph_edge *e = cfg->vertices[i].succ; e; e = e->succ_next)
|
|
bitmap_set_bit (edges[e->src], e->dest);
|
|
return edges;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
/* Find the prime paths for CFG. The search gives up after approximate
|
|
PATHLIMIT probable paths have been generated to address path explosions.
|
|
The PATHLIMIT flag is typically controlled by -fpath-coverage-limit. This
|
|
function is a part of -fpath-coverage and will also be called from gcov.
|
|
The paths are returned in lexicographical order based on node (basic block)
|
|
ID. If the path limit was exceeded, an empty vector is returned.
|
|
|
|
A simple path is a path where all vertices are unique, except possibly the
|
|
first and last. A prime path is a maximal-length simple path which is not a
|
|
part of any other simple path. Prime paths strike a good balance between
|
|
coverage thoroughness, loops (requiring them to be taken and skipped), and
|
|
number of paths.
|
|
|
|
The algorithm is based on Fazli & Afsharchi's "A Time and Space-Efficient
|
|
Compositional Method for Prime and Test Paths Generation" (2019), combined
|
|
with a suffix trie for removing duplicate or redundant paths. An auxillary
|
|
graph of the strongly connected components (SCCs) is built. Then, the prime
|
|
paths of the SCCs composes the prime paths of each SCC with the prime paths
|
|
of this auxillary graph. This can drastically cut the number of redundant
|
|
paths generated compared to a naive algorithm.
|
|
|
|
This does not work for all graphs. Some structures, e.g. when most of the
|
|
graph is inside a single SCC, cause the algorithm to degenerate to a naive
|
|
one. The same happens for functions with many SCCs that are either
|
|
singletons or very small. Those cases will be slower with respect to the
|
|
number of paths, but still fast enough if the path limit is kept reasonably
|
|
low (a few hundred thousand). */
|
|
vec<vec<int>>
|
|
prime_paths (struct graph *cfg, size_t pathlimit)
|
|
{
|
|
const int nscc = graphds_scc (cfg, NULL);
|
|
auto_graph disconnected (disconnect_sccs (cfg));
|
|
auto_graph ccfg (build_ccfg (cfg, nscc));
|
|
auto_sbitmap_vector edges (edge_matrix (cfg));
|
|
|
|
auto_sbitmap seen (cfg->n_vertices);
|
|
auto_vec<int, 8> pathbuf {};
|
|
|
|
limit_reset (pathlimit);
|
|
|
|
/* Store an SCC-ID -> vertices mapping to quickly find the vertices that
|
|
make up a strongly connected component. */
|
|
auto_vec_vec sccs {};
|
|
sccs.safe_grow_cleared (ccfg->n_vertices);
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
sccs[cfg->vertices[i].component].safe_push (i);
|
|
|
|
auto_vec_vec_vec scc_internal_pp {};
|
|
scc_internal_pp.safe_grow_cleared (nscc);
|
|
for (int i = 0; i != nscc; ++i)
|
|
{
|
|
trie internal_pp;
|
|
for (int V : sccs[i])
|
|
simple_paths (disconnected, V, seen, pathbuf, internal_pp);
|
|
if (limit_exceed_p (internal_pp.size ()))
|
|
return {};
|
|
scc_internal_pp[i] = internal_pp.paths ();
|
|
if (limit_checked_add (scc_internal_pp[i].length ()))
|
|
return {};
|
|
}
|
|
|
|
auto_vec<trie, 8> scc_enex_paths (nscc);
|
|
scc_enex_paths.safe_grow_cleared (nscc);
|
|
trie scc_en_paths;
|
|
trie scc_ex_paths;
|
|
|
|
for (int i = 0; i != ccfg->n_vertices; ++i)
|
|
{
|
|
for (int Ven : sccs[i])
|
|
{
|
|
if (!scc_entry_vertex_p (cfg, Ven))
|
|
continue;
|
|
|
|
for (int Vex : sccs[i])
|
|
{
|
|
if (!scc_exit_vertex_p (cfg, Vex))
|
|
continue;
|
|
scc_entry_exit_paths (scc_internal_pp[i], Ven, Vex,
|
|
scc_enex_paths[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
{
|
|
const int scc = cfg->vertices[i].component;
|
|
if (scc_entry_vertex_p (cfg, i))
|
|
scc_entry_paths (scc_internal_pp[scc], i, scc_en_paths);
|
|
|
|
if (scc_exit_vertex_p (cfg, i))
|
|
scc_exit_paths (scc_internal_pp[scc], i, scc_ex_paths);
|
|
}
|
|
|
|
/* In the presence of abnormal edges (like longjmp) it is possible to have
|
|
multiple "entry points" in function -- build ccfg prime paths starting at
|
|
any vertex without predecessor. For most graphs this will only be the
|
|
ENTRY_BLOCK. */
|
|
trie ccfg_prime_paths;
|
|
for (int i = 0; i != ccfg->n_vertices; ++i)
|
|
if (!ccfg->vertices[i].pred)
|
|
simple_paths (ccfg, i, seen, pathbuf, ccfg_prime_paths);
|
|
if (limit_exceed_p (ccfg_prime_paths.size ()))
|
|
return {};
|
|
|
|
trie complete_prime_paths = cfg_complete_prime_paths (edges, scc_enex_paths,
|
|
ccfg_prime_paths);
|
|
if (limit_checked_add (complete_prime_paths.size ()))
|
|
return {};
|
|
trie exit_prime_paths = scc_exit_prime_paths (cfg, scc_ex_paths,
|
|
complete_prime_paths);
|
|
if (limit_checked_add (exit_prime_paths.size ()))
|
|
return {};
|
|
trie entry_prime_paths = scc_entry_prime_paths (cfg, scc_en_paths,
|
|
complete_prime_paths,
|
|
exit_prime_paths);
|
|
if (limit_checked_add (entry_prime_paths.size ()))
|
|
return {};
|
|
|
|
trie &merged = merge (complete_prime_paths, entry_prime_paths,
|
|
exit_prime_paths, scc_internal_pp);
|
|
if (merged.size () > pathlimit)
|
|
return {};
|
|
|
|
return merged.paths ();
|
|
}
|
|
|
|
#if CHECKING_P
|
|
|
|
namespace selftest
|
|
{
|
|
|
|
/* Check if the trie contains PATH. */
|
|
static bool
|
|
contains (const trie &trie, array_slice<const int> path)
|
|
{
|
|
size_t index = 0;
|
|
for (int id : path)
|
|
{
|
|
const trie_node ¤t = trie.vertices[index];
|
|
if (!current.inserted)
|
|
return false;
|
|
const auto *xp = current.get (id);
|
|
if (!xp)
|
|
return false;
|
|
index = xp->val;
|
|
}
|
|
return trie.vertices[index].inserted && trie.vertices[index].endofpath;
|
|
}
|
|
|
|
static bool
|
|
equal_p (array_slice<const int> lhs, array_slice<const int> rhs)
|
|
{
|
|
if (lhs.size () != rhs.size ())
|
|
return false;
|
|
|
|
size_t length = lhs.size ();
|
|
for (size_t i = 0; i != length; ++i)
|
|
if (lhs[i] != rhs[i])
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static bool
|
|
any_equal_p (const array_slice<const int> &needle,
|
|
const vec<vec<int>> &haystack)
|
|
{
|
|
for (const vec<int> &x : haystack)
|
|
if (equal_p (needle, array_slice <const int> (x)))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
static size_t
|
|
count (const trie &trie)
|
|
{
|
|
size_t n = 0;
|
|
auto_vec<int> path {};
|
|
auto iter = trie.paths (path);
|
|
while (iter.next ())
|
|
n += 1;
|
|
return n;
|
|
}
|
|
|
|
static vec<vec<int>>
|
|
simple_paths (struct graph *cfg, trie &trie, int root = 0)
|
|
{
|
|
auto_sbitmap seen (cfg->n_vertices);
|
|
auto_vec<int> path;
|
|
simple_paths (cfg, root, seen, path, trie);
|
|
return trie.paths ();
|
|
}
|
|
|
|
/* Create a CFG that roughly corresponds to this program:
|
|
|
|
int binary_search(int a[], int len, int from, int to, int key)
|
|
{
|
|
int low = from;
|
|
int high = to - 1;
|
|
|
|
while (low <= high)
|
|
{
|
|
int mid = (low + high) >> 1;
|
|
long midVal = a[mid];
|
|
|
|
if (midVal < key)
|
|
low = mid + 1;
|
|
else if (midVal > key)
|
|
high = mid - 1;
|
|
else
|
|
return mid; // key found
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
This program would emit a CFG very similar to the CFG used by Fazli &
|
|
Afsharchi (2019). The selftest cases are built from the partial paths used
|
|
in that paper. */
|
|
static struct graph*
|
|
binary_search_cfg ()
|
|
{
|
|
struct graph *g = new_graph (11);
|
|
add_edge (g, 0, 1);
|
|
add_edge (g, 1, 2);
|
|
add_edge (g, 2, 3);
|
|
add_edge (g, 2, 4);
|
|
add_edge (g, 3, 10);
|
|
add_edge (g, 4, 5);
|
|
add_edge (g, 4, 6);
|
|
add_edge (g, 5, 7);
|
|
add_edge (g, 6, 8);
|
|
add_edge (g, 6, 9);
|
|
add_edge (g, 7, 2);
|
|
add_edge (g, 8, 10);
|
|
add_edge (g, 9, 7);
|
|
graphds_scc (g, NULL);
|
|
return g;
|
|
}
|
|
|
|
/* Test a full run of the algorithm against a known graph (binary-search). */
|
|
static void
|
|
test_prime_paths ()
|
|
{
|
|
auto_graph g (binary_search_cfg ());
|
|
vec<vec<int>> paths = prime_paths (g, 100);
|
|
const int p01[] = { 0, 1, 2, 3, 10 };
|
|
const int p02[] = { 0, 1, 2, 4, 6, 8, 10 };
|
|
const int p03[] = { 5, 7, 2, 4, 6, 9 };
|
|
const int p04[] = { 4, 6, 9, 7, 2, 4 };
|
|
const int p05[] = { 2, 4, 6, 9, 7, 2 };
|
|
const int p06[] = { 6, 9, 7, 2, 4, 6 };
|
|
const int p07[] = { 9, 7, 2, 4, 6, 9 };
|
|
const int p08[] = { 7, 2, 4, 6, 9, 7 };
|
|
const int p09[] = { 6, 9, 7, 2, 4, 5 };
|
|
const int p10[] = { 4, 5, 7, 2, 4 };
|
|
const int p11[] = { 2, 4, 5, 7, 2 };
|
|
const int p12[] = { 5, 7, 2, 4, 5 };
|
|
const int p13[] = { 7, 2, 4, 5, 7 };
|
|
const int p14[] = { 4, 6, 9, 7, 2, 3, 10 };
|
|
const int p15[] = { 5, 7, 2, 4, 6, 8, 10 };
|
|
const int p16[] = { 9, 7, 2, 4, 6, 8, 10 };
|
|
const int p17[] = { 4, 5, 7, 2, 3, 10 };
|
|
const int p18[] = { 0, 1, 2, 4, 6, 9, 7 };
|
|
const int p19[] = { 0, 1, 2, 4, 5, 7 };
|
|
|
|
ASSERT_EQ (paths.length (), 19);
|
|
ASSERT_TRUE (any_equal_p (p01, paths));
|
|
ASSERT_TRUE (any_equal_p (p02, paths));
|
|
ASSERT_TRUE (any_equal_p (p03, paths));
|
|
ASSERT_TRUE (any_equal_p (p04, paths));
|
|
ASSERT_TRUE (any_equal_p (p05, paths));
|
|
ASSERT_TRUE (any_equal_p (p06, paths));
|
|
ASSERT_TRUE (any_equal_p (p07, paths));
|
|
ASSERT_TRUE (any_equal_p (p08, paths));
|
|
ASSERT_TRUE (any_equal_p (p09, paths));
|
|
ASSERT_TRUE (any_equal_p (p10, paths));
|
|
ASSERT_TRUE (any_equal_p (p11, paths));
|
|
ASSERT_TRUE (any_equal_p (p12, paths));
|
|
ASSERT_TRUE (any_equal_p (p13, paths));
|
|
ASSERT_TRUE (any_equal_p (p14, paths));
|
|
ASSERT_TRUE (any_equal_p (p15, paths));
|
|
ASSERT_TRUE (any_equal_p (p16, paths));
|
|
ASSERT_TRUE (any_equal_p (p17, paths));
|
|
ASSERT_TRUE (any_equal_p (p18, paths));
|
|
ASSERT_TRUE (any_equal_p (p19, paths));
|
|
release_vec_vec (paths);
|
|
}
|
|
|
|
/* The strongly connected component graph for binary_search looks like
|
|
this, using the vertex numbers from the original graph:
|
|
|
|
START
|
|
|
|
|
1
|
|
|
|
|
2 (SCC)
|
|
/ \
|
|
3 8
|
|
\ /
|
|
END
|
|
|
|
The components gets renumbered by graphds_scc, so the ccfg looks like
|
|
this. The actual numbers don't matter as long as the structure of the
|
|
graph is preserved, and this test is now sensitive to the numbering set
|
|
by graphds_scc. It does not have to be - if that function should reverse
|
|
the numbering this test must be updated.
|
|
|
|
5
|
|
|
|
|
4
|
|
|
|
|
3 (SCC)
|
|
/ \
|
|
2 1
|
|
\ /
|
|
0
|
|
*/
|
|
static void
|
|
test_build_ccfg ()
|
|
{
|
|
auto_graph cfg (binary_search_cfg ());
|
|
const int nscc = graphds_scc (cfg, NULL);
|
|
auto_graph ccfg (build_ccfg (cfg, nscc));
|
|
ASSERT_EQ (6, nscc);
|
|
|
|
ASSERT_TRUE (edge_p (ccfg, 5, 4));
|
|
ASSERT_TRUE (edge_p (ccfg, 4, 3));
|
|
ASSERT_TRUE (edge_p (ccfg, 3, 2));
|
|
ASSERT_TRUE (edge_p (ccfg, 3, 1));
|
|
ASSERT_TRUE (edge_p (ccfg, 2, 0));
|
|
ASSERT_TRUE (edge_p (ccfg, 1, 0));
|
|
}
|
|
|
|
/* This test checks some basic assumptions on finding the strongly connected
|
|
components and disconnecting the graph by removing all edges between SCCs.
|
|
Creating a single auxillary graph simplifies the bookkeeping. */
|
|
static void
|
|
test_split_components ()
|
|
{
|
|
auto_graph cfg (binary_search_cfg ());
|
|
int nscc = graphds_scc (cfg, NULL);
|
|
auto_graph ccfg (disconnect_sccs (cfg));
|
|
|
|
auto_vec_vec entries {};
|
|
auto_vec_vec exits {};
|
|
entries.safe_grow_cleared (nscc);
|
|
exits.safe_grow_cleared (nscc);
|
|
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
{
|
|
if (scc_entry_vertex_p (cfg, i))
|
|
entries[cfg->vertices[i].component].safe_push (i);
|
|
if (scc_exit_vertex_p (cfg, i))
|
|
exits[cfg->vertices[i].component].safe_push (i);
|
|
}
|
|
|
|
const int p10[] = { 10 };
|
|
const int p08[] = { 8 };
|
|
const int p03[] = { 3 };
|
|
const int p02[] = { 2 };
|
|
const int p01[] = { 1 };
|
|
const int p00[] = { 0 };
|
|
const int p26[] = { 2, 6 };
|
|
|
|
ASSERT_EQ (entries.length (), 6);
|
|
ASSERT_TRUE (any_equal_p (p10, entries));
|
|
ASSERT_TRUE (any_equal_p (p08, entries));
|
|
ASSERT_TRUE (any_equal_p (p03, entries));
|
|
ASSERT_TRUE (any_equal_p (p02, entries));
|
|
ASSERT_TRUE (any_equal_p (p01, entries));
|
|
ASSERT_TRUE (any_equal_p (p00, entries));
|
|
|
|
ASSERT_EQ (exits.length (), 6);
|
|
ASSERT_TRUE (any_equal_p (p10, exits));
|
|
ASSERT_TRUE (any_equal_p (p08, exits));
|
|
ASSERT_TRUE (any_equal_p (p03, exits));
|
|
ASSERT_TRUE (any_equal_p (p26, exits));
|
|
ASSERT_TRUE (any_equal_p (p01, exits));
|
|
ASSERT_TRUE (any_equal_p (p00, exits));
|
|
|
|
/* The result of disconnect_sccs () disconnects the graph into its strongly
|
|
connected components. The subgraphs are either singletons (a single
|
|
vertex with no edges) or graphs with cycles. The SCC internal prime
|
|
paths can be found by running a DFS from every SCC vertex, terminating
|
|
on a duplicated vertex. This may create some redundant paths still,
|
|
which must be filtered out.
|
|
|
|
Singletons can either be detected and skipped (requires counting the
|
|
components) or filtered after. For this test case they are skipped
|
|
because other graph inconsistencies are easier to detect. */
|
|
|
|
/* Count and check singleton components. */
|
|
auto_vec<int> scc_size {};
|
|
scc_size.safe_grow_cleared (nscc);
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
scc_size[cfg->vertices[i].component]++;
|
|
ASSERT_EQ (nscc, 6);
|
|
ASSERT_EQ (scc_size[0], 1);
|
|
ASSERT_EQ (scc_size[1], 1);
|
|
ASSERT_EQ (scc_size[2], 1);
|
|
ASSERT_EQ (scc_size[3], 6);
|
|
ASSERT_EQ (scc_size[4], 1);
|
|
ASSERT_EQ (scc_size[5], 1);
|
|
|
|
/* Manually unroll the loop finding the simple paths starting at the
|
|
vertices in the SCCs. In this case there is only the one SCC. */
|
|
trie ccfg_paths;
|
|
auto_vec_vec (simple_paths (ccfg, ccfg_paths, 2));
|
|
auto_vec_vec (simple_paths (ccfg, ccfg_paths, 4));
|
|
auto_vec_vec (simple_paths (ccfg, ccfg_paths, 5));
|
|
auto_vec_vec (simple_paths (ccfg, ccfg_paths, 6));
|
|
auto_vec_vec (simple_paths (ccfg, ccfg_paths, 7));
|
|
auto_vec_vec (simple_paths (ccfg, ccfg_paths, 9));
|
|
/* Then in+out of trie. */
|
|
auto_vec_vec xscc_internal_pp = ccfg_paths.paths ();
|
|
trie scc_internal_pp;
|
|
for (auto &p : xscc_internal_pp)
|
|
scc_internal_pp.insert_with_suffix (p);
|
|
|
|
const int pp01[] = { 5, 7, 2, 4, 6, 9 };
|
|
const int pp02[] = { 4, 5, 7, 2, 4 };
|
|
const int pp03[] = { 4, 6, 9, 7, 2, 4 };
|
|
const int pp04[] = { 2, 4, 5, 7, 2 };
|
|
const int pp05[] = { 2, 4, 6, 9, 7, 2 };
|
|
const int pp06[] = { 5, 7, 2, 4, 5 };
|
|
const int pp07[] = { 6, 9, 7, 2, 4, 6 };
|
|
const int pp08[] = { 7, 2, 4, 5, 7 };
|
|
const int pp09[] = { 9, 7, 2, 4, 6, 9 };
|
|
const int pp10[] = { 7, 2, 4, 6, 9, 7 };
|
|
const int pp11[] = { 6, 9, 7, 2, 4, 5 };
|
|
|
|
ASSERT_EQ (count (scc_internal_pp), 11);
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp01));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp02));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp03));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp04));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp05));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp06));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp07));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp08));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp09));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp10));
|
|
ASSERT_TRUE (contains (scc_internal_pp, pp11));
|
|
}
|
|
|
|
/* The remaining tests deconstructs the algorithm and runs only a single phase
|
|
with good inputs at that point. This makes it easier to pinpoint where
|
|
things go wrong, and helps show in steps how the algorithm works and the
|
|
phases relate.
|
|
|
|
The phases and their inputs and outputs are bazed on Fazli & Afshsarchi. */
|
|
|
|
static void
|
|
test_scc_internal_prime_paths ()
|
|
{
|
|
/* This graph has only the SCC subgraph. The result of running prime-paths
|
|
on it should be the SCC internal prime paths of the full graph. */
|
|
auto_graph scc (new_graph (11));
|
|
add_edge (scc, 0, 2);
|
|
add_edge (scc, 2, 4);
|
|
add_edge (scc, 4, 5);
|
|
add_edge (scc, 4, 6);
|
|
add_edge (scc, 5, 7);
|
|
add_edge (scc, 6, 9);
|
|
add_edge (scc, 9, 7);
|
|
add_edge (scc, 7, 2);
|
|
|
|
auto_vec_vec paths = prime_paths (scc, 100);
|
|
const int p01[] = { 5, 7, 2, 4, 6, 9 };
|
|
const int p02[] = { 4, 6, 9, 7, 2, 4 };
|
|
const int p03[] = { 2, 4, 6, 9, 7, 2 };
|
|
const int p04[] = { 6, 9, 7, 2, 4, 6 };
|
|
const int p05[] = { 9, 7, 2, 4, 6, 9 };
|
|
const int p06[] = { 7, 2, 4, 6, 9, 7 };
|
|
const int p07[] = { 6, 9, 7, 2, 4, 5 };
|
|
const int p08[] = { 4, 5, 7, 2, 4 };
|
|
const int p09[] = { 2, 4, 5, 7, 2 };
|
|
const int p10[] = { 5, 7, 2, 4, 5 };
|
|
const int p11[] = { 7, 2, 4, 5, 7 };
|
|
|
|
ASSERT_TRUE (any_equal_p (p01, paths));
|
|
ASSERT_TRUE (any_equal_p (p02, paths));
|
|
ASSERT_TRUE (any_equal_p (p03, paths));
|
|
ASSERT_TRUE (any_equal_p (p04, paths));
|
|
ASSERT_TRUE (any_equal_p (p05, paths));
|
|
ASSERT_TRUE (any_equal_p (p06, paths));
|
|
ASSERT_TRUE (any_equal_p (p07, paths));
|
|
ASSERT_TRUE (any_equal_p (p08, paths));
|
|
ASSERT_TRUE (any_equal_p (p09, paths));
|
|
ASSERT_TRUE (any_equal_p (p10, paths));
|
|
ASSERT_TRUE (any_equal_p (p11, paths));
|
|
}
|
|
|
|
/* Test the entry/exit path helpers for the strongly connected component in
|
|
binary_search. The SCC has one entry (2, the loop header) and two exits (2,
|
|
6, the loop exit and return). */
|
|
static void
|
|
test_scc_entry_exit_paths ()
|
|
{
|
|
auto_graph scc (new_graph (11));
|
|
add_edge (scc, 2, 4);
|
|
add_edge (scc, 4, 5);
|
|
add_edge (scc, 4, 6);
|
|
add_edge (scc, 5, 7);
|
|
add_edge (scc, 6, 9);
|
|
add_edge (scc, 9, 7);
|
|
add_edge (scc, 7, 2);
|
|
|
|
trie scc_internal_trie;
|
|
auto_vec_vec (simple_paths (scc, scc_internal_trie, 2));
|
|
auto_vec_vec (simple_paths (scc, scc_internal_trie, 4));
|
|
auto_vec_vec (simple_paths (scc, scc_internal_trie, 5));
|
|
auto_vec_vec (simple_paths (scc, scc_internal_trie, 6));
|
|
auto_vec_vec (simple_paths (scc, scc_internal_trie, 7));
|
|
auto_vec_vec (simple_paths (scc, scc_internal_trie, 9));
|
|
auto_vec_vec scc_prime_paths = scc_internal_trie.paths ();
|
|
|
|
trie entry_exits {};
|
|
scc_entry_exit_paths (scc_prime_paths, 2, 2, entry_exits);
|
|
scc_entry_exit_paths (scc_prime_paths, 2, 6, entry_exits);
|
|
|
|
const int p01[] = { 2 };
|
|
const int p02[] = { 2, 4, 6 };
|
|
|
|
ASSERT_EQ (count (entry_exits), 2);
|
|
ASSERT_TRUE (contains (entry_exits, p01));
|
|
ASSERT_TRUE (contains (entry_exits, p02));
|
|
|
|
trie exits;
|
|
scc_exit_paths (scc_prime_paths, 2, exits);
|
|
scc_exit_paths (scc_prime_paths, 6, exits);
|
|
|
|
const int p03[] = { 4, 6, 9, 7, 2 };
|
|
const int p04[] = { 5, 7, 2, 4, 6 };
|
|
const int p05[] = { 9, 7, 2, 4, 6 };
|
|
const int p06[] = { 4, 5, 7, 2 };
|
|
|
|
ASSERT_EQ (count (exits), 4);
|
|
ASSERT_TRUE (contains (exits, p03));
|
|
ASSERT_TRUE (contains (exits, p04));
|
|
ASSERT_TRUE (contains (exits, p05));
|
|
ASSERT_TRUE (contains (exits, p06));
|
|
|
|
trie entries;
|
|
scc_entry_paths (scc_prime_paths, 2, entries);
|
|
|
|
const int p07[] = { 2, 4, 6, 9, 7 };
|
|
const int p08[] = { 2, 4, 5, 7 };
|
|
ASSERT_EQ (count (entries), 2);
|
|
ASSERT_TRUE (contains (entries, p07));
|
|
ASSERT_TRUE (contains (entries, p08));
|
|
}
|
|
|
|
static void
|
|
test_complete_prime_paths ()
|
|
{
|
|
const int ccfgpp0[] = { 0, 1, 2, 3, 5 };
|
|
const int ccfgpp1[] = { 0, 1, 2, 4, 5 };
|
|
trie ccfg_prime_paths {};
|
|
ccfg_prime_paths.insert (ccfgpp0);
|
|
ccfg_prime_paths.insert (ccfgpp1);
|
|
|
|
const int scc0[] = { 2 };
|
|
const int scc1[] = { 2, 4, 6 };
|
|
|
|
const int ccfg_single[] = { 0, 1, 3, 8, 10 };
|
|
|
|
auto_graph cfg (binary_search_cfg ());
|
|
auto_sbitmap_vector edges (sbitmap_vector_alloc (cfg->n_vertices,
|
|
cfg->n_vertices));
|
|
bitmap_vector_clear (edges, cfg->n_vertices);
|
|
for (int i = 0; i != cfg->n_vertices; ++i)
|
|
for (graph_edge *e = cfg->vertices[i].succ; e; e = e->succ_next)
|
|
bitmap_set_bit (edges[e->src], e->dest);
|
|
|
|
auto_vec<trie> ccfg_paths {};
|
|
ccfg_paths.safe_grow_cleared (6);
|
|
ccfg_paths[0].insert (array_slice <const int> (ccfg_single + 0, 1));
|
|
ccfg_paths[1].insert (array_slice <const int> (ccfg_single + 1, 1));
|
|
ccfg_paths[3].insert (array_slice <const int> (ccfg_single + 2, 1));
|
|
ccfg_paths[4].insert (array_slice <const int> (ccfg_single + 3, 1));
|
|
ccfg_paths[5].insert (array_slice <const int> (ccfg_single + 4, 1));
|
|
|
|
/* The paths for the SCC would need to be updated in ccfg pre pass. This
|
|
feels like a clumsy interface and should maybe be disconnected from the
|
|
struct graph. */
|
|
ccfg_paths[2].hard_insert (scc0);
|
|
ccfg_paths[2].hard_insert (scc1);
|
|
|
|
trie cpp = cfg_complete_prime_paths (edges, ccfg_paths, ccfg_prime_paths);
|
|
|
|
const int p01[] = { 0, 1, 2, 3, 10 };
|
|
const int p02[] = { 0, 1, 2, 4, 6, 8, 10 };
|
|
|
|
ASSERT_EQ (count (cpp), 2);
|
|
ASSERT_TRUE (contains (cpp, p01));
|
|
ASSERT_TRUE (contains (cpp, p02));
|
|
}
|
|
|
|
static vec<int>
|
|
binary_search_scc_map ()
|
|
{
|
|
vec<int> sccs {};
|
|
sccs.safe_grow (11);
|
|
sccs[0] = 0;
|
|
sccs[1] = 1;
|
|
sccs[2] = 2;
|
|
sccs[3] = 3;
|
|
sccs[4] = 2;
|
|
sccs[5] = 2;
|
|
sccs[6] = 2;
|
|
sccs[7] = 2;
|
|
sccs[8] = 4;
|
|
sccs[9] = 2;
|
|
sccs[10] = 5;
|
|
return sccs;
|
|
}
|
|
|
|
static void
|
|
test_exit_prime_paths ()
|
|
{
|
|
const int cpp01[] = { 0, 1, 2, 3, 10 };
|
|
const int cpp02[] = { 0, 1, 2, 4, 6, 8, 10 };
|
|
trie complete_prime_paths {};
|
|
complete_prime_paths.insert_with_suffix (cpp01);
|
|
complete_prime_paths.insert_with_suffix (cpp02);
|
|
|
|
const int ep01[] = { 4, 6, 9, 7, 2 };
|
|
const int ep02[] = { 5, 7, 2, 4, 6 };
|
|
const int ep03[] = { 9, 7, 2, 4, 6 };
|
|
const int ep04[] = { 4, 5, 7, 2 };
|
|
trie exit_paths;
|
|
exit_paths.insert (ep01);
|
|
exit_paths.insert (ep02);
|
|
exit_paths.insert (ep03);
|
|
exit_paths.insert (ep04);
|
|
|
|
auto_graph cfg (binary_search_cfg ());
|
|
trie epp = scc_exit_prime_paths (cfg, exit_paths, complete_prime_paths);
|
|
|
|
const int pp01[] = { 4, 6, 9, 7, 2, 3, 10 };
|
|
const int pp02[] = { 5, 7, 2, 4, 6, 8, 10 };
|
|
const int pp03[] = { 9, 7, 2, 4, 6, 8, 10 };
|
|
const int pp04[] = { 4, 5, 7, 2, 3, 10 };
|
|
|
|
ASSERT_EQ (count (epp), 4);
|
|
ASSERT_TRUE (contains (epp, pp01));
|
|
ASSERT_TRUE (contains (epp, pp02));
|
|
ASSERT_TRUE (contains (epp, pp03));
|
|
ASSERT_TRUE (contains (epp, pp04));
|
|
}
|
|
|
|
static void
|
|
test_entry_prime_paths ()
|
|
{
|
|
auto_graph cfg (binary_search_cfg ());
|
|
|
|
static int sccep01[] = { 2, 4, 6, 9, 7 };
|
|
static int sccep02[] = { 2, 4, 5, 7 };
|
|
trie scc_entry_paths;
|
|
scc_entry_paths.insert (sccep01);
|
|
scc_entry_paths.insert (sccep02);
|
|
|
|
const int cpp01[] = { 0, 1, 2, 3, 10 };
|
|
const int cpp02[] = { 0, 1, 2, 4, 6, 8, 10 };
|
|
trie complete_prime_paths {};
|
|
complete_prime_paths.insert (cpp01);
|
|
complete_prime_paths.insert (cpp02);
|
|
|
|
const int ep01[] = { 4, 6, 9, 7, 2, 3, 10 };
|
|
const int ep02[] = { 5, 7, 2, 4, 6, 8, 10 };
|
|
const int ep03[] = { 9, 7, 2, 4, 6, 8, 10 };
|
|
const int ep04[] = { 4, 5, 7, 2, 3, 10 };
|
|
trie exit_prime_paths {};
|
|
exit_prime_paths.insert (ep01);
|
|
exit_prime_paths.insert (ep02);
|
|
exit_prime_paths.insert (ep03);
|
|
exit_prime_paths.insert (ep04);
|
|
|
|
auto_vec<int> sccs = binary_search_scc_map ();
|
|
|
|
trie epp = scc_entry_prime_paths (cfg, scc_entry_paths,
|
|
complete_prime_paths,
|
|
exit_prime_paths);
|
|
|
|
/* The 0 (start node) does not show up in the Fazli & Afsharchi paper and
|
|
kinda, but has no real impact on the result. The prime-paths functions
|
|
do not care about these vertices, but the path-coverage instrumentation
|
|
filters out the ENTRY/EXIT blocks from all the paths. */
|
|
const int pp01[] = { 0, 1, 2, 4, 6, 9, 7 };
|
|
const int pp02[] = { 0, 1, 2, 4, 5, 7 };
|
|
ASSERT_EQ (count (epp), 2);
|
|
ASSERT_TRUE (contains (epp, pp01));
|
|
ASSERT_TRUE (contains (epp, pp02));
|
|
}
|
|
|
|
/* A straight-line graph with one vertex should yield a single path of length 1
|
|
with just the non-exit non-entry node in it. */
|
|
void
|
|
test_singleton_path ()
|
|
{
|
|
auto_graph cfg (new_graph (3));
|
|
add_edge (cfg, 0, 2);
|
|
add_edge (cfg, 2, 1);
|
|
auto_vec_vec paths = prime_paths (cfg, 100);
|
|
|
|
ASSERT_EQ (paths.length (), 1);
|
|
ASSERT_EQ (paths[0].length (), 3);
|
|
ASSERT_EQ (paths[0][0], 0);
|
|
ASSERT_EQ (paths[0][1], 2);
|
|
ASSERT_EQ (paths[0][2], 1);
|
|
}
|
|
|
|
void
|
|
path_coverage_cc_tests ()
|
|
{
|
|
limit_reset (250000);
|
|
test_prime_paths ();
|
|
test_build_ccfg ();
|
|
test_split_components ();
|
|
test_scc_internal_prime_paths ();
|
|
test_scc_entry_exit_paths ();
|
|
test_complete_prime_paths ();
|
|
test_exit_prime_paths ();
|
|
test_entry_prime_paths ();
|
|
test_singleton_path ();
|
|
}
|
|
|
|
} // namespace selftest
|
|
|
|
#endif /* #if CHECKING_P */
|