spirv-fuzz: improvements to representation of data synonym facts (#3006)

This change fixes a bug in EquivalenceRelation, changes the interface
of EquivalenceRelation to avoid exposing (potentially
nondeterministic) unordered sets, and changes the interface of
FactManager to allow querying data synonyms directly. These interface
changes have required a lot of corresponding changes to client code
and tests.
diff --git a/source/fuzz/data_descriptor.cpp b/source/fuzz/data_descriptor.cpp
index a88d992..86e5325 100644
--- a/source/fuzz/data_descriptor.cpp
+++ b/source/fuzz/data_descriptor.cpp
@@ -20,14 +20,12 @@
 namespace fuzz {
 
 protobufs::DataDescriptor MakeDataDescriptor(uint32_t object,
-                                             std::vector<uint32_t>&& indices,
-                                             uint32_t num_contiguous_elements) {
+                                             std::vector<uint32_t>&& indices) {
   protobufs::DataDescriptor result;
   result.set_object(object);
   for (auto index : indices) {
     result.add_index(index);
   }
-  result.set_num_contiguous_elements(num_contiguous_elements);
   return result;
 }
 
@@ -50,5 +48,22 @@
                     second->index().begin());
 }
 
+std::ostream& operator<<(std::ostream& out,
+                         const protobufs::DataDescriptor& data_descriptor) {
+  out << data_descriptor.object();
+  out << "[";
+  bool first = true;
+  for (auto index : data_descriptor.index()) {
+    if (first) {
+      first = false;
+    } else {
+      out << ", ";
+    }
+    out << index;
+  }
+  out << "]";
+  return out;
+}
+
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/data_descriptor.h b/source/fuzz/data_descriptor.h
index 3d8818e..c569ac8 100644
--- a/source/fuzz/data_descriptor.h
+++ b/source/fuzz/data_descriptor.h
@@ -17,6 +17,7 @@
 
 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
 
+#include <ostream>
 #include <vector>
 
 namespace spvtools {
@@ -25,8 +26,7 @@
 // Factory method to create a data descriptor message from an object id and a
 // list of indices.
 protobufs::DataDescriptor MakeDataDescriptor(uint32_t object,
-                                             std::vector<uint32_t>&& indices,
-                                             uint32_t num_contiguous_elements);
+                                             std::vector<uint32_t>&& indices);
 
 // Hash function for data descriptors.
 struct DataDescriptorHash {
@@ -39,6 +39,9 @@
                   const protobufs::DataDescriptor* second) const;
 };
 
+std::ostream& operator<<(std::ostream& out,
+                         const protobufs::DataDescriptor& data_descriptor);
+
 }  // namespace fuzz
 }  // namespace spvtools
 
diff --git a/source/fuzz/equivalence_relation.h b/source/fuzz/equivalence_relation.h
index 61e1be7..4f7eff1 100644
--- a/source/fuzz/equivalence_relation.h
+++ b/source/fuzz/equivalence_relation.h
@@ -68,8 +68,6 @@
 template <typename T, typename PointerHashT, typename PointerEqualsT>
 class EquivalenceRelation {
  public:
-  using ValueSet = std::unordered_set<const T*, PointerHashT, PointerEqualsT>;
-
   // Merges the equivalence classes associated with |value1| and |value2|.
   // If any of these values was not previously in the equivalence relation, it
   // is added to the pool of values known to be in the relation.
@@ -86,6 +84,7 @@
 
         // Initially say that the value is its own parent and that it has no
         // children.
+        assert(pointer_to_value && "Representatives should never be null.");
         parent_[pointer_to_value] = pointer_to_value;
         children_[pointer_to_value] = std::unordered_set<const T*>();
       }
@@ -105,18 +104,31 @@
     // are not already in the same class, make one the parent of the other.
     const T* representative1 = Find(value1_ptr);
     const T* representative2 = Find(value2_ptr);
+    assert(representative1 && "Representatives should never be null.");
+    assert(representative2 && "Representatives should never be null.");
     if (representative1 != representative2) {
       parent_[representative1] = representative2;
       children_[representative2].insert(representative1);
     }
   }
 
+  // Returns exactly one representative per equivalence class.
+  std::vector<const T*> GetEquivalenceClassRepresentatives() const {
+    std::vector<const T*> result;
+    for (auto& value : owned_values_) {
+      if (parent_[value.get()] == value.get()) {
+        result.push_back(value.get());
+      }
+    }
+    return result;
+  }
+
   // Returns pointers to all values in the equivalence class of |value|, which
   // must already be part of the equivalence relation.
