Skip to content

Commit e2aefc7

Browse files
authored
[NFC] Generalize DFS machinery in makeInhabitable (#7685)
The heap type fuzzer provides a utility for transforming a type definition graph to ensure that all the defined types are inhabitable. This requires finding and breaking cycles of non-nullable references. We use a simple DFS to find the cycles, but this DFS previously assumed that each type would be visited only once at a time because it would always make the first backedge found nullable. A follow-on PR will update this DFS to handle descriptor types, which are like non-nullable references that cannot be made nullable. When the DFS finds a backedge correspnding to a descriptor, it will not be able to make that edge nullable. Instead, it will have to continue searching for the next backedge. To make that follow-on PR simpler, refactor the data structures used in the DFS to make it easier to adapt to visiting a type more than once at a time.
1 parent dcf18a7 commit e2aefc7

File tree

1 file changed

+63
-32
lines changed

1 file changed

+63
-32
lines changed

src/tools/fuzzing/heap-types.cpp

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -811,87 +811,118 @@ void Inhabitator::markExternRefsNullable() {
811811
}
812812
}
813813

814-
// Use a depth-first search to find cycles, marking the last found reference in
815-
// the cycle to be made non-nullable.
814+
// Break cycles of non-nullable references. Doing this optimally (i.e. by
815+
// changing the fewest possible references) is NP-complete[1], so use a simple
816+
// depth-first search rather than anything fancy. When we find a back edge
817+
// forming a cycle, mark the reference forming the edge as nullable.
818+
//
819+
// [1]: https://en.wikipedia.org/wiki/Feedback_arc_set
816820
void Inhabitator::breakNonNullableCycles() {
821+
// The types reachable from each heap type.
822+
// TODO: Include descriptors.
823+
std::unordered_map<HeapType, std::vector<Type>> children;
824+
825+
auto getChildren = [&children](HeapType type) {
826+
auto [it, inserted] = children.insert({type, {}});
827+
if (inserted) {
828+
// TODO: Add descriptors.
829+
it->second = type.getTypeChildren();
830+
}
831+
return it->second;
832+
};
833+
834+
// The sequence of visited types and edge indices comprising the current DFS
835+
// search path.
836+
std::vector<std::pair<HeapType, Index>> path;
837+
838+
// Track how many times each heap type appears on the current path.
839+
std::unordered_map<HeapType, Index> visiting;
840+
817841
// Types we've finished visiting. We don't need to visit them again.
818842
std::unordered_set<HeapType> visited;
819843

820-
// The path of types we are currently visiting. If one of them comes back up,
821-
// we've found a cycle. Map the types to the other types they reference and
822-
// our current index into that list so we can track where we are in each level
823-
// of the search.
824-
InsertOrderedMap<HeapType, std::pair<std::vector<Type>, Index>> visiting;
844+
auto visitType = [&](HeapType type) {
845+
path.push_back({type, 0});
846+
++visiting[type];
847+
};
848+
849+
auto finishType = [&]() {
850+
auto type = path.back().first;
851+
path.pop_back();
852+
auto it = visiting.find(type);
853+
assert(it != visiting.end());
854+
if (--it->second == 0) {
855+
visiting.erase(it);
856+
}
857+
visited.insert(type);
858+
};
825859

826860
for (auto root : types) {
827861
if (visited.count(root)) {
828862
continue;
829863
}
830864
assert(visiting.size() == 0);
831-
visiting.insert({root, {root.getTypeChildren(), 0}});
865+
visitType(root);
832866

833-
while (visiting.size()) {
834-
auto& [curr, state] = *std::prev(visiting.end());
835-
auto& [children, idx] = state;
867+
while (path.size()) {
868+
auto [curr, index] = path.back();
869+
const auto& children = getChildren(curr);
836870

837-
while (idx < children.size()) {
871+
while (index < children.size()) {
838872
// Skip non-reference children because they cannot refer to other types.
839-
if (!children[idx].isRef()) {
840-
++idx;
873+
if (!children[index].isRef()) {
874+
++index;
841875
continue;
842876
}
843877
// Skip nullable references because they don't cause uninhabitable
844878
// cycles.
845-
if (children[idx].isNullable()) {
846-
++idx;
879+
if (children[index].isNullable()) {
880+
++index;
847881
continue;
848882
}
849883
// Skip references that we have already marked nullable to satisfy
850-
// subtyping constraints. TODO: We could take such nullable references
851-
// into account when detecting cycles by tracking where in the current
852-
// search path we have made references nullable.
853-
if (nullables.count({curr, idx})) {
854-
++idx;
884+
// subtyping constraints.
885+
if (nullables.count({curr, index})) {
886+
++index;
855887
continue;
856888
}
857889
// Skip references to types that we have finished visiting. We have
858890
// visited the full graph reachable from such references, so we know
859891
// they cannot cycle back to anything we are currently visiting.
860-
auto heapType = children[idx].getHeapType();
892+
auto heapType = children[index].getHeapType();
861893
if (visited.count(heapType)) {
862-
++idx;
894+
++index;
863895
continue;
864896
}
865897
// Skip references to function types. Functions types can always be
866898
// instantiated since functions can be created even with uninhabitable
867899
// params or results. Function references therefore break cycles that
868900
// would otherwise produce uninhabitability.
869901
if (heapType.isSignature()) {
870-
++idx;
902+
++index;
871903
continue;
872904
}
873905
// If this ref forms a cycle, break the cycle by marking it nullable and
874906
// continue.
875907
if (auto it = visiting.find(heapType); it != visiting.end()) {
876-
markNullable({curr, idx});
877-
++idx;
908+
markNullable({curr, index});
909+
++index;
878910
continue;
879911
}
880912
break;
881913
}
882914

883915
// If we've finished the DFS on the current type, pop it off the search
884916
// path and continue searching the previous type.
885-
if (idx == children.size()) {
886-
visited.insert(curr);
887-
visiting.erase(std::prev(visiting.end()));
917+
if (index == children.size()) {
918+
finishType();
888919
continue;
889920
}
890921

891922
// Otherwise we have a non-nullable reference we need to search.
892-
assert(children[idx].isRef() && children[idx].isNonNullable());
893-
auto next = children[idx++].getHeapType();
894-
visiting.insert({next, {next.getTypeChildren(), 0}});
923+
assert(children[index].isRef() && children[index].isNonNullable());
924+
auto next = children[index++].getHeapType();
925+
visitType(next);
895926
}
896927
}
897928
}

0 commit comments

Comments
 (0)