diff options
-rw-r--r-- | src/wasm-type.h | 8 | ||||
-rw-r--r-- | src/wasm/wasm-type.cpp | 429 | ||||
-rw-r--r-- | test/gtest/type-builder.cpp | 151 |
3 files changed, 558 insertions, 30 deletions
diff --git a/src/wasm-type.h b/src/wasm-type.h index 99194b81d..ea12e52e3 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -393,6 +393,7 @@ class RecGroup { public: explicit RecGroup(uintptr_t id) : id(id) {} + constexpr TypeID getID() const { return id; } bool operator==(const RecGroup& other) const { return id == other.id; } bool operator!=(const RecGroup& other) const { return id != other.id; } size_t size() const; @@ -406,6 +407,7 @@ public: Iterator begin() const { return Iterator{{this, 0}}; } Iterator end() const { return Iterator{{this, size()}}; } + HeapType operator[](size_t i) const { return *Iterator{{this, i}}; } }; typedef std::vector<Type> TypeList; @@ -518,7 +520,7 @@ struct Rtt { static constexpr uint32_t NoDepth = -1; uint32_t depth; HeapType heapType; - Rtt(HeapType heapType) : depth(NoDepth), heapType(heapType) {} + explicit Rtt(HeapType heapType) : depth(NoDepth), heapType(heapType) {} Rtt(uint32_t depth, HeapType heapType) : depth(depth), heapType(heapType) {} bool operator==(const Rtt& other) const { return depth == other.depth && heapType == other.heapType; @@ -706,6 +708,10 @@ template<> class hash<wasm::Rtt> { public: size_t operator()(const wasm::Rtt&) const; }; +template<> class hash<wasm::RecGroup> { +public: + size_t operator()(const wasm::RecGroup&) const; +}; } // namespace std diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index f63505b20..b0b218189 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -284,6 +284,88 @@ struct FiniteShapeEquator { bool eq(const Rtt& a, const Rtt& b); }; +struct RecGroupHasher { + // `group` may or may not be canonical, but any other recursion group it + // reaches must be canonical. + RecGroup group; + + RecGroupHasher(RecGroup group) : group(group) {} + + // Perform the hash. + size_t operator()() const; + + // `topLevelHash` is applied to the top-level group members and observes their + // structure, while `hash(HeapType)` is applied to the children of group + // members and does not observe their structure. + size_t topLevelHash(HeapType type) const; + size_t hash(Type type) const; + size_t hash(HeapType type) const; + size_t hash(const TypeInfo& info) const; + size_t hash(const HeapTypeInfo& info) const; + size_t hash(const Tuple& tuple) const; + size_t hash(const Field& field) const; + size_t hash(const Signature& sig) const; + size_t hash(const Struct& struct_) const; + size_t hash(const Array& array) const; + size_t hash(const Rtt& rtt) const; +}; + +struct RecGroupEquator { + // `newGroup` may or may not be canonical, but `otherGroup` and any other + // recursion group reachable by either of them must be canonical. + RecGroup newGroup, otherGroup; + + RecGroupEquator(RecGroup newGroup, RecGroup otherGroup) + : newGroup(newGroup), otherGroup(otherGroup) {} + + // Perform the comparison. + bool operator()() const; + + // `topLevelEq` is applied to the top-level group members and observes their + // structure, while `eq(HeapType)` is applied to the children of group members + // and does not observe their structure. + bool topLevelEq(HeapType a, HeapType b) const; + bool eq(Type a, Type b) const; + bool eq(HeapType a, HeapType b) const; + bool eq(const TypeInfo& a, const TypeInfo& b) const; + bool eq(const HeapTypeInfo& a, const HeapTypeInfo& b) const; + bool eq(const Tuple& a, const Tuple& b) const; + bool eq(const Field& a, const Field& b) const; + bool eq(const Signature& a, const Signature& b) const; + bool eq(const Struct& a, const Struct& b) const; + bool eq(const Array& a, const Array& b) const; + bool eq(const Rtt& a, const Rtt& b) const; +}; + +// A wrapper around a RecGroup that provides equality and hashing based on the +// structure of the group such that isorecursively equivalent recursion groups +// will compare equal and will have the same hash. Assumes that all recursion +// groups reachable from this one have been canonicalized, except for the +// wrapped group itself. +struct RecGroupStructure { + RecGroup group; + bool operator==(const RecGroupStructure& other) const { + return RecGroupEquator{group, other.group}(); + } +}; + +} // anonymous namespace +} // namespace wasm + +namespace std { + +template<> class hash<wasm::RecGroupStructure> { +public: + size_t operator()(const wasm::RecGroupStructure& structure) const { + return wasm::RecGroupHasher{structure.group}(); + } +}; + +} // namespace std + +namespace wasm { +namespace { + // Generic utility for traversing type graphs. The inserted roots must live as // long as the Walker because they are referenced by address. This base class // only has logic for traversing type graphs; figuring out when to stop @@ -773,8 +855,41 @@ struct SignatureTypeCache { static SignatureTypeCache nominalSignatureCache; // Keep track of the constructed recursion groups. -static std::mutex recGroupsMutex; -static std::vector<std::unique_ptr<RecGroupInfo>> recGroups; +struct RecGroupStore { + std::mutex mutex; + // Store the structures of all rec groups created so far so we can avoid + // creating duplicates. + std::unordered_set<RecGroupStructure> canonicalGroups; + // Keep the `RecGroupInfos` for the nontrivial groups stored in + // `canonicalGroups` alive. + std::vector<std::unique_ptr<RecGroupInfo>> builtGroups; + + RecGroup insert(RecGroup group) { + RecGroupStructure structure{group}; + auto [it, inserted] = canonicalGroups.insert(structure); + if (inserted) { + return group; + } else { + return it->group; + } + } + + RecGroup insert(std::unique_ptr<RecGroupInfo>&& info) { + RecGroup group{uintptr_t(info.get())}; + auto canonical = insert(group); + if (canonical == group) { + builtGroups.emplace_back(std::move(info)); + } + return canonical; + } + + void clear() { + canonicalGroups.clear(); + builtGroups.clear(); + } +}; + +static RecGroupStore globalRecGroupStore; } // anonymous namespace @@ -782,6 +897,7 @@ void destroyAllTypesForTestingPurposesOnly() { globalTypeStore.clear(); globalHeapTypeStore.clear(); nominalSignatureCache.clear(); + globalRecGroupStore.clear(); } Type::Type(std::initializer_list<Type> types) : Type(Tuple(types)) {} @@ -2203,6 +2319,239 @@ bool FiniteShapeEquator::eq(const Rtt& a, const Rtt& b) { return a.depth == b.depth && eq(a.heapType, b.heapType); } +size_t RecGroupHasher::operator()() const { + size_t digest = wasm::hash(group.size()); + for (auto type : group) { + hash_combine(digest, topLevelHash(type)); + } + return digest; +} + +size_t RecGroupHasher::topLevelHash(HeapType type) const { + size_t digest = wasm::hash(type.isBasic()); + if (type.isBasic()) { + wasm::rehash(digest, type.getID()); + } else { + hash_combine(digest, hash(*getHeapTypeInfo(type))); + } + return digest; +} + +size_t RecGroupHasher::hash(Type type) const { + size_t digest = wasm::hash(type.isBasic()); + if (type.isBasic()) { + wasm::rehash(digest, type.getID()); + } else { + hash_combine(digest, hash(*getTypeInfo(type))); + } + return digest; +} + +size_t RecGroupHasher::hash(HeapType type) const { + // Do not recurse into the structure of this child type, but rather hash it as + // an index into a rec group. Only take the rec group identity into account if + // the child is not a member of the top-level group because in that case the + // group may not be canonicalized yet. + size_t digest = wasm::hash(type.getRecGroupIndex()); + auto currGroup = type.getRecGroup(); + if (currGroup != group) { + wasm::rehash(digest, currGroup.getID()); + } + return digest; +} + +size_t RecGroupHasher::hash(const TypeInfo& info) const { + size_t digest = wasm::hash(info.kind); + switch (info.kind) { + case TypeInfo::TupleKind: + hash_combine(digest, hash(info.tuple)); + return digest; + case TypeInfo::RefKind: + rehash(digest, info.ref.nullable); + hash_combine(digest, hash(info.ref.heapType)); + return digest; + case TypeInfo::RttKind: + hash_combine(digest, hash(info.rtt)); + return digest; + } + WASM_UNREACHABLE("unexpected kind"); +} + +size_t RecGroupHasher::hash(const HeapTypeInfo& info) const { + assert(info.isFinalized); + size_t digest = wasm::hash(uintptr_t(info.supertype)); + wasm::rehash(digest, info.kind); + switch (info.kind) { + case HeapTypeInfo::BasicKind: + WASM_UNREACHABLE("Basic HeapTypeInfo should have been canonicalized"); + case HeapTypeInfo::SignatureKind: + hash_combine(digest, hash(info.signature)); + return digest; + case HeapTypeInfo::StructKind: + hash_combine(digest, hash(info.struct_)); + return digest; + case HeapTypeInfo::ArrayKind: + hash_combine(digest, hash(info.array)); + return digest; + } + WASM_UNREACHABLE("unexpected kind"); +} + +size_t RecGroupHasher::hash(const Tuple& tuple) const { + size_t digest = wasm::hash(tuple.types.size()); + for (auto type : tuple.types) { + hash_combine(digest, hash(type)); + } + return digest; +} + +size_t RecGroupHasher::hash(const Field& field) const { + size_t digest = wasm::hash(field.packedType); + rehash(digest, field.mutable_); + hash_combine(digest, hash(field.type)); + return digest; +} + +size_t RecGroupHasher::hash(const Signature& sig) const { + size_t digest = hash(sig.params); + hash_combine(digest, hash(sig.results)); + return digest; +} + +size_t RecGroupHasher::hash(const Struct& struct_) const { + size_t digest = wasm::hash(struct_.fields.size()); + for (const auto& field : struct_.fields) { + hash_combine(digest, hash(field)); + } + return digest; +} + +size_t RecGroupHasher::hash(const Array& array) const { + return hash(array.element); +} + +size_t RecGroupHasher::hash(const Rtt& rtt) const { + size_t digest = wasm::hash(rtt.depth); + hash_combine(digest, hash(rtt.heapType)); + return digest; +} + +bool RecGroupEquator::operator()() const { + if (newGroup == otherGroup) { + return true; + } + // The rec groups are equivalent if they are piecewise equivalent. + return std::equal( + newGroup.begin(), + newGroup.end(), + otherGroup.begin(), + otherGroup.end(), + [&](const HeapType& a, const HeapType& b) { return topLevelEq(a, b); }); +} + +bool RecGroupEquator::topLevelEq(HeapType a, HeapType b) const { + if (a == b) { + return true; + } + if (a.isBasic() || b.isBasic()) { + return false; + } + return eq(*getHeapTypeInfo(a), *getHeapTypeInfo(b)); +} + +bool RecGroupEquator::eq(Type a, Type b) const { + if (a == b) { + return true; + } + if (a.isBasic() || b.isBasic()) { + return false; + } + return eq(*getTypeInfo(a), *getTypeInfo(b)); +} + +bool RecGroupEquator::eq(HeapType a, HeapType b) const { + // Do not recurse into the structure of children `a` and `b`, but check + // whether their recursion groups and indices match. Since `newGroup` may not + // be canonicalized, explicitly check whether `a` and `b` are in the + // respective recursion groups of the respective top-level groups we are + // comparing, in which case the structure is still equivalent. + if (a.getRecGroupIndex() != b.getRecGroupIndex()) { + return false; + } + auto groupA = a.getRecGroup(); + auto groupB = b.getRecGroup(); + return groupA == groupB || (groupA == newGroup && groupB == otherGroup); +} + +bool RecGroupEquator::eq(const TypeInfo& a, const TypeInfo& b) const { + if (a.kind != b.kind) { + return false; + } + switch (a.kind) { + case TypeInfo::TupleKind: + return eq(a.tuple, b.tuple); + case TypeInfo::RefKind: + return a.ref.nullable == b.ref.nullable && + eq(a.ref.heapType, b.ref.heapType); + case TypeInfo::RttKind: + return eq(a.rtt, b.rtt); + } + WASM_UNREACHABLE("unexpected kind"); +} + +bool RecGroupEquator::eq(const HeapTypeInfo& a, const HeapTypeInfo& b) const { + if (a.supertype != b.supertype) { + return false; + } + if (a.kind != b.kind) { + return false; + } + switch (a.kind) { + case HeapTypeInfo::BasicKind: + WASM_UNREACHABLE("Basic HeapTypeInfo should have been canonicalized"); + case HeapTypeInfo::SignatureKind: + return eq(a.signature, b.signature); + case HeapTypeInfo::StructKind: + return eq(a.struct_, b.struct_); + case HeapTypeInfo::ArrayKind: + return eq(a.array, b.array); + } + WASM_UNREACHABLE("unexpected kind"); +} + +bool RecGroupEquator::eq(const Tuple& a, const Tuple& b) const { + return std::equal(a.types.begin(), + a.types.end(), + b.types.begin(), + b.types.end(), + [&](const Type& x, const Type& y) { return eq(x, y); }); +} + +bool RecGroupEquator::eq(const Field& a, const Field& b) const { + return a.packedType == b.packedType && a.mutable_ == b.mutable_ && + eq(a.type, b.type); +} + +bool RecGroupEquator::eq(const Signature& a, const Signature& b) const { + return eq(a.params, b.params) && eq(a.results, b.results); +} + +bool RecGroupEquator::eq(const Struct& a, const Struct& b) const { + return std::equal(a.fields.begin(), + a.fields.end(), + b.fields.begin(), + b.fields.end(), + [&](const Field& x, const Field& y) { return eq(x, y); }); +} + +bool RecGroupEquator::eq(const Array& a, const Array& b) const { + return eq(a.element, b.element); +} + +bool RecGroupEquator::eq(const Rtt& a, const Rtt& b) const { + return a.depth == b.depth && eq(a.heapType, b.heapType); +} + template<typename Self> void TypeGraphWalkerBase<Self>::walkRoot(Type* type) { assert(taskList.empty()); taskList.push_back(Task::scan(type)); @@ -2420,11 +2769,12 @@ void TypeBuilder::createRecGroup(size_t i, size_t length) { if (length < 2) { return; } - recGroups.emplace_back(std::make_unique<RecGroupInfo>()); + auto& groups = impl->recGroups; + groups.emplace_back(std::make_unique<RecGroupInfo>()); for (; length > 0; --length) { auto& info = impl->entries[i + length - 1].info; assert(info->recGroup == nullptr && "group already assigned"); - info->recGroup = recGroups.back().get(); + info->recGroup = groups.back().get(); } } @@ -3190,13 +3540,9 @@ validateStructuralSubtyping(const std::vector<HeapType>& types) { std::optional<TypeBuilder::Error> canonicalizeNominal(CanonicalizationState& state) { - // TODO: clear recursion groups once we are no longer piggybacking the - // isorecursive system on the nominal system. - // if (typeSystem != TypeSystem::Isorecursive) { - // for (auto& info : state.newInfos) { - // assert(info->recGroup == nullptr && "unexpected recursion group"); - // } - // } + for (auto& info : state.newInfos) { + info->recGroup = nullptr; + } // Nominal types do not require separate canonicalization, so just validate // that their subtyping is correct. @@ -3253,12 +3599,19 @@ std::optional<TypeBuilder::Error> canonicalizeIsorecursive( } } + // Map rec groups to the unique pointers to their infos. + std::unordered_map<RecGroup, std::unique_ptr<RecGroupInfo>> groupInfoMap; + for (auto& info : recGroupInfos) { + RecGroup group{uintptr_t(info.get())}; + groupInfoMap[group] = std::move(info); + } + // Check that supertypes precede their subtypes and that other child types // either precede their parents or appear later in the same recursion group. // `indexOfType` both maps types to their indices and keeps track of which // types we have seen so far. std::unordered_map<HeapType, size_t> indexOfType; - std::optional<RecGroup> currGroup; + std::vector<RecGroup> groups; size_t groupStart = 0; // Validate the children of all types in a recursion group after all the types @@ -3288,12 +3641,12 @@ std::optional<TypeBuilder::Error> canonicalizeIsorecursive( } // Check whether we have finished a rec group. auto newGroup = type.getRecGroup(); - if (currGroup && *currGroup != newGroup) { + if (groups.empty() || groups.back() != newGroup) { if (auto error = finishGroup(index)) { return *error; } + groups.push_back(newGroup); } - currGroup = newGroup; // Register this type as seen. indexOfType.insert({type, index}); } @@ -3306,13 +3659,45 @@ std::optional<TypeBuilder::Error> canonicalizeIsorecursive( return {*error}; } - // Move the recursion groups into the global store. TODO: after proper - // isorecursive canonicalization, some groups may no longer be used, so they - // will need to be filtered out. - std::lock_guard<std::mutex> lock(recGroupsMutex); - for (auto& info : recGroupInfos) { - recGroups.emplace_back(std::move(info)); + // Now that we know everything is valid, start canonicalizing recursion + // groups. Before canonicalizing each group, update all the HeapType use sites + // within it to make sure it only refers to other canonical groups or to + // itself (since other groups it refers to may have been duplicates of + // previously canonicalized groups and therefore would not be canonical). To + // canonicalize the group, try to insert it into the global store. If that + // fails, we already have an isorecursively equivalent group, so update the + // replacements accordingly. + CanonicalizationState::ReplacementMap replacements; + { + std::lock_guard<std::mutex> lock(globalRecGroupStore.mutex); + groupStart = 0; + for (auto group : groups) { + size_t size = group.size(); + for (size_t i = 0; i < size; ++i) { + state.updateUses(replacements, state.newInfos[groupStart + i]); + } + groupStart += size; + RecGroup canonical(0); + // There may or may not be a `RecGroupInfo` for this group depending on + // whether it is a singleton group or not. If there is one, it is in the + // `groupInfoMap`. Make the info available for the global store to take + // ownership of it in case this group is made the canonical group for its + // structure. + if (auto it = groupInfoMap.find(group); it != groupInfoMap.end()) { + canonical = globalRecGroupStore.insert(std::move(it->second)); + } else { + canonical = globalRecGroupStore.insert(group); + } + if (group != canonical) { + // Replace the non-canonical types with their canonical equivalents. + assert(canonical.size() == size); + for (size_t i = 0; i < size; ++i) { + replacements.insert({group[i], canonical[i]}); + } + } + } } + state.updateShallow(replacements); return {}; } @@ -3467,6 +3852,10 @@ size_t hash<wasm::HeapType>::operator()(const wasm::HeapType& heapType) const { return wasm::hash(heapType.getID()); } +size_t hash<wasm::RecGroup>::operator()(const wasm::RecGroup& group) const { + return wasm::hash(group.getID()); +} + size_t hash<wasm::Rtt>::operator()(const wasm::Rtt& rtt) const { auto digest = wasm::hash(rtt.depth); wasm::rehash(digest, rtt.heapType); diff --git a/test/gtest/type-builder.cpp b/test/gtest/type-builder.cpp index 9cbd04298..8f922380d 100644 --- a/test/gtest/type-builder.cpp +++ b/test/gtest/type-builder.cpp @@ -16,6 +16,17 @@ protected: destroyAllTypesForTestingPurposesOnly(); setTypeSystem(originalSystem); } + + // Utilities + Struct makeStruct(TypeBuilder& builder, + std::initializer_list<size_t> indices) { + FieldList fields; + for (auto index : indices) { + Type ref = builder.getTempRefType(builder[index], Nullable); + fields.emplace_back(ref, Mutable); + } + return Struct(std::move(fields)); + } }; using TypeTest = TypeSystemTest<TypeSystem::Equirecursive>; @@ -214,15 +225,6 @@ static void testInvalidSupertype() { TEST_F(NominalTest, InvalidSupertype) { testInvalidSupertype(); } TEST_F(IsorecursiveTest, InvalidSupertype) { testInvalidSupertype(); } -TEST_F(IsorecursiveTest, SelfReferencedChild) { - // A self-referential type should be ok even without an explicit rec group. - TypeBuilder builder(1); - Type selfRef = builder.getTempRefType(builder[0], Nullable); - builder[0] = Struct({Field(selfRef, Immutable)}); - auto result = builder.build(); - EXPECT_TRUE(result); -} - TEST_F(IsorecursiveTest, ForwardReferencedChild) { TypeBuilder builder(3); builder.createRecGroup(0, 2); @@ -269,3 +271,134 @@ TEST_F(IsorecursiveTest, RecGroupIndices) { EXPECT_EQ(built[3].getRecGroupIndex(), 1u); EXPECT_EQ(built[4].getRecGroupIndex(), 2u); } + +TEST_F(IsorecursiveTest, CanonicalizeGroups) { + // Trivial types in the same group are not equivalent. + TypeBuilder builderA(2); + builderA.createRecGroup(0, 2); + builderA[0] = Struct{}; + builderA[1] = Struct{}; + auto resultA = builderA.build(); + ASSERT_TRUE(resultA); + auto builtA = *resultA; + + EXPECT_NE(builtA[0], builtA[1]); + + // But if they are in their own separate groups, they are equivalent. + TypeBuilder builderB(2); + builderB[0] = Struct{}; + builderB[1] = Struct{}; + auto resultB = builderB.build(); + ASSERT_TRUE(resultB); + auto builtB = *resultB; + + EXPECT_EQ(builtB[0], builtB[1]); + EXPECT_NE(builtB[0], builtA[0]); + EXPECT_NE(builtB[0], builtA[1]); + + // If we build the same groups again, we should get the same results. + TypeBuilder builderA2(4); + builderA2.createRecGroup(0, 2); + builderA2.createRecGroup(2, 2); + builderA2[0] = Struct{}; + builderA2[1] = Struct{}; + builderA2[2] = Struct{}; + builderA2[3] = Struct{}; + auto resultA2 = builderA2.build(); + ASSERT_TRUE(resultA2); + auto builtA2 = *resultA2; + + EXPECT_EQ(builtA2[0], builtA[0]); + EXPECT_EQ(builtA2[1], builtA[1]); + EXPECT_EQ(builtA2[2], builtA[0]); + EXPECT_EQ(builtA2[3], builtA[1]); + + TypeBuilder builderB2(1); + builderB2[0] = Struct{}; + auto resultB2 = builderB2.build(); + ASSERT_TRUE(resultB2); + auto builtB2 = *resultB2; + + EXPECT_EQ(builtB2[0], builtB[0]); +} + +TEST_F(IsorecursiveTest, CanonicalizeUses) { + TypeBuilder builder(8); + builder[0] = makeStruct(builder, {}); + builder[1] = makeStruct(builder, {}); + builder[2] = makeStruct(builder, {0}); + builder[3] = makeStruct(builder, {1}); + builder[4] = makeStruct(builder, {0, 2}); + builder[5] = makeStruct(builder, {1, 3}); + builder[6] = makeStruct(builder, {2, 4}); + builder[7] = makeStruct(builder, {3, 5}); + + auto result = builder.build(); + ASSERT_TRUE(result); + auto built = *result; + + EXPECT_EQ(built[0], built[1]); + EXPECT_EQ(built[2], built[3]); + EXPECT_EQ(built[4], built[5]); + EXPECT_EQ(built[6], built[7]); + + EXPECT_NE(built[0], built[2]); + EXPECT_NE(built[0], built[4]); + EXPECT_NE(built[0], built[6]); + EXPECT_NE(built[2], built[4]); + EXPECT_NE(built[2], built[6]); + EXPECT_NE(built[4], built[6]); +} + +TEST_F(IsorecursiveTest, CanonicalizeSelfReferences) { + TypeBuilder builder(5); + // Single self-reference + builder[0] = makeStruct(builder, {0}); + builder[1] = makeStruct(builder, {1}); + // Single other reference + builder[2] = makeStruct(builder, {0}); + // Other reference followed by self-reference + builder[3] = makeStruct(builder, {2, 3}); + // Self-reference followed by other reference + builder[4] = makeStruct(builder, {4, 2}); + + auto result = builder.build(); + ASSERT_TRUE(result); + auto built = *result; + + EXPECT_EQ(built[0], built[1]); + + EXPECT_NE(built[0], built[2]); + EXPECT_NE(built[0], built[3]); + EXPECT_NE(built[0], built[4]); + EXPECT_NE(built[2], built[3]); + EXPECT_NE(built[2], built[4]); + EXPECT_NE(built[3], built[4]); +} + +TEST_F(IsorecursiveTest, CanonicalizeSupertypes) { + TypeBuilder builder(6); + builder[0] = Struct{}; + builder[1] = Struct{}; + // Type with a supertype + builder[2] = Struct{}; + builder[2].subTypeOf(builder[0]); + // Type with the same supertype after canonicalization. + builder[3] = Struct{}; + builder[3].subTypeOf(builder[1]); + // Type with a different supertype + builder[4] = Struct{}; + builder[4].subTypeOf(builder[2]); + // Type with no supertype + builder[5] = Struct{}; + + auto result = builder.build(); + ASSERT_TRUE(result); + auto built = *result; + + EXPECT_EQ(built[2], built[3]); + + EXPECT_NE(built[3], built[4]); + EXPECT_NE(built[3], built[5]); + EXPECT_NE(built[4], built[5]); +} |