-  ValueSet GetEquivalenceClass(const T& value) const {
+  std::vector<const T*> GetEquivalenceClass(const T& value) const {
     assert(Exists(value));
 
-    ValueSet result;
+    std::vector<const T*> result;
 
     // Traverse the tree of values rooted at the representative of the
     // equivalence class to which |value| belongs, and collect up all the values
@@ -125,7 +137,7 @@
     stack.push_back(Find(*value_set_.find(&value)));
     while (!stack.empty()) {
       const T* item = stack.back();
-      result.insert(item);
+      result.push_back(item);
       stack.pop_back();
       for (auto child : children_[item]) {
         stack.push_back(child);
@@ -141,40 +153,45 @@
     return Find(&value1) == Find(&value2);
   }
 
-  // Returns the set of all values known to be part of the equivalence relation.
-  ValueSet GetAllKnownValues() const {
-    ValueSet result;
+  // Returns all values known to be part of the equivalence relation.
+  std::vector<const T*> GetAllKnownValues() const {
+    std::vector<const T*> result;
     for (auto& value : owned_values_) {
-      result.insert(value.get());
+      result.push_back(value.get());
     }
     return result;
   }
 
- private:
   // Returns true if and only if |value| is known to be part of the equivalence
   // relation.
   bool Exists(const T& value) const {
     return value_set_.find(&value) != value_set_.end();
   }
 
+ private:
   // Returns the representative of the equivalence class of |value|, which must
   // already be known to the equivalence relation.  This is the 'Find' operation
   // in a classic union-find data structure.
   const T* Find(const T* value) const {
     assert(Exists(*value));
 
+    // Get the canonical pointer to the value from the value pool.
+    const T* known_value = *value_set_.find(value);
+    assert(parent_[known_value] && "Every known value should have a parent.");
+
     // Compute the result by chasing parents until we find a value that is its
     // own parent.
-    const T* result = value;
+    const T* result = known_value;
     while (parent_[result] != result) {
       result = parent_[result];
     }
+    assert(result && "Representatives should never be null.");
 
     // At this point, |result| is the representative of the equivalence class.
     // Now perform the 'path compression' optimization by doing another pass up
     // the parent chain, setting the parent of each node to be the
     // representative, and rewriting children correspondingly.
-    const T* current = value;
+    const T* current = known_value;
     while (parent_[current] != result) {
       const T* next = parent_[current];
       parent_[current] = result;
@@ -205,7 +222,7 @@
   // |owned_values_|, and |value_pool_| provides (via |PointerHashT| and
   // |PointerEqualsT|) a means for mapping a value of interest to a pointer
   // into an equivalent value in |owned_values_|.
-  ValueSet value_set_;
+  std::unordered_set<const T*, PointerHashT, PointerEqualsT> value_set_;
   std::vector<std::unique_ptr<T>> owned_values_;
 };
 
diff --git a/source/fuzz/fact_manager.cpp b/source/fuzz/fact_manager.cpp
index 59e50ac..017db21 100644
--- a/source/fuzz/fact_manager.cpp
+++ b/source/fuzz/fact_manager.cpp
@@ -16,7 +16,6 @@
 
 #include <map>
 #include <sstream>
-#include <unordered_set>
 
 #include "source/fuzz/equivalence_relation.h"
 #include "source/fuzz/fuzzer_util.h"
@@ -328,6 +327,10 @@
   // See method in FactManager which delegates to this method.
   void AddFact(const protobufs::FactDataSynonym& fact);
 
+  // See method in FactManager which delegates to this method.
+  bool IsSynonymous(const protobufs::DataDescriptor& data_descriptor1,
+                    const protobufs::DataDescriptor& data_descriptor2) const;
+
   EquivalenceRelation<protobufs::DataDescriptor, DataDescriptorHash,
                       DataDescriptorEquals>
       synonymous;
@@ -338,6 +341,14 @@
   synonymous.MakeEquivalent(fact.data1(), fact.data2());
 }
 
+bool FactManager::DataSynonymFacts::IsSynonymous(
+    const protobufs::DataDescriptor& data_descriptor1,
+    const protobufs::DataDescriptor& data_descriptor2) const {
+  return synonymous.Exists(data_descriptor1) &&
+         synonymous.Exists(data_descriptor2) &&
+         synonymous.IsEquivalent(data_descriptor1, data_descriptor2);
+}
+
 // End of data synonym facts
 //==============================
 
@@ -375,7 +386,8 @@
 }
 
 void FactManager::AddFactDataSynonym(const protobufs::DataDescriptor& data1,
-                                     const protobufs::DataDescriptor& data2) {
+                                     const protobufs::DataDescriptor& data2,
+                                     opt::IRContext* /*unused*/) {
   protobufs::FactDataSynonym fact;
   *fact.mutable_data1() = data1;
   *fact.mutable_data2() = data2;
@@ -412,27 +424,28 @@
   return uniform_constant_facts_->facts_and_type_ids;
 }
 
-std::set<uint32_t> FactManager::GetIdsForWhichSynonymsAreKnown() const {
-  std::set<uint32_t> result;
+std::vector<uint32_t> FactManager::GetIdsForWhichSynonymsAreKnown() const {
+  std::vector<uint32_t> result;
   for (auto& data_descriptor :
        data_synonym_facts_->synonymous.GetAllKnownValues()) {
     if (data_descriptor->index().empty()) {
-      assert(data_descriptor->num_contiguous_elements() == 1 &&
-             "Multiple contiguous elements are only allowed for data "
-             "descriptors that "
-             "are indices into vectors.");
-      result.insert(data_descriptor->object());
+      result.push_back(data_descriptor->object());
     }
   }
   return result;
 }
 
-std::unordered_set<const protobufs::DataDescriptor*, DataDescriptorHash,
-                   DataDescriptorEquals>
-FactManager::GetSynonymsForId(uint32_t id) const {
+std::vector<const protobufs::DataDescriptor*> FactManager::GetSynonymsForId(
+    uint32_t id) const {
   return data_synonym_facts_->synonymous.GetEquivalenceClass(
-      MakeDataDescriptor(id, {}, 1));
+      MakeDataDescriptor(id, {}));
 }
 
+bool FactManager::IsSynonymous(
+    const protobufs::DataDescriptor& data_descriptor1,
+    const protobufs::DataDescriptor& data_descriptor2) const {
+  return data_synonym_facts_->IsSynonymous(data_descriptor1, data_descriptor2);
+};
+
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/fact_manager.h b/source/fuzz/fact_manager.h
index b120b8d..7ee31d0 100644
--- a/source/fuzz/fact_manager.h
+++ b/source/fuzz/fact_manager.h
@@ -55,7 +55,8 @@
 
   // Record the fact that |data1| and |data2| are synonymous.
   void AddFactDataSynonym(const protobufs::DataDescriptor& data1,
-                          const protobufs::DataDescriptor& data2);
+                          const protobufs::DataDescriptor& data2,
+                          opt::IRContext* context);
 
   // The fact manager is responsible for managing a few distinct categories of
   // facts. In principle there could be different fact managers for each kind
@@ -106,13 +107,17 @@
 
   // Returns every id for which a fact of the form "this id is synonymous
   // with this piece of data" is known.
-  std::set<uint32_t> GetIdsForWhichSynonymsAreKnown() const;
+  std::vector<uint32_t> GetIdsForWhichSynonymsAreKnown() const;
 
-  // Requires that at least one synonym for |id| is known, and returns the
-  // equivalence class of all known synonyms.
-  std::unordered_set<const protobufs::DataDescriptor*, DataDescriptorHash,
-                     DataDescriptorEquals>
-  GetSynonymsForId(uint32_t id) const;
+  // Returns the equivalence class of all known synonyms of |id|, or an empty
+  // set if no synonyms are known.
+  std::vector<const protobufs::DataDescriptor*> GetSynonymsForId(
+      uint32_t id) const;
+
+  // Return true if and ony if |data_descriptor1| and |data_descriptor2| are
+  // known to be synonymous.
+  bool IsSynonymous(const protobufs::DataDescriptor& data_descriptor1,
+                    const protobufs::DataDescriptor& data_descriptor2) const;
 
   // End of id synonym facts
   //==============================
@@ -121,13 +126,13 @@
   // For each distinct kind of fact to be managed, we use a separate opaque
   // struct type.
 
-  struct ConstantUniformFacts;  // Opaque struct for holding data about uniform
-                                // buffer elements.
+  struct ConstantUniformFacts;  // Opaque class for management of
+                                // constant uniform facts.
   std::unique_ptr<ConstantUniformFacts>
       uniform_constant_facts_;  // Unique pointer to internal data.
 
-  struct DataSynonymFacts;  // Opaque struct for holding data about data
-                            // synonyms.
+  struct DataSynonymFacts;  // Opaque class for management of data synonym
+                            // facts.
   std::unique_ptr<DataSynonymFacts>
       data_synonym_facts_;  // Unique pointer to internal data.
 };
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index 060a44d..b558ca8 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -82,10 +82,6 @@
   // 0 or more indices, used to index into a composite object
   repeated uint32 index = 2;
 
-  // The number of contiguous elements.  This will typically be 1, but e.g. 2 or
-  // 3 can be used to describe the 'xy' or 'xyz' portion of a vec4.
-  uint32 num_contiguous_elements = 3;
-
 }
 
 message UniformBufferElementDescriptor {
diff --git a/source/fuzz/transformation_composite_construct.cpp b/source/fuzz/transformation_composite_construct.cpp
index fde3c9b..7a3aff1 100644
--- a/source/fuzz/transformation_composite_construct.cpp
+++ b/source/fuzz/transformation_composite_construct.cpp
@@ -133,45 +133,45 @@
       context, SpvOpCompositeConstruct, message_.composite_type_id(),
       message_.fresh_id(), in_operands));
 
