|
16 | 16 |
|
17 | 17 | #include <variant>
|
18 | 18 |
|
| 19 | +#include "ir/subtypes.h" |
| 20 | +#include "support/insert_ordered.h" |
19 | 21 | #include "tools/fuzzing/heap-types.h"
|
20 | 22 | #include "tools/fuzzing/parameters.h"
|
21 | 23 |
|
@@ -654,4 +656,366 @@ HeapTypeGenerator::create(Random& rand, FeatureSet features, size_t n) {
|
654 | 656 | return HeapTypeGeneratorImpl(rand, features, n).result;
|
655 | 657 | }
|
656 | 658 |
|
| 659 | +namespace { |
| 660 | + |
| 661 | +// `makeInhabitable` implementation. |
| 662 | +// |
| 663 | +// There are two root causes of uninhabitability: First, a non-nullable |
| 664 | +// reference to a bottom type is always uninhabitable. Second, a cycle in the |
| 665 | +// type graph formed from non-nullable references makes all the types involved |
| 666 | +// in that cycle uninhabitable because there is no way to construct the types |
| 667 | +// one at at time. All types that reference uninhabitable types via non-nullable |
| 668 | +// references are also themselves uninhabitable, but these transitively |
| 669 | +// uninhabitable types will become inhabitable once we fix the root causes, so |
| 670 | +// we don't worry about them. |
| 671 | +// |
| 672 | +// To modify uninhabitable types to make them habitable, it suffices to make all |
| 673 | +// non-nullable references to bottom types nullable and to break all cycles of |
| 674 | +// non-nullable references by making one of the references nullable. To preserve |
| 675 | +// valid subtyping, the newly nullable fields must also be made nullable in any |
| 676 | +// supertypes in which they appear. |
| 677 | +struct Inhabitator { |
| 678 | + // Uniquely identify fields as an index into a type. |
| 679 | + using FieldPos = std::pair<HeapType, Index>; |
| 680 | + |
| 681 | + // When we make a reference nullable, we typically need to make the same |
| 682 | + // reference in other types nullable to maintain valid subtyping. Which types |
| 683 | + // we need to update depends on the variance of the reference, which is |
| 684 | + // determined by how it is used in its enclosing heap type. |
| 685 | + // |
| 686 | + // An invariant field of a heaptype must have the same type in subtypes of |
| 687 | + // that heaptype. A covariant field of a heaptype must be typed with a subtype |
| 688 | + // of its original type in subtypes of the heaptype. A contravariant field of |
| 689 | + // a heap type must be typed with a supertype of its original type in subtypes |
| 690 | + // of the heaptype. |
| 691 | + enum Variance { Invariant, Covariant, Contravariant }; |
| 692 | + |
| 693 | + // The input types. |
| 694 | + const std::vector<HeapType>& types; |
| 695 | + |
| 696 | + // The fields we will make nullable. |
| 697 | + std::unordered_set<FieldPos> nullables; |
| 698 | + |
| 699 | + SubTypes subtypes; |
| 700 | + |
| 701 | + Inhabitator(const std::vector<HeapType>& types) |
| 702 | + : types(types), subtypes(types) {} |
| 703 | + |
| 704 | + Variance getVariance(FieldPos field); |
| 705 | + void markNullable(FieldPos field); |
| 706 | + void markBottomRefsNullable(); |
| 707 | + void markExternRefsNullable(); |
| 708 | + void breakNonNullableCycles(); |
| 709 | + |
| 710 | + std::vector<HeapType> build(); |
| 711 | +}; |
| 712 | + |
| 713 | +Inhabitator::Variance Inhabitator::getVariance(FieldPos field) { |
| 714 | + auto [type, idx] = field; |
| 715 | + assert(!type.isBasic()); |
| 716 | + if (type.isStruct()) { |
| 717 | + if (type.getStruct().fields[idx].mutable_ == Mutable) { |
| 718 | + return Invariant; |
| 719 | + } else { |
| 720 | + return Covariant; |
| 721 | + } |
| 722 | + } |
| 723 | + if (type.isArray()) { |
| 724 | + if (type.getArray().element.mutable_ == Mutable) { |
| 725 | + return Invariant; |
| 726 | + } else { |
| 727 | + return Covariant; |
| 728 | + } |
| 729 | + } |
| 730 | + if (type.isSignature()) { |
| 731 | + if (idx < type.getSignature().params.size()) { |
| 732 | + return Contravariant; |
| 733 | + } else { |
| 734 | + return Covariant; |
| 735 | + } |
| 736 | + } |
| 737 | + WASM_UNREACHABLE("unexpected kind"); |
| 738 | +} |
| 739 | + |
| 740 | +void Inhabitator::markNullable(FieldPos field) { |
| 741 | + // Mark the given field nullable in the original type and in other types |
| 742 | + // necessary to keep subtyping valid. |
| 743 | + nullables.insert(field); |
| 744 | + auto [curr, idx] = field; |
| 745 | + switch (getVariance(field)) { |
| 746 | + case Covariant: |
| 747 | + // Mark the field null in all supertypes. If the supertype field is |
| 748 | + // already nullable or does not exist, that's ok and this will have no |
| 749 | + // effect. |
| 750 | + while (auto super = curr.getSuperType()) { |
| 751 | + nullables.insert({*super, idx}); |
| 752 | + curr = *super; |
| 753 | + } |
| 754 | + break; |
| 755 | + case Invariant: |
| 756 | + // Find the top type for which this field exists and mark the field |
| 757 | + // nullable in all of its subtypes. |
| 758 | + if (curr.isArray()) { |
| 759 | + while (auto super = curr.getSuperType()) { |
| 760 | + curr = *super; |
| 761 | + } |
| 762 | + } else { |
| 763 | + assert(curr.isStruct()); |
| 764 | + while (auto super = curr.getSuperType()) { |
| 765 | + if (super->getStruct().fields.size() <= idx) { |
| 766 | + break; |
| 767 | + } |
| 768 | + curr = *super; |
| 769 | + } |
| 770 | + } |
| 771 | + [[fallthrough]]; |
| 772 | + case Contravariant: |
| 773 | + // Mark the field nullable in all subtypes. If the subtype field is |
| 774 | + // already nullable, that's ok and this will have no effect. TODO: Remove |
| 775 | + // this extra `index` variable once we have C++20. It's a workaround for |
| 776 | + // lambdas being unable to capture structured bindings. |
| 777 | + const size_t index = idx; |
| 778 | + subtypes.iterSubTypes(curr, [&](HeapType type, Index) { |
| 779 | + nullables.insert({type, index}); |
| 780 | + }); |
| 781 | + break; |
| 782 | + } |
| 783 | +} |
| 784 | + |
| 785 | +void Inhabitator::markBottomRefsNullable() { |
| 786 | + for (auto type : types) { |
| 787 | + auto children = type.getTypeChildren(); |
| 788 | + for (size_t i = 0; i < children.size(); ++i) { |
| 789 | + auto child = children[i]; |
| 790 | + if (child.isRef() && child.getHeapType().isBottom() && |
| 791 | + child.isNonNullable()) { |
| 792 | + markNullable({type, i}); |
| 793 | + } |
| 794 | + } |
| 795 | + } |
| 796 | +} |
| 797 | + |
| 798 | +void Inhabitator::markExternRefsNullable() { |
| 799 | + // The fuzzer cannot instantiate non-nullable externrefs, so make sure they |
| 800 | + // are all nullable. |
| 801 | + // TODO: Remove this once the fuzzer imports externref globals or gets some |
| 802 | + // other way to instantiate externrefs. |
| 803 | + for (auto type : types) { |
| 804 | + auto children = type.getTypeChildren(); |
| 805 | + for (size_t i = 0; i < children.size(); ++i) { |
| 806 | + auto child = children[i]; |
| 807 | + if (child.isRef() && child.getHeapType() == HeapType::ext && |
| 808 | + child.isNonNullable()) { |
| 809 | + markNullable({type, i}); |
| 810 | + } |
| 811 | + } |
| 812 | + } |
| 813 | +} |
| 814 | + |
| 815 | +// Use a depth-first search to find cycles, marking the last found reference in |
| 816 | +// the cycle to be made non-nullable. |
| 817 | +void Inhabitator::breakNonNullableCycles() { |
| 818 | + // Types we've finished visiting. We don't need to visit them again. |
| 819 | + std::unordered_set<HeapType> visited; |
| 820 | + |
| 821 | + // The path of types we are currently visiting. If one of them comes back up, |
| 822 | + // we've found a cycle. Map the types to the other types they reference and |
| 823 | + // our current index into that list so we can track where we are in each level |
| 824 | + // of the search. |
| 825 | + InsertOrderedMap<HeapType, std::pair<std::vector<Type>, Index>> visiting; |
| 826 | + |
| 827 | + for (auto root : types) { |
| 828 | + if (visited.count(root)) { |
| 829 | + continue; |
| 830 | + } |
| 831 | + assert(visiting.size() == 0); |
| 832 | + visiting.insert({root, {root.getTypeChildren(), 0}}); |
| 833 | + |
| 834 | + while (visiting.size()) { |
| 835 | + auto& [curr, state] = *std::prev(visiting.end()); |
| 836 | + auto& [children, idx] = state; |
| 837 | + |
| 838 | + while (idx < children.size()) { |
| 839 | + // Skip non-reference children because they cannot refer to other types. |
| 840 | + if (!children[idx].isRef()) { |
| 841 | + ++idx; |
| 842 | + continue; |
| 843 | + } |
| 844 | + // Skip nullable references because they don't cause uninhabitable |
| 845 | + // cycles. |
| 846 | + if (children[idx].isNullable()) { |
| 847 | + ++idx; |
| 848 | + continue; |
| 849 | + } |
| 850 | + // Skip references that we have already marked nullable to satisfy |
| 851 | + // subtyping constraints. TODO: We could take such nullable references |
| 852 | + // into account when detecting cycles by tracking where in the current |
| 853 | + // search path we have made references nullable. |
| 854 | + if (nullables.count({curr, idx})) { |
| 855 | + ++idx; |
| 856 | + continue; |
| 857 | + } |
| 858 | + // Skip references to types that we have finished visiting. We have |
| 859 | + // visited the full graph reachable from such references, so we know |
| 860 | + // they cannot cycle back to anything we are currently visiting. |
| 861 | + auto heapType = children[idx].getHeapType(); |
| 862 | + if (visited.count(heapType)) { |
| 863 | + ++idx; |
| 864 | + continue; |
| 865 | + } |
| 866 | + // If this ref forms a cycle, break the cycle by marking it nullable and |
| 867 | + // continue. |
| 868 | + if (auto it = visiting.find(heapType); it != visiting.end()) { |
| 869 | + markNullable({curr, idx}); |
| 870 | + ++idx; |
| 871 | + continue; |
| 872 | + } |
| 873 | + break; |
| 874 | + } |
| 875 | + |
| 876 | + // If we've finished the DFS on the current type, pop it off the search |
| 877 | + // path and continue searching the previous type. |
| 878 | + if (idx == children.size()) { |
| 879 | + visited.insert(curr); |
| 880 | + visiting.erase(std::prev(visiting.end())); |
| 881 | + continue; |
| 882 | + } |
| 883 | + |
| 884 | + // Otherwise we have a non-nullable reference we need to search. |
| 885 | + assert(children[idx].isRef() && children[idx].isNonNullable()); |
| 886 | + auto next = children[idx++].getHeapType(); |
| 887 | + visiting.insert({next, {next.getTypeChildren(), 0}}); |
| 888 | + } |
| 889 | + } |
| 890 | +} |
| 891 | + |
| 892 | +std::vector<HeapType> Inhabitator::build() { |
| 893 | + std::unordered_map<HeapType, size_t> typeIndices; |
| 894 | + for (size_t i = 0; i < types.size(); ++i) { |
| 895 | + typeIndices.insert({types[i], i}); |
| 896 | + } |
| 897 | + |
| 898 | + TypeBuilder builder(types.size()); |
| 899 | + |
| 900 | + // Copy types. Update references to point to the corresponding new type and |
| 901 | + // make them nullable where necessary. |
| 902 | + auto updateType = [&](FieldPos pos, Type& type) { |
| 903 | + if (!type.isRef()) { |
| 904 | + return; |
| 905 | + } |
| 906 | + auto heapType = type.getHeapType(); |
| 907 | + auto nullability = type.getNullability(); |
| 908 | + if (auto it = typeIndices.find(heapType); it != typeIndices.end()) { |
| 909 | + heapType = builder[it->second]; |
| 910 | + } |
| 911 | + if (nullables.count(pos)) { |
| 912 | + nullability = Nullable; |
| 913 | + } |
| 914 | + type = builder.getTempRefType(heapType, nullability); |
| 915 | + }; |
| 916 | + |
| 917 | + for (size_t i = 0; i < types.size(); ++i) { |
| 918 | + auto type = types[i]; |
| 919 | + if (type.isStruct()) { |
| 920 | + Struct copy = type.getStruct(); |
| 921 | + for (size_t j = 0; j < copy.fields.size(); ++j) { |
| 922 | + updateType({type, j}, copy.fields[j].type); |
| 923 | + } |
| 924 | + builder[i] = copy; |
| 925 | + continue; |
| 926 | + } |
| 927 | + if (type.isArray()) { |
| 928 | + Array copy = type.getArray(); |
| 929 | + updateType({type, 0}, copy.element.type); |
| 930 | + builder[i] = copy; |
| 931 | + continue; |
| 932 | + } |
| 933 | + if (type.isSignature()) { |
| 934 | + auto sig = type.getSignature(); |
| 935 | + size_t j = 0; |
| 936 | + std::vector<Type> params; |
| 937 | + for (auto param : sig.params) { |
| 938 | + params.push_back(param); |
| 939 | + updateType({type, j++}, params.back()); |
| 940 | + } |
| 941 | + std::vector<Type> results; |
| 942 | + for (auto result : sig.results) { |
| 943 | + results.push_back(result); |
| 944 | + updateType({type, j++}, results.back()); |
| 945 | + } |
| 946 | + builder[i] = Signature(builder.getTempTupleType(params), |
| 947 | + builder.getTempTupleType(results)); |
| 948 | + continue; |
| 949 | + } |
| 950 | + WASM_UNREACHABLE("unexpected type kind"); |
| 951 | + } |
| 952 | + |
| 953 | + // Establish rec groups. |
| 954 | + for (size_t start = 0; start < types.size();) { |
| 955 | + size_t size = types[start].getRecGroup().size(); |
| 956 | + builder.createRecGroup(start, size); |
| 957 | + start += size; |
| 958 | + } |
| 959 | + |
| 960 | + // Establish supertypes. |
| 961 | + for (size_t i = 0; i < types.size(); ++i) { |
| 962 | + if (auto super = types[i].getSuperType()) { |
| 963 | + if (auto it = typeIndices.find(*super); it != typeIndices.end()) { |
| 964 | + builder[i].subTypeOf(builder[it->second]); |
| 965 | + } else { |
| 966 | + builder[i].subTypeOf(*super); |
| 967 | + } |
| 968 | + } |
| 969 | + } |
| 970 | + |
| 971 | + auto built = builder.build(); |
| 972 | + assert(!built.getError() && "unexpected build error"); |
| 973 | + return *built; |
| 974 | +} |
| 975 | + |
| 976 | +} // anonymous namespace |
| 977 | + |
| 978 | +std::vector<HeapType> |
| 979 | +HeapTypeGenerator::makeInhabitable(const std::vector<HeapType>& types) { |
| 980 | + if (types.empty()) { |
| 981 | + return {}; |
| 982 | + } |
| 983 | + |
| 984 | + // Remove duplicate and basic types. We will insert them back at the end. |
| 985 | + std::unordered_map<HeapType, size_t> typeIndices; |
| 986 | + std::vector<size_t> deduplicatedIndices; |
| 987 | + std::vector<HeapType> deduplicated; |
| 988 | + for (auto type : types) { |
| 989 | + if (type.isBasic()) { |
| 990 | + deduplicatedIndices.push_back(-1); |
| 991 | + continue; |
| 992 | + } |
| 993 | + auto [it, inserted] = typeIndices.insert({type, deduplicated.size()}); |
| 994 | + if (inserted) { |
| 995 | + deduplicated.push_back(type); |
| 996 | + } |
| 997 | + deduplicatedIndices.push_back(it->second); |
| 998 | + } |
| 999 | + assert(deduplicatedIndices.size() == types.size()); |
| 1000 | + |
| 1001 | + // Construct the new types. |
| 1002 | + Inhabitator inhabitator(deduplicated); |
| 1003 | + inhabitator.markBottomRefsNullable(); |
| 1004 | + inhabitator.markExternRefsNullable(); |
| 1005 | + inhabitator.breakNonNullableCycles(); |
| 1006 | + deduplicated = inhabitator.build(); |
| 1007 | + |
| 1008 | + // Re-duplicate and re-insert basic types as necessary. |
| 1009 | + std::vector<HeapType> result; |
| 1010 | + for (size_t i = 0; i < types.size(); ++i) { |
| 1011 | + if (deduplicatedIndices[i] == (size_t)-1) { |
| 1012 | + assert(types[i].isBasic()); |
| 1013 | + result.push_back(types[i]); |
| 1014 | + } else { |
| 1015 | + result.push_back(deduplicated[deduplicatedIndices[i]]); |
| 1016 | + } |
| 1017 | + } |
| 1018 | + return result; |
| 1019 | +} |
| 1020 | + |
657 | 1021 | } // namespace wasm
|
0 commit comments