@@ -811,87 +811,118 @@ void Inhabitator::markExternRefsNullable() {
811
811
}
812
812
}
813
813
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
816
820
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
+
817
841
// Types we've finished visiting. We don't need to visit them again.
818
842
std::unordered_set<HeapType> visited;
819
843
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
+ };
825
859
826
860
for (auto root : types) {
827
861
if (visited.count (root)) {
828
862
continue ;
829
863
}
830
864
assert (visiting.size () == 0 );
831
- visiting. insert ({ root, {root. getTypeChildren (), 0 }} );
865
+ visitType ( root);
832
866
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) ;
836
870
837
- while (idx < children.size ()) {
871
+ while (index < children.size ()) {
838
872
// 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 ;
841
875
continue ;
842
876
}
843
877
// Skip nullable references because they don't cause uninhabitable
844
878
// cycles.
845
- if (children[idx ].isNullable ()) {
846
- ++idx ;
879
+ if (children[index ].isNullable ()) {
880
+ ++index ;
847
881
continue ;
848
882
}
849
883
// 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;
855
887
continue ;
856
888
}
857
889
// Skip references to types that we have finished visiting. We have
858
890
// visited the full graph reachable from such references, so we know
859
891
// they cannot cycle back to anything we are currently visiting.
860
- auto heapType = children[idx ].getHeapType ();
892
+ auto heapType = children[index ].getHeapType ();
861
893
if (visited.count (heapType)) {
862
- ++idx ;
894
+ ++index ;
863
895
continue ;
864
896
}
865
897
// Skip references to function types. Functions types can always be
866
898
// instantiated since functions can be created even with uninhabitable
867
899
// params or results. Function references therefore break cycles that
868
900
// would otherwise produce uninhabitability.
869
901
if (heapType.isSignature ()) {
870
- ++idx ;
902
+ ++index ;
871
903
continue ;
872
904
}
873
905
// If this ref forms a cycle, break the cycle by marking it nullable and
874
906
// continue.
875
907
if (auto it = visiting.find (heapType); it != visiting.end ()) {
876
- markNullable ({curr, idx });
877
- ++idx ;
908
+ markNullable ({curr, index });
909
+ ++index ;
878
910
continue ;
879
911
}
880
912
break ;
881
913
}
882
914
883
915
// If we've finished the DFS on the current type, pop it off the search
884
916
// 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 ();
888
919
continue ;
889
920
}
890
921
891
922
// 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);
895
926
}
896
927
}
897
928
}
0 commit comments