+  fuzzerutil::UpdateModuleIdBound(context, message_.fresh_id());
+  context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+
   // Inform the fact manager that we now have new synonyms: every component of
-  // the composite is synonymous with the id used to construct that component.
+  // the composite is synonymous with the id used to construct that component,
+  // except in the case of a vector where a single vector id can span multiple
+  // components.
   auto composite_type =
       context->get_type_mgr()->GetType(message_.composite_type_id());
   uint32_t index = 0;
   for (auto component : message_.component()) {
-    // Decide how many contiguous composite elements are represented by the
-    // components.  This is always 1, except in the case of a vector that is
-    // constructed by smaller vectors.
-    uint32_t num_contiguous_elements;
-    if (composite_type->AsVector()) {
-      // The vector case is a bit fiddly, because one argument to a vector
-      // constructor can cover more than one element.
-      auto component_type = context->get_type_mgr()->GetType(
-          context->get_def_use_mgr()->GetDef(component)->type_id());
-      if (component_type->AsVector()) {
-        assert(component_type->AsVector()->element_type() ==
-               composite_type->AsVector()->element_type());
-        num_contiguous_elements = component_type->AsVector()->element_count();
-      } else {
-        assert(component_type == composite_type->AsVector()->element_type());
-        num_contiguous_elements = 1;
+    auto component_type = context->get_type_mgr()->GetType(
+        context->get_def_use_mgr()->GetDef(component)->type_id());
+    if (composite_type->AsVector() && component_type->AsVector()) {
+      // The case where the composite being constructed is a vector and the
+      // component provided for construction is also a vector is special.  It
+      // requires adding a synonym fact relating each element of the sub-vector
+      // to the corresponding element of the composite being constructed.
+      assert(component_type->AsVector()->element_type() ==
+             composite_type->AsVector()->element_type());
+      assert(component_type->AsVector()->element_count() <
+             composite_type->AsVector()->element_count());
+      for (uint32_t subvector_index = 0;
+           subvector_index < component_type->AsVector()->element_count();
+           subvector_index++) {
+        fact_manager->AddFactDataSynonym(
+            MakeDataDescriptor(component, {subvector_index}),
+            MakeDataDescriptor(message_.fresh_id(), {index}), context);
+        index++;
       }
     } else {
-      // The non-vector cases are all easy: the constructor has exactly the same
-      // number of arguments as the number of sub-components, so we can just
-      // increment the index.
-      num_contiguous_elements = 1;
+      // The other cases are simple: the component is made directly synonymous
+      // with the element of the composite being constructed.
+      fact_manager->AddFactDataSynonym(
+          MakeDataDescriptor(component, {}),
+          MakeDataDescriptor(message_.fresh_id(), {index}), context);
+      index++;
     }
-
-    fact_manager->AddFactDataSynonym(
-        MakeDataDescriptor(component, {}, 1),
-        MakeDataDescriptor(message_.fresh_id(), {index},
-                           num_contiguous_elements));
-    index += num_contiguous_elements;
   }
-
-  fuzzerutil::UpdateModuleIdBound(context, message_.fresh_id());
-  context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
 }
 
 bool TransformationCompositeConstruct::ComponentsForArrayConstructionAreOK(
diff --git a/source/fuzz/transformation_composite_extract.cpp b/source/fuzz/transformation_composite_extract.cpp
index c5c694d..8c706bb 100644
--- a/source/fuzz/transformation_composite_extract.cpp
+++ b/source/fuzz/transformation_composite_extract.cpp
@@ -106,11 +106,11 @@
     indices.push_back(an_index);
   }
   protobufs::DataDescriptor data_descriptor_for_extracted_element =
-      MakeDataDescriptor(message_.composite_id(), std::move(indices), 1);
+      MakeDataDescriptor(message_.composite_id(), std::move(indices));
   protobufs::DataDescriptor data_descriptor_for_result_id =
-      MakeDataDescriptor(message_.fresh_id(), {}, 1);
+      MakeDataDescriptor(message_.fresh_id(), {});
   fact_manager->AddFactDataSynonym(data_descriptor_for_extracted_element,
-                                   data_descriptor_for_result_id);
+                                   data_descriptor_for_result_id, context);
 }
 
 protobufs::Transformation TransformationCompositeExtract::ToMessage() const {
diff --git a/source/fuzz/transformation_copy_object.cpp b/source/fuzz/transformation_copy_object.cpp
index cc3c160..af1e81c 100644
--- a/source/fuzz/transformation_copy_object.cpp
+++ b/source/fuzz/transformation_copy_object.cpp
@@ -102,9 +102,9 @@
   fuzzerutil::UpdateModuleIdBound(context, message_.fresh_id());
   context->InvalidateAnalysesExceptFor(opt::IRContext::Analysis::kAnalysisNone);
 
-  fact_manager->AddFactDataSynonym(
-      MakeDataDescriptor(message_.object(), {}, 1),
-      MakeDataDescriptor(message_.fresh_id(), {}, 1));
+  fact_manager->AddFactDataSynonym(MakeDataDescriptor(message_.object(), {}),
+                                   MakeDataDescriptor(message_.fresh_id(), {}),
+                                   context);
 }
 
 protobufs::Transformation TransformationCopyObject::ToMessage() const {
diff --git a/source/fuzz/transformation_replace_id_with_synonym.cpp b/source/fuzz/transformation_replace_id_with_synonym.cpp
index ec5081c..0e8b891 100644
--- a/source/fuzz/transformation_replace_id_with_synonym.cpp
+++ b/source/fuzz/transformation_replace_id_with_synonym.cpp
@@ -46,8 +46,9 @@
   auto id_of_interest = message_.id_use_descriptor().id_of_interest();
 
   // Does the fact manager know about the synonym?
-  if (fact_manager.GetIdsForWhichSynonymsAreKnown().count(id_of_interest) ==
-      0) {
+  auto ids_with_known_synonyms = fact_manager.GetIdsForWhichSynonymsAreKnown();
+  if (std::find(ids_with_known_synonyms.begin(), ids_with_known_synonyms.end(),
+                id_of_interest) == ids_with_known_synonyms.end()) {
     return false;
   }
 
diff --git a/test/fuzz/equivalence_relation_test.cpp b/test/fuzz/equivalence_relation_test.cpp
index 9ab4b23..a4f4ad4 100644
--- a/test/fuzz/equivalence_relation_test.cpp
+++ b/test/fuzz/equivalence_relation_test.cpp
@@ -14,6 +14,7 @@
 
 #include <set>
 
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "source/fuzz/equivalence_relation.h"
 
@@ -33,12 +34,11 @@
   }
 };
 
-std::set<uint32_t> ToUIntSet(
-    EquivalenceRelation<uint32_t, UInt32Hash, UInt32Equals>::ValueSet
-        pointers) {
-  std::set<uint32_t> result;
+std::vector<uint32_t> ToUIntVector(
+    const std::vector<const uint32_t*>& pointers) {
+  std::vector<uint32_t> result;
   for (auto pointer : pointers) {
-    result.insert(*pointer);
+    result.push_back(*pointer);
   }
   return result;
 }
@@ -59,38 +59,63 @@
 
   relation.MakeEquivalent(78, 80);
 
-  std::set<uint32_t> class1;
+  std::vector<uint32_t> class1;
   for (uint32_t element = 0; element < 98; element += 2) {
     ASSERT_TRUE(relation.IsEquivalent(0, element));
     ASSERT_TRUE(relation.IsEquivalent(element, element + 2));
-    class1.insert(element);
+    class1.push_back(element);
   }
-  class1.insert(98);
-  ASSERT_TRUE(class1 == ToUIntSet(relation.GetEquivalenceClass(0)));
-  ASSERT_TRUE(class1 == ToUIntSet(relation.GetEquivalenceClass(4)));
-  ASSERT_TRUE(class1 == ToUIntSet(relation.GetEquivalenceClass(40)));
+  class1.push_back(98);
 
-  std::set<uint32_t> class2;
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(0)),
+              testing::WhenSorted(class1));
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(4)),
+              testing::WhenSorted(class1));
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(40)),
+              testing::WhenSorted(class1));
+
+  std::vector<uint32_t> class2;
   for (uint32_t element = 1; element < 79; element += 2) {
     ASSERT_TRUE(relation.IsEquivalent(1, element));
     ASSERT_TRUE(relation.IsEquivalent(element, element + 2));
-    class2.insert(element);
+    class2.push_back(element);
   }
-  class2.insert(79);
-  ASSERT_TRUE(class2 == ToUIntSet(relation.GetEquivalenceClass(1)));
-  ASSERT_TRUE(class2 == ToUIntSet(relation.GetEquivalenceClass(11)));
-  ASSERT_TRUE(class2 == ToUIntSet(relation.GetEquivalenceClass(31)));
+  class2.push_back(79);
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(1)),
+              testing::WhenSorted(class2));
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(11)),
+              testing::WhenSorted(class2));
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(31)),
+              testing::WhenSorted(class2));
 
-  std::set<uint32_t> class3;
+  std::vector<uint32_t> class3;
   for (uint32_t element = 81; element < 99; element += 2) {
     ASSERT_TRUE(relation.IsEquivalent(81, element));
     ASSERT_TRUE(relation.IsEquivalent(element, element + 2));
-    class3.insert(element);
+    class3.push_back(element);
   }
-  class3.insert(99);
-  ASSERT_TRUE(class3 == ToUIntSet(relation.GetEquivalenceClass(81)));
-  ASSERT_TRUE(class3 == ToUIntSet(relation.GetEquivalenceClass(91)));
-  ASSERT_TRUE(class3 == ToUIntSet(relation.GetEquivalenceClass(99)));
+  class3.push_back(99);
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(81)),
+              testing::WhenSorted(class3));
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(91)),
+              testing::WhenSorted(class3));
+  ASSERT_THAT(ToUIntVector(relation.GetEquivalenceClass(99)),
+              testing::WhenSorted(class3));
+
+  bool first = true;
+  std::vector<const uint32_t*> previous_class;
+  for (auto representative : relation.GetEquivalenceClassRepresentatives()) {
+    std::vector<const uint32_t*> current_class =
+        relation.GetEquivalenceClass(*representative);
+    ASSERT_TRUE(std::find(current_class.begin(), current_class.end(),
+                          representative) != current_class.end());
+    if (!first) {
+      ASSERT_TRUE(std::find(previous_class.begin(), previous_class.end(),
+                            representative) == previous_class.end());
+    }
+    previous_class = current_class;
+    first = false;
+  }
 }
 
 }  // namespace
diff --git a/test/fuzz/transformation_composite_construct_test.cpp b/test/fuzz/transformation_composite_construct_test.cpp
index 7f1faf7..b73bb3d 100644
--- a/test/fuzz/transformation_composite_construct_test.cpp
+++ b/test/fuzz/transformation_composite_construct_test.cpp
@@ -21,21 +21,6 @@
 namespace fuzz {
 namespace {
 
-bool SynonymFactHolds(const FactManager& fact_manager, uint32_t id,
-                      uint32_t synonym_base_id,
-                      std::vector<uint32_t>&& synonym_indices) {
-  if (fact_manager.GetIdsForWhichSynonymsAreKnown().count(id) == 0) {
-    return false;
-  }
-  auto synonyms = fact_manager.GetSynonymsForId(id);
-  auto temp =
-      MakeDataDescriptor(synonym_base_id, std::move(synonym_indices), 1);
-  return std::find_if(synonyms.begin(), synonyms.end(),
-                      [&temp](const protobufs::DataDescriptor* dd) -> bool {
-                        return DataDescriptorEquals()(dd, &temp);
-                      }) != synonyms.end();
-}
-
 TEST(TransformationCompositeConstructTest, ConstructArrays) {
   std::string shader = R"(
                OpCapability Shader
@@ -159,9 +144,12 @@
       make_vec2_array_length_3_bad.IsApplicable(context.get(), fact_manager));
   make_vec2_array_length_3.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 41, 200, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 45, 200, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 27, 200, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(41, {}),
+                                        MakeDataDescriptor(200, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(45, {}),
+                                        MakeDataDescriptor(200, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(27, {}),
+                                        MakeDataDescriptor(200, {2})));
 
   // Make a float[2]
   TransformationCompositeConstruct make_float_array_length_2(
@@ -175,8 +163,10 @@
       make_float_array_length_2_bad.IsApplicable(context.get(), fact_manager));
   make_float_array_length_2.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 24, 201, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 40, 201, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(24, {}),
+                                        MakeDataDescriptor(201, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(40, {}),
+                                        MakeDataDescriptor(201, {1})));
 
   // Make a bool[3]
   TransformationCompositeConstruct make_bool_array_length_3(
@@ -192,9 +182,12 @@
       make_bool_array_length_3_bad.IsApplicable(context.get(), fact_manager));
   make_bool_array_length_3.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 33, 202, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 50, 202, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 50, 202, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(33, {}),
+                                        MakeDataDescriptor(202, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(50, {}),
+                                        MakeDataDescriptor(202, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(50, {}),
+                                        MakeDataDescriptor(202, {2})));
 
   // make a uvec3[2][2]
   TransformationCompositeConstruct make_uvec3_array_length_2_2(
@@ -208,8 +201,10 @@
                                                             fact_manager));
   make_uvec3_array_length_2_2.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 69, 203, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 100, 203, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(69, {}),
+                                        MakeDataDescriptor(203, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(100, {}),
+                                        MakeDataDescriptor(203, {1})));
 
   std::string after_transformation = R"(
                OpCapability Shader
@@ -408,9 +403,12 @@
   ASSERT_FALSE(make_mat34_bad.IsApplicable(context.get(), fact_manager));
   make_mat34.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 25, 200, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 28, 200, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 31, 200, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(25, {}),
+                                        MakeDataDescriptor(200, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(28, {}),
+                                        MakeDataDescriptor(200, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(31, {}),
+                                        MakeDataDescriptor(200, {2})));
 
   // make a mat4x3
   TransformationCompositeConstruct make_mat43(
@@ -422,10 +420,14 @@
   ASSERT_FALSE(make_mat43_bad.IsApplicable(context.get(), fact_manager));
   make_mat43.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 11, 201, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 13, 201, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 16, 201, {2}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 100, 201, {3}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(11, {}),
+                                        MakeDataDescriptor(201, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(13, {}),
+                                        MakeDataDescriptor(201, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(16, {}),
+                                        MakeDataDescriptor(201, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(100, {}),
+                                        MakeDataDescriptor(201, {3})));
 
   std::string after_transformation = R"(
                OpCapability Shader
@@ -609,8 +611,10 @@
   ASSERT_FALSE(make_inner_bad.IsApplicable(context.get(), fact_manager));
   make_inner.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 25, 200, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 19, 200, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(25, {}),
+                                        MakeDataDescriptor(200, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(19, {}),
+                                        MakeDataDescriptor(200, {1})));
 
   // make an Outer
   TransformationCompositeConstruct make_outer(
@@ -624,9 +628,12 @@
   ASSERT_FALSE(make_outer_bad.IsApplicable(context.get(), fact_manager));
   make_outer.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 46, 201, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 200, 201, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 56, 201, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(46, {}),
+                                        MakeDataDescriptor(201, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(200, {}),
+                                        MakeDataDescriptor(201, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(56, {}),
+                                        MakeDataDescriptor(201, {2})));
 
   std::string after_transformation = R"(
                OpCapability Shader
@@ -922,8 +929,10 @@
   ASSERT_FALSE(make_vec2_bad.IsApplicable(context.get(), fact_manager));
   make_vec2.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 17, 200, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 11, 200, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(17, {}),
+                                        MakeDataDescriptor(200, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(11, {}),
+                                        MakeDataDescriptor(200, {1})));
 
   TransformationCompositeConstruct make_vec3(
       25, {12, 32}, MakeInstructionDescriptor(35, SpvOpCompositeConstruct, 0),
@@ -936,8 +945,12 @@
   ASSERT_FALSE(make_vec3_bad.IsApplicable(context.get(), fact_manager));
   make_vec3.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 12, 201, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 32, 201, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(12, {0}),
+                                        MakeDataDescriptor(201, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(12, {1}),
+                                        MakeDataDescriptor(201, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(32, {}),
+                                        MakeDataDescriptor(201, {2})));
 
   TransformationCompositeConstruct make_vec4(
       44, {32, 32, 10, 11}, MakeInstructionDescriptor(75, SpvOpAccessChain, 0),
@@ -950,10 +963,14 @@
   ASSERT_FALSE(make_vec4_bad.IsApplicable(context.get(), fact_manager));
   make_vec4.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 32, 202, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 32, 202, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 10, 202, {2}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 11, 202, {3}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(32, {}),
+                                        MakeDataDescriptor(202, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(32, {}),
+                                        MakeDataDescriptor(202, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(10, {}),
+                                        MakeDataDescriptor(202, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(11, {}),
+                                        MakeDataDescriptor(202, {3})));
 
   TransformationCompositeConstruct make_ivec2(
       51, {126, 120}, MakeInstructionDescriptor(128, SpvOpLoad, 0), 203);
@@ -964,8 +981,10 @@
   ASSERT_FALSE(make_ivec2_bad.IsApplicable(context.get(), fact_manager));
   make_ivec2.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 126, 203, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 120, 203, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(126, {}),
+                                        MakeDataDescriptor(203, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(120, {}),
+                                        MakeDataDescriptor(203, {1})));
 
   TransformationCompositeConstruct make_ivec3(
       114, {56, 117, 56}, MakeInstructionDescriptor(66, SpvOpAccessChain, 0),
@@ -978,9 +997,12 @@
   ASSERT_FALSE(make_ivec3_bad.IsApplicable(context.get(), fact_manager));
   make_ivec3.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 56, 204, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 117, 204, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 56, 204, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(56, {}),
+                                        MakeDataDescriptor(204, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(117, {}),
+                                        MakeDataDescriptor(204, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(56, {}),
+                                        MakeDataDescriptor(204, {2})));
 
   TransformationCompositeConstruct make_ivec4(
       122, {56, 117, 117, 117}, MakeInstructionDescriptor(66, SpvOpIAdd, 0),
@@ -993,10 +1015,14 @@
   ASSERT_FALSE(make_ivec4_bad.IsApplicable(context.get(), fact_manager));
   make_ivec4.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 56, 205, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 117, 205, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 117, 205, {2}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 117, 205, {3}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(56, {}),
+                                        MakeDataDescriptor(205, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(117, {}),
+                                        MakeDataDescriptor(205, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(117, {}),
+                                        MakeDataDescriptor(205, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(117, {}),
+                                        MakeDataDescriptor(205, {3})));
 
   TransformationCompositeConstruct make_uvec2(
       86, {18, 38}, MakeInstructionDescriptor(133, SpvOpAccessChain, 0), 206);
@@ -1006,8 +1032,10 @@
   ASSERT_FALSE(make_uvec2_bad.IsApplicable(context.get(), fact_manager));
   make_uvec2.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 18, 206, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 38, 206, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(18, {}),
+                                        MakeDataDescriptor(206, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(38, {}),
+                                        MakeDataDescriptor(206, {1})));
 
   TransformationCompositeConstruct make_uvec3(
       59, {14, 18, 136}, MakeInstructionDescriptor(137, SpvOpReturn, 0), 207);
@@ -1018,9 +1046,12 @@
   ASSERT_FALSE(make_uvec3_bad.IsApplicable(context.get(), fact_manager));
   make_uvec3.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 14, 207, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 18, 207, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 136, 207, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(14, {}),
+                                        MakeDataDescriptor(207, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(18, {}),
+                                        MakeDataDescriptor(207, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(136, {}),
+                                        MakeDataDescriptor(207, {2})));
 
   TransformationCompositeConstruct make_uvec4(
       131, {14, 18, 136, 136},
@@ -1033,10 +1064,14 @@
   ASSERT_FALSE(make_uvec4_bad.IsApplicable(context.get(), fact_manager));
   make_uvec4.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 14, 208, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 18, 208, {1}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 136, 208, {2}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 136, 208, {3}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(14, {}),
+                                        MakeDataDescriptor(208, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(18, {}),
+                                        MakeDataDescriptor(208, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(136, {}),
+                                        MakeDataDescriptor(208, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(136, {}),
+                                        MakeDataDescriptor(208, {3})));
 
   TransformationCompositeConstruct make_bvec2(
       102,
@@ -1057,8 +1092,10 @@
   ASSERT_FALSE(make_bvec2_bad.IsApplicable(context.get(), fact_manager));
   make_bvec2.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 111, 209, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 41, 209, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(111, {}),
+                                        MakeDataDescriptor(209, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(41, {}),
+                                        MakeDataDescriptor(209, {1})));
 
   TransformationCompositeConstruct make_bvec3(
       93, {108, 73}, MakeInstructionDescriptor(108, SpvOpStore, 0), 210);
@@ -1069,8 +1106,12 @@
   ASSERT_FALSE(make_bvec3_bad.IsApplicable(context.get(), fact_manager));
   make_bvec3.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 108, 210, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 73, 210, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(108, {0}),
+                                        MakeDataDescriptor(210, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(108, {1}),
+                                        MakeDataDescriptor(210, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(73, {}),
+                                        MakeDataDescriptor(210, {2})));
 
   TransformationCompositeConstruct make_bvec4(
       70, {108, 108}, MakeInstructionDescriptor(108, SpvOpBranch, 0), 211);
@@ -1081,8 +1122,14 @@
   ASSERT_FALSE(make_bvec4_bad.IsApplicable(context.get(), fact_manager));
   make_bvec4.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 108, 211, {0}));
-  ASSERT_TRUE(SynonymFactHolds(fact_manager, 108, 211, {2}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(108, {0}),
+                                        MakeDataDescriptor(211, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(108, {1}),
+                                        MakeDataDescriptor(211, {1})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(108, {0}),
+                                        MakeDataDescriptor(211, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(108, {1}),
+                                        MakeDataDescriptor(211, {3})));
 
   std::string after_transformation = R"(
                OpCapability Shader
diff --git a/test/fuzz/transformation_composite_extract_test.cpp b/test/fuzz/transformation_composite_extract_test.cpp
index 30d912c..172ce8e 100644
--- a/test/fuzz/transformation_composite_extract_test.cpp
+++ b/test/fuzz/transformation_composite_extract_test.cpp
@@ -20,13 +20,6 @@
 namespace fuzz {
 namespace {
 
-bool IsSynonymous(const FactManager& fact_manager, uint32_t id,
-                  uint32_t composite_id, std::vector<uint32_t>&& indices) {
-  protobufs::DataDescriptor data_descriptor =
-      MakeDataDescriptor(composite_id, std::move(indices), 1);
-  return fact_manager.GetSynonymsForId(id).count(&data_descriptor) == 1;
-}
-
 TEST(TransformationCompositeExtractTest, BasicTest) {
   std::string shader = R"(
                OpCapability Shader
@@ -179,12 +172,18 @@
   transformation_6.Apply(context.get(), &fact_manager);
   ASSERT_TRUE(IsValid(env, context.get()));
 
-  ASSERT_TRUE(IsSynonymous(fact_manager, 201, 100, {2}));
-  ASSERT_TRUE(IsSynonymous(fact_manager, 202, 104, {0, 2}));
-  ASSERT_TRUE(IsSynonymous(fact_manager, 203, 104, {0}));
-  ASSERT_TRUE(IsSynonymous(fact_manager, 204, 101, {0}));
-  ASSERT_TRUE(IsSynonymous(fact_manager, 205, 102, {2}));
-  ASSERT_TRUE(IsSynonymous(fact_manager, 206, 103, {1}));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(201, {}),
+                                        MakeDataDescriptor(100, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(202, {}),
+                                        MakeDataDescriptor(104, {0, 2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(203, {}),
+                                        MakeDataDescriptor(104, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(204, {}),
+                                        MakeDataDescriptor(101, {0})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(205, {}),
+                                        MakeDataDescriptor(102, {2})));
+  ASSERT_TRUE(fact_manager.IsSynonymous(MakeDataDescriptor(206, {}),
+                                        MakeDataDescriptor(103, {1})));
 
   std::string after_transformation = R"(
                OpCapability Shader
diff --git a/test/fuzz/transformation_copy_object_test.cpp b/test/fuzz/transformation_copy_object_test.cpp
index 2792a5c..cc35113 100644
--- a/test/fuzz/transformation_copy_object_test.cpp
+++ b/test/fuzz/transformation_copy_object_test.cpp
@@ -60,14 +60,16 @@
     ASSERT_TRUE(copy_true.IsApplicable(context.get(), fact_manager));
     copy_true.Apply(context.get(), &fact_manager);
 
-    std::set<uint32_t> ids_for_which_synonyms_are_known =
+    auto ids_for_which_synonyms_are_known =
         fact_manager.GetIdsForWhichSynonymsAreKnown();
     ASSERT_EQ(2, ids_for_which_synonyms_are_known.size());
-    ASSERT_TRUE(ids_for_which_synonyms_are_known.find(7) !=
-                ids_for_which_synonyms_are_known.end());
+    ASSERT_TRUE(std::find(ids_for_which_synonyms_are_known.begin(),
+                          ids_for_which_synonyms_are_known.end(),
+                          7) != ids_for_which_synonyms_are_known.end());
     ASSERT_EQ(2, fact_manager.GetSynonymsForId(7).size());
-    protobufs::DataDescriptor descriptor_100 = MakeDataDescriptor(100, {}, 1);
-    ASSERT_TRUE(fact_manager.GetSynonymsForId(7).count(&descriptor_100) > 0);
+    protobufs::DataDescriptor descriptor_100 = MakeDataDescriptor(100, {});
+    ASSERT_TRUE(
+        fact_manager.IsSynonymous(MakeDataDescriptor(7, {}), descriptor_100));
   }
 
   {
@@ -75,14 +77,16 @@
         8, MakeInstructionDescriptor(100, SpvOpReturn, 0), 101);
     ASSERT_TRUE(copy_false.IsApplicable(context.get(), fact_manager));
     copy_false.Apply(context.get(), &fact_manager);
-    std::set<uint32_t> ids_for_which_synonyms_are_known =
+    auto ids_for_which_synonyms_are_known =
         fact_manager.GetIdsForWhichSynonymsAreKnown();
     ASSERT_EQ(4, ids_for_which_synonyms_are_known.size());
-    ASSERT_TRUE(ids_for_which_synonyms_are_known.find(8) !=
-                ids_for_which_synonyms_are_known.end());
+    ASSERT_TRUE(std::find(ids_for_which_synonyms_are_known.begin(),
+                          ids_for_which_synonyms_are_known.end(),
+                          8) != ids_for_which_synonyms_are_known.end());
     ASSERT_EQ(2, fact_manager.GetSynonymsForId(8).size());
-    protobufs::DataDescriptor descriptor_101 = MakeDataDescriptor(101, {}, 1);
-    ASSERT_TRUE(fact_manager.GetSynonymsForId(8).count(&descriptor_101) > 0);
+    protobufs::DataDescriptor descriptor_101 = MakeDataDescriptor(101, {});
+    ASSERT_TRUE(
+        fact_manager.IsSynonymous(MakeDataDescriptor(8, {}), descriptor_101));
   }
 
   {
@@ -90,14 +94,16 @@
         101, MakeInstructionDescriptor(5, SpvOpReturn, 0), 102);
     ASSERT_TRUE(copy_false_again.IsApplicable(context.get(), fact_manager));
     copy_false_again.Apply(context.get(), &fact_manager);
-    std::set<uint32_t> ids_for_which_synonyms_are_known =
+    auto ids_for_which_synonyms_are_known =
         fact_manager.GetIdsForWhichSynonymsAreKnown();
     ASSERT_EQ(5, ids_for_which_synonyms_are_known.size());
-    ASSERT_TRUE(ids_for_which_synonyms_are_known.find(101) !=
-                ids_for_which_synonyms_are_known.end());
+    ASSERT_TRUE(std::find(ids_for_which_synonyms_are_known.begin(),
+                          ids_for_which_synonyms_are_known.end(),
+                          101) != ids_for_which_synonyms_are_known.end());
     ASSERT_EQ(3, fact_manager.GetSynonymsForId(101).size());
-    protobufs::DataDescriptor descriptor_102 = MakeDataDescriptor(102, {}, 1);
-    ASSERT_TRUE(fact_manager.GetSynonymsForId(101).count(&descriptor_102) > 0);
+    protobufs::DataDescriptor descriptor_102 = MakeDataDescriptor(102, {});
+    ASSERT_TRUE(
+        fact_manager.IsSynonymous(MakeDataDescriptor(101, {}), descriptor_102));
   }
 
   {
@@ -105,14 +111,16 @@
         7, MakeInstructionDescriptor(102, SpvOpReturn, 0), 103);
     ASSERT_TRUE(copy_true_again.IsApplicable(context.get(), fact_manager));
     copy_true_again.Apply(context.get(), &fact_manager);
-    std::set<uint32_t> ids_for_which_synonyms_are_known =
+    auto ids_for_which_synonyms_are_known =
         fact_manager.GetIdsForWhichSynonymsAreKnown();
     ASSERT_EQ(6, ids_for_which_synonyms_are_known.size());
-    ASSERT_TRUE(ids_for_which_synonyms_are_known.find(7) !=
-                ids_for_which_synonyms_are_known.end());
+    ASSERT_TRUE(std::find(ids_for_which_synonyms_are_known.begin(),
+                          ids_for_which_synonyms_are_known.end(),
+                          7) != ids_for_which_synonyms_are_known.end());
     ASSERT_EQ(3, fact_manager.GetSynonymsForId(7).size());
-    protobufs::DataDescriptor descriptor_103 = MakeDataDescriptor(103, {}, 1);
-    ASSERT_TRUE(fact_manager.GetSynonymsForId(7).count(&descriptor_103) > 0);
+    protobufs::DataDescriptor descriptor_103 = MakeDataDescriptor(103, {});
+    ASSERT_TRUE(
+        fact_manager.IsSynonymous(MakeDataDescriptor(7, {}), descriptor_103));
   }
 
   std::string after_transformation = R"(