spirv-fuzz: add transformation and pass to construct composites (#2941)

Adds a fuzzer pass and transformation to create a composite (array,
matrix, struct or vector) from available constituent components, and
inform the fact manager that each component of the new composite is
synonymous with the id that was used to construct it. This allows the
"replace id with synonym" pass to then replace uses of said ids with
uses of elements extracted from the composite.

Fixes #2858.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 5ec62cd..dbb4964 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -40,6 +40,7 @@
         fuzzer_pass_add_useful_constructs.h
         fuzzer_pass_adjust_selection_controls.h
         fuzzer_pass_apply_id_synonyms.h
+        fuzzer_pass_construct_composites.h
         fuzzer_pass_copy_objects.h
         fuzzer_pass_obfuscate_constants.h
         fuzzer_pass_permute_blocks.h
@@ -60,6 +61,7 @@
         transformation_add_type_float.h
         transformation_add_type_int.h
         transformation_add_type_pointer.h
+        transformation_construct_composite.h
         transformation_copy_object.h
         transformation_move_block_down.h
         transformation_replace_boolean_constant_with_constant_binary.h
@@ -81,6 +83,7 @@
         fuzzer_pass_add_useful_constructs.cpp
         fuzzer_pass_adjust_selection_controls.cpp
         fuzzer_pass_apply_id_synonyms.cpp
+        fuzzer_pass_construct_composites.cpp
         fuzzer_pass_copy_objects.cpp
         fuzzer_pass_obfuscate_constants.cpp
         fuzzer_pass_permute_blocks.cpp
@@ -100,6 +103,7 @@
         transformation_add_type_float.cpp
         transformation_add_type_int.cpp
         transformation_add_type_pointer.cpp
+        transformation_construct_composite.cpp
         transformation_copy_object.cpp
         transformation_move_block_down.cpp
         transformation_replace_boolean_constant_with_constant_binary.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index e28ecd7..fbeafb3 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -25,6 +25,7 @@
 #include "source/fuzz/fuzzer_pass_add_useful_constructs.h"
 #include "source/fuzz/fuzzer_pass_adjust_selection_controls.h"
 #include "source/fuzz/fuzzer_pass_apply_id_synonyms.h"
+#include "source/fuzz/fuzzer_pass_construct_composites.h"
 #include "source/fuzz/fuzzer_pass_copy_objects.h"
 #include "source/fuzz/fuzzer_pass_obfuscate_constants.h"
 #include "source/fuzz/fuzzer_pass_permute_blocks.h"
@@ -139,6 +140,9 @@
     MaybeAddPass<FuzzerPassApplyIdSynonyms>(&passes, ir_context.get(),
                                             &fact_manager, &fuzzer_context,
                                             transformation_sequence_out);
+    MaybeAddPass<FuzzerPassConstructComposites>(&passes, ir_context.get(),
+                                                &fact_manager, &fuzzer_context,
+                                                transformation_sequence_out);
     MaybeAddPass<FuzzerPassCopyObjects>(&passes, ir_context.get(),
                                         &fact_manager, &fuzzer_context,
                                         transformation_sequence_out);
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 5a8f394..2328621 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -28,6 +28,7 @@
 const std::pair<uint32_t, uint32_t> kChanceOfAdjustingSelectionControl = {20,
                                                                           90};
 const std::pair<uint32_t, uint32_t> kChanceOfCopyingObject = {20, 50};
+const std::pair<uint32_t, uint32_t> kChanceOfConstructingComposite = {20, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfMovingBlockDown = {20, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfObfuscatingConstant = {10, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingIdWithSynonym = {10, 90};
@@ -57,6 +58,8 @@
       ChooseBetweenMinAndMax(kChanceOfAddingDeadContinue);
   chance_of_adjusting_selection_control_ =
       ChooseBetweenMinAndMax(kChanceOfAdjustingSelectionControl);
+  chance_of_constructing_composite_ =
+      ChooseBetweenMinAndMax(kChanceOfConstructingComposite);
   chance_of_copying_object_ = ChooseBetweenMinAndMax(kChanceOfCopyingObject);
   chance_of_moving_block_down_ =
       ChooseBetweenMinAndMax(kChanceOfMovingBlockDown);
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index f50098e..eabe346 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -65,6 +65,9 @@
   uint32_t GetChanceOfAdjustingSelectionControl() {
     return chance_of_adjusting_selection_control_;
   }
+  uint32_t GetChanceOfConstructingComposite() {
+    return chance_of_constructing_composite_;
+  }
   uint32_t GetChanceOfCopyingObject() { return chance_of_copying_object_; }
   uint32_t GetChanceOfMovingBlockDown() { return chance_of_moving_block_down_; }
   uint32_t GetChanceOfObfuscatingConstant() {
@@ -92,6 +95,7 @@
   uint32_t chance_of_adding_dead_break_;
   uint32_t chance_of_adding_dead_continue_;
   uint32_t chance_of_adjusting_selection_control_;
+  uint32_t chance_of_constructing_composite_;
   uint32_t chance_of_copying_object_;
   uint32_t chance_of_moving_block_down_;
   uint32_t chance_of_obfuscating_constant_;
diff --git a/source/fuzz/fuzzer_pass.cpp b/source/fuzz/fuzzer_pass.cpp
index 823f2e0..cb26d79 100644
--- a/source/fuzz/fuzzer_pass.cpp
+++ b/source/fuzz/fuzzer_pass.cpp
@@ -27,5 +27,104 @@
 
 FuzzerPass::~FuzzerPass() = default;
 
+std::vector<opt::Instruction*> FuzzerPass::FindAvailableInstructions(
+    const opt::Function& function, opt::BasicBlock* block,
+    opt::BasicBlock::iterator inst_it,
+    std::function<bool(opt::IRContext*, opt::Instruction*)>
+        instruction_is_relevant) {
+  // TODO(afd) The following is (relatively) simple, but may end up being
+  //  prohibitively inefficient, as it walks the whole dominator tree for
+  //  every instruction that is considered.
+
+  std::vector<opt::Instruction*> result;
+  // Consider all global declarations
+  for (auto& global : GetIRContext()->module()->types_values()) {
+    if (instruction_is_relevant(GetIRContext(), &global)) {
+      result.push_back(&global);
+    }
+  }
+
+  // Consider all previous instructions in this block
+  for (auto prev_inst_it = block->begin(); prev_inst_it != inst_it;
+       ++prev_inst_it) {
+    if (instruction_is_relevant(GetIRContext(), &*prev_inst_it)) {
+      result.push_back(&*prev_inst_it);
+    }
+  }
+
+  // Walk the dominator tree to consider all instructions from dominating
+  // blocks
+  auto dominator_analysis = GetIRContext()->GetDominatorAnalysis(&function);
+  for (auto next_dominator = dominator_analysis->ImmediateDominator(block);
+       next_dominator != nullptr;
+       next_dominator =
+           dominator_analysis->ImmediateDominator(next_dominator)) {
+    for (auto& dominating_inst : *next_dominator) {
+      if (instruction_is_relevant(GetIRContext(), &dominating_inst)) {
+        result.push_back(&dominating_inst);
+      }
+    }
+  }
+  return result;
+}
+
+void FuzzerPass::MaybeAddTransformationBeforeEachInstruction(
+    std::function<uint32_t(
+        const opt::Function& function, opt::BasicBlock* block,
+        opt::BasicBlock::iterator inst_it, uint32_t base, uint32_t offset)>
+        maybe_apply_transformation) {
+  // Consider every block in every function.
+  for (auto& function : *GetIRContext()->module()) {
+    for (auto& block : function) {
+      // We now consider every instruction in the block, randomly deciding
+      // whether to apply a transformation before it.
+
+      // In order for transformations to insert new instructions, they need to
+      // be able to identify the instruction to insert before.  We enable this
+      // by tracking a base instruction, which must generate a result id, and
+      // an offset (to allow us to identify instructions that do not generate
+      // result ids).
+
+      // The initial base instruction is the block label.
+      uint32_t base = block.id();
+      uint32_t offset = 0;
+      // Consider every instruction in the block.
+      for (auto inst_it = block.begin(); inst_it != block.end(); ++inst_it) {
+        if (inst_it->HasResultId()) {
+          // In the case that the instruction has a result id, we use the
+          // instruction as its own base, with zero offset.
+          base = inst_it->result_id();
+          offset = 0;
+        } else {
+          // The instruction does not have a result id, so we need to identify
+          // it via the latest instruction that did have a result id (base), and
+          // an incremented offset.
+          offset++;
+        }
+
+        // Invoke the provided function, which might apply a transformation.
+        // Its return value informs us of how many instructions it inserted.
+        // (This will be 0 if no transformation was applied.)
+        uint32_t num_instructions_inserted =
+            maybe_apply_transformation(function, &block, inst_it, base, offset);
+
+        if (!inst_it->HasResultId()) {
+          // We are tracking the current id-less instruction via an offset,
+          // |offset|, from a previous instruction, |base|, that has an id. We
+          // increment |offset| to reflect any newly-inserted instructions.
+          //
+          // An alternative would be to reset |base| to be an id generated by
+          // a newly-inserted instruction, but that would be more complex, and
+          // sticking to a |base| that already existed before this
+          // transformation was applied makes the applicability of future
+          // transformations less tightly coupled with the presence of the just-
+          // applied transformation.
+          offset += num_instructions_inserted;
+        }
+      }
+    }
+  }
+}
+
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass.h b/source/fuzz/fuzzer_pass.h
index cd1b194..d56cc53 100644
--- a/source/fuzz/fuzzer_pass.h
+++ b/source/fuzz/fuzzer_pass.h
@@ -15,9 +15,13 @@
 #ifndef SOURCE_FUZZ_FUZZER_PASS_H_
 #define SOURCE_FUZZ_FUZZER_PASS_H_
 
+#include <functional>
+#include <vector>
+
 #include "source/fuzz/fact_manager.h"
 #include "source/fuzz/fuzzer_context.h"
 #include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/opt/ir_context.h"
 
 namespace spvtools {
 namespace fuzz {
@@ -48,6 +52,44 @@
     return transformations_;
   }
 
+  // Returns all instructions that are *available* at |inst_it|, which is
+  // required to be inside block |block| of function |function| - that is, all
+  // instructions at global scope and all instructions that strictly dominate
+  // |inst_it|.
+  //
+  // Filters said instructions to return only those that satisfy the
+  // |instruction_is_relevant| predicate.  This, for instance, could ignore all
+  // instructions that have a particular decoration.
+  std::vector<opt::Instruction*> FindAvailableInstructions(
+      const opt::Function& function, opt::BasicBlock* block,
+      opt::BasicBlock::iterator inst_it,
+      std::function<bool(opt::IRContext*, opt::Instruction*)>
+          instruction_is_relevant);
+
+  // A helper method that iterates through each instruction in each block, at
+  // all times tracking a base instruction and offset that allows that latest
+  // instruction to be located even if it has no result id.
+  //
+  // The code to manipulate the base and offset is a bit fiddly, and the point
+  // of this method is to avoiding having to duplicate it in multiple
+  // transformation passes.
+  //
+  // The function |maybe_apply_transformation| is invoked for each instruction
+  // |inst_it| in block |block| of function |function| that is encountered.  The
+  // |base| and |offset| parameters to the function object allow |inst_it| to be
+  // identified.
+  //
+  // The job of |maybe_apply_transformation| is to randomly decide whether to
+  // try to apply some transformation, and then - if selected - to attempt to
+  // apply it.  The function returns the number of instructions that were
+  // inserted before |inst_it|, so that |offset| can be updated.
+  //
+  void MaybeAddTransformationBeforeEachInstruction(
+      std::function<uint32_t(
+          const opt::Function& function, opt::BasicBlock* block,
+          opt::BasicBlock::iterator inst_it, uint32_t base, uint32_t offset)>
+          maybe_apply_transformation);
+
  private:
   opt::IRContext* ir_context_;
   FactManager* fact_manager_;
diff --git a/source/fuzz/fuzzer_pass_apply_id_synonyms.cpp b/source/fuzz/fuzzer_pass_apply_id_synonyms.cpp
index 64131f9..250bcc2 100644
--- a/source/fuzz/fuzzer_pass_apply_id_synonyms.cpp
+++ b/source/fuzz/fuzzer_pass_apply_id_synonyms.cpp
@@ -63,9 +63,6 @@
                 GetFuzzerContext()->RandomIndex(synonyms_to_try);
             auto synonym_to_try = synonyms_to_try[synonym_index];
             synonyms_to_try.erase(synonyms_to_try.begin() + synonym_index);
-            assert(synonym_to_try->index().empty() &&
-                   "Right now we only support id == id synonyms; supporting "
-                   "e.g. id == index-into-vector will come later");
 
             if (!TransformationReplaceIdWithSynonym::
                     ReplacingUseWithSynonymIsOk(GetIRContext(), use_inst,
@@ -74,10 +71,24 @@
               continue;
             }
 
+            // At present, we generate direct id synonyms (through
+            // OpCopyObject), which require no indices, and id synonyms that
+            // require a single index (through OpCompositeConstruct).
+            assert(synonym_to_try->index_size() <= 1);
+
+            // If an index is required, then we need to extract an element
+            // from a composite (e.g. through OpCompositeExtract), and this
+            // requires a fresh result id.
+            auto fresh_id_for_temporary =
+                synonym_to_try->index().empty()
+                    ? 0
+                    : GetFuzzerContext()->GetFreshId();
+
             TransformationReplaceIdWithSynonym replace_id_transformation(
                 transformation::MakeIdUseDescriptorFromUse(
                     GetIRContext(), use_inst, use_in_operand_index),
-                *synonym_to_try, 0);
+                *synonym_to_try, fresh_id_for_temporary);
+
             // The transformation should be applicable by construction.
             assert(replace_id_transformation.IsApplicable(GetIRContext(),
                                                           *GetFactManager()));
diff --git a/source/fuzz/fuzzer_pass_construct_composites.cpp b/source/fuzz/fuzzer_pass_construct_composites.cpp
new file mode 100644
index 0000000..e0d2184
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_construct_composites.cpp
@@ -0,0 +1,328 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/fuzzer_pass_construct_composites.h"
+
+#include <cmath>
+#include <memory>
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/transformation_construct_composite.h"
+#include "source/util/make_unique.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassConstructComposites::FuzzerPassConstructComposites(
+    opt::IRContext* ir_context, FactManager* fact_manager,
+    FuzzerContext* fuzzer_context,
+    protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, fact_manager, fuzzer_context, transformations) {}
+
+FuzzerPassConstructComposites::~FuzzerPassConstructComposites() = default;
+
+void FuzzerPassConstructComposites::Apply() {
+  // Gather up the ids of all composite types.
+  std::vector<uint32_t> composite_type_ids;
+  for (auto& inst : GetIRContext()->types_values()) {
+    if (fuzzerutil::IsCompositeType(
+            GetIRContext()->get_type_mgr()->GetType(inst.result_id()))) {
+      composite_type_ids.push_back(inst.result_id());
+    }
+  }
+
+  MaybeAddTransformationBeforeEachInstruction(
+      [this, &composite_type_ids](const opt::Function& function,
+                                  opt::BasicBlock* block,
+                                  opt::BasicBlock::iterator inst_it,
+                                  uint32_t base, uint32_t offset) -> uint32_t {
+        // Check whether it is legitimate to insert a composite construction
+        // before the instruction.
+        if (!fuzzerutil::CanInsertOpcodeBeforeInstruction(
+                SpvOpCompositeConstruct, inst_it)) {
+          return 0;
+        }
+
+        // Randomly decide whether to try inserting an object copy here.
+        if (!GetFuzzerContext()->ChoosePercentage(
+                GetFuzzerContext()->GetChanceOfConstructingComposite())) {
+          return 0;
+        }
+
+        // For each instruction that is available at this program point (i.e. an
+        // instruction that is global or whose definition strictly dominates the
+        // program point) and suitable for making a synonym of, associate it
+        // with the id of its result type.
+        TypeIdToInstructions type_id_to_available_instructions;
+        for (auto instruction : FindAvailableInstructions(
+                 function, block, inst_it, fuzzerutil::CanMakeSynonymOf)) {
+          RecordAvailableInstruction(instruction,
+                                     &type_id_to_available_instructions);
+        }
+
+        // At this point, |composite_type_ids| captures all the composite types
+        // we could try to create, while |type_id_to_available_instructions|
+        // captures all the available result ids we might use, organized by
+        // type.
+
+        // Now we try to find a composite that we can construct.  We might not
+        // manage, if there is a paucity of available ingredients in the module
+        // (e.g. if our only available composite was a boolean vector and we had
+        // no instructions generating boolean result types available).
+        //
+        // If we succeed, |chosen_composite_type| will end up being non-zero,
+        // and |constructor_arguments| will end up giving us result ids suitable
+        // for constructing a composite of that type.  Otherwise these variables
+        // will remain 0 and null respectively.
+        uint32_t chosen_composite_type = 0;
+        std::unique_ptr<std::vector<uint32_t>> constructor_arguments = nullptr;
+
+        // Initially, all composite type ids are available for us to try.  Keep
+        // trying until we run out of options.
+        auto composites_to_try_constructing = composite_type_ids;
+        while (!composites_to_try_constructing.empty()) {
+          // Remove a composite type from the composite types left for us to
+          // try.
+          auto index =
+              GetFuzzerContext()->RandomIndex(composites_to_try_constructing);
+          auto next_composite_to_try_constructing =
+              composites_to_try_constructing[index];
+          composites_to_try_constructing.erase(
+              composites_to_try_constructing.begin() + index);
+
+          // Now try to construct a composite of this type, using an appropriate
+          // helper method depending on the kind of composite type.
+          auto composite_type = GetIRContext()->get_type_mgr()->GetType(
+              next_composite_to_try_constructing);
+          if (auto array_type = composite_type->AsArray()) {
+            constructor_arguments = TryConstructingArrayComposite(
+                *array_type, type_id_to_available_instructions);
+          } else if (auto matrix_type = composite_type->AsMatrix()) {
+            constructor_arguments = TryConstructingMatrixComposite(
+                *matrix_type, type_id_to_available_instructions);
+          } else if (auto struct_type = composite_type->AsStruct()) {
+            constructor_arguments = TryConstructingStructComposite(
+                *struct_type, type_id_to_available_instructions);
+          } else {
+            auto vector_type = composite_type->AsVector();
+            assert(vector_type &&
+                   "The space of possible composite types should be covered by "
+                   "the above cases.");
+            constructor_arguments = TryConstructingVectorComposite(
+                *vector_type, type_id_to_available_instructions);
+          }
+          if (constructor_arguments != nullptr) {
+            // We succeeded!  Note the composite type we finally settled on, and
+            // exit from the loop.
+            chosen_composite_type = next_composite_to_try_constructing;
+            break;
+          }
+        }
+
+        if (!chosen_composite_type) {
+          // We did not manage to make a composite; return 0 to indicate that no
+          // instructions were added.
+          assert(constructor_arguments == nullptr);
+          return 0;
+        }
+        assert(constructor_arguments != nullptr);
+
+        // Make and apply a transformation.
+        TransformationConstructComposite transformation(
+            chosen_composite_type, *constructor_arguments, base, offset,
+            GetFuzzerContext()->GetFreshId());
+        assert(transformation.IsApplicable(GetIRContext(), *GetFactManager()) &&
+               "This transformation should be applicable by construction.");
+        transformation.Apply(GetIRContext(), GetFactManager());
+        *GetTransformations()->add_transformation() =
+            transformation.ToMessage();
+        // Indicate that one instruction was added.
+        return 1;
+      });
+}
+
+void FuzzerPassConstructComposites::RecordAvailableInstruction(
+    opt::Instruction* inst,
+    TypeIdToInstructions* type_id_to_available_instructions) {
+  if (type_id_to_available_instructions->count(inst->type_id()) == 0) {
+    (*type_id_to_available_instructions)[inst->type_id()] = {};
+  }
+  type_id_to_available_instructions->at(inst->type_id()).push_back(inst);
+}
+
+std::unique_ptr<std::vector<uint32_t>>
+FuzzerPassConstructComposites::TryConstructingArrayComposite(
+    const opt::analysis::Array& array_type,
+    const TypeIdToInstructions& type_id_to_available_instructions) {
+  // TODO make these be true by construction
+  assert(array_type.length_info().words.size() == 2);
+  assert(array_type.length_info().words[0] ==
+         opt::analysis::Array::LengthInfo::kConstant);
+
+  auto result = MakeUnique<std::vector<uint32_t>>();
+  auto element_type_id =
+      GetIRContext()->get_type_mgr()->GetId(array_type.element_type());
+  auto available_instructions =
+      type_id_to_available_instructions.find(element_type_id);
+  if (available_instructions == type_id_to_available_instructions.cend()) {
+    // TODO comment infeasible
+    return nullptr;
+  }
+  for (uint32_t index = 0; index < array_type.length_info().words[1]; index++) {
+    result->push_back(available_instructions
+                          ->second[GetFuzzerContext()->RandomIndex(
+                              available_instructions->second)]
+                          ->result_id());
+  }
+  return result;
+}
+
+std::unique_ptr<std::vector<uint32_t>>
+FuzzerPassConstructComposites::TryConstructingMatrixComposite(
+    const opt::analysis::Matrix& matrix_type,
+    const TypeIdToInstructions& type_id_to_available_instructions) {
+  (void)(matrix_type);
+  (void)(type_id_to_available_instructions);
+  assert(false);
+  return nullptr;
+}
+
+std::unique_ptr<std::vector<uint32_t>>
+FuzzerPassConstructComposites::TryConstructingStructComposite(
+    const opt::analysis::Struct& struct_type,
+    const TypeIdToInstructions& type_id_to_available_instructions) {
+  auto result = MakeUnique<std::vector<uint32_t>>();
+  for (auto element_type : struct_type.element_types()) {
+    auto element_type_id = GetIRContext()->get_type_mgr()->GetId(element_type);
+    auto available_instructions =
+        type_id_to_available_instructions.find(element_type_id);
+    if (available_instructions == type_id_to_available_instructions.cend()) {
+      // TODO comment infeasible
+      return nullptr;
+    }
+    result->push_back(available_instructions
+                          ->second[GetFuzzerContext()->RandomIndex(
+                              available_instructions->second)]
+                          ->result_id());
+  }
+  return result;
+}
+
+std::unique_ptr<std::vector<uint32_t>>
+FuzzerPassConstructComposites::TryConstructingVectorComposite(
+    const opt::analysis::Vector& vector_type,
+    const TypeIdToInstructions& type_id_to_available_instructions) {
+  // Get details of the type underlying the vector, and the width of the vector,
+  // for convenience.
+  auto element_type = vector_type.element_type();
+  auto element_count = vector_type.element_count();
+
+  // Collect a mapping, from type id to width, for scalar/vector types that are
+  // smaller in width than |vector_type|, but that have the same underlying
+  // type.  For example, if |vector_type| is vec4, the mapping will be { float
+  // -> 1, vec2 -> 2, vec3 -> 3 }.  The mapping will have missing entries if
+  // some of these types do not exist.
+
+  // TODO comment why we have the list as well.
+  std::vector<uint32_t> smaller_vector_type_ids;
+  std::map<uint32_t, uint32_t> smaller_vector_type_id_to_width;
+  // Add the underlying type.  This id must exist, in order for |vector_type| to
+  // exist.
+  auto scalar_type_id = GetIRContext()->get_type_mgr()->GetId(element_type);
+  smaller_vector_type_ids.push_back(scalar_type_id);
+  smaller_vector_type_id_to_width[scalar_type_id] = 1;
+
+  // Now add every vector type with width at least 2, and less than the width of
+  // |vector_type|.
+  for (uint32_t width = 2; width < element_count; width++) {
+    opt::analysis::Vector smaller_vector_type(vector_type.element_type(),
+                                              width);
+    auto smaller_vector_type_id =
+        GetIRContext()->get_type_mgr()->GetId(&smaller_vector_type);
+    // TODO recap why it might be 0
+    if (smaller_vector_type_id) {
+      smaller_vector_type_ids.push_back(smaller_vector_type_id);
+      smaller_vector_type_id_to_width[smaller_vector_type_id] = width;
+    }
+  }
+
+  // Now we know the types that are available to us, we set about populating a
+  // vector of the right length.  We do this by deciding, with no order in mind,
+  // which instructions we will use to populate the vector, and subsequently
+  // randomly choosing an order.  This is to avoid biasing construction of
+  // vectors with smaller vectors to the left and scalars to the right.  That is
+  // a concern because, e.g. in the case of populating a vec4, if we populate
+  // the constructor instructions left-to-right, we can always choose a vec3 to
+  // construct the first three elements, but can only choose a vec3 to construct
+  // the last three elements if we chose a float to construct the first element
+  // (otherwise there will not be space left for a vec3).
+
+  uint32_t vector_slots_used = 0;
+  // The instructions we will use to construct the vector, in no particular
+  // order at this stage.
+  std::vector<opt::Instruction*> instructions_to_use;
+
+  while (vector_slots_used < vector_type.element_count()) {
+    std::vector<opt::Instruction*> instructions_to_choose_from;
+    for (auto& entry : smaller_vector_type_id_to_width) {
+      if (entry.second >
+          std::min(vector_type.element_count() - 1,
+                   vector_type.element_count() - vector_slots_used)) {
+        continue;
+      }
+      auto available_instructions =
+          type_id_to_available_instructions.find(entry.first);
+      if (available_instructions == type_id_to_available_instructions.cend()) {
+        continue;
+      }
+      instructions_to_choose_from.insert(instructions_to_choose_from.end(),
+                                         available_instructions->second.begin(),
+                                         available_instructions->second.end());
+    }
+    if (instructions_to_choose_from.empty()) {
+      // TODO comment - like fuzzed into a corner
+      return nullptr;
+    }
+    auto instruction_to_use =
+        instructions_to_choose_from[GetFuzzerContext()->RandomIndex(
+            instructions_to_choose_from)];
+    instructions_to_use.push_back(instruction_to_use);
+    auto chosen_type =
+        GetIRContext()->get_type_mgr()->GetType(instruction_to_use->type_id());
+    if (chosen_type->AsVector()) {
+      assert(chosen_type->AsVector()->element_type() == element_type);
+      assert(chosen_type->AsVector()->element_count() < element_count);
+      assert(chosen_type->AsVector()->element_count() <=
+             element_count - vector_slots_used);
+      vector_slots_used += chosen_type->AsVector()->element_count();
+    } else {
+      assert(chosen_type == element_type);
+      vector_slots_used += 1;
+    }
+  }
+  assert(vector_slots_used == vector_type.element_count());
+
+  auto result = MakeUnique<std::vector<uint32_t>>();
+  std::vector<uint32_t> operands;
+  while (!instructions_to_use.empty()) {
+    auto index = GetFuzzerContext()->RandomIndex(instructions_to_use);
+    result->push_back(instructions_to_use[index]->result_id());
+    instructions_to_use.erase(instructions_to_use.begin() + index);
+  }
+  assert(result->size() > 1);
+  return result;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_construct_composites.h b/source/fuzz/fuzzer_pass_construct_composites.h
new file mode 100644
index 0000000..99ef31f
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_construct_composites.h
@@ -0,0 +1,79 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_FUZZER_PASS_CONSTRUCT_COMPOSITES_H_
+#define SOURCE_FUZZ_FUZZER_PASS_CONSTRUCT_COMPOSITES_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+#include <map>
+#include <vector>
+
+namespace spvtools {
+namespace fuzz {
+
+// A fuzzer pass for constructing composite objects from smaller objects.
+class FuzzerPassConstructComposites : public FuzzerPass {
+ public:
+  FuzzerPassConstructComposites(
+      opt::IRContext* ir_context, FactManager* fact_manager,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassConstructComposites();
+
+  void Apply() override;
+
+ private:
+  // Used to map a type id to relevant instructions whose result type matches
+  // the type id.
+  typedef std::map<uint32_t, std::vector<opt::Instruction*>>
+      TypeIdToInstructions;
+
+  // Considers all instructions that are available at |inst| - instructions
+  // whose results could be packed into a composite - and updates
+  // |type_id_to_available_instructions| so that each such instruction is
+  // associated with its the id of its result type.
+  void RecordAvailableInstruction(
+      opt::Instruction* inst,
+      TypeIdToInstructions* type_id_to_available_instructions);
+
+  // Attempts to find suitable instruction result ids from the values of
+  // |type_id_to_available_instructions| that would allow a composite of type
+  // |array_type| to be constructed.  Returns said ids if they can be found.
+  // Returns |nullptr| otherwise.
+  std::unique_ptr<std::vector<uint32_t>> TryConstructingArrayComposite(
+      const opt::analysis::Array& array_type,
+      const TypeIdToInstructions& type_id_to_available_instructions);
+
+  // Similar to TryConstructingArrayComposite, but for matrices.
+  std::unique_ptr<std::vector<uint32_t>> TryConstructingMatrixComposite(
+      const opt::analysis::Matrix& matrix_type,
+      const TypeIdToInstructions& type_id_to_available_instructions);
+
+  // Similar to TryConstructingArrayComposite, but for structs.
+  std::unique_ptr<std::vector<uint32_t>> TryConstructingStructComposite(
+      const opt::analysis::Struct& struct_type,
+      const TypeIdToInstructions& type_id_to_available_instructions);
+
+  // Similar to TryConstructingArrayComposite, but for vectors.
+  std::unique_ptr<std::vector<uint32_t>> TryConstructingVectorComposite(
+      const opt::analysis::Vector& vector_type,
+      const TypeIdToInstructions& type_id_to_available_instructions);
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_CONSTRUCT_COMPOSITES_H_
diff --git a/source/fuzz/fuzzer_pass_copy_objects.cpp b/source/fuzz/fuzzer_pass_copy_objects.cpp
index a1b118a..8c85a3b 100644
--- a/source/fuzz/fuzzer_pass_copy_objects.cpp
+++ b/source/fuzz/fuzzer_pass_copy_objects.cpp
@@ -14,6 +14,7 @@
 
 #include "source/fuzz/fuzzer_pass_copy_objects.h"
 
+#include "source/fuzz/fuzzer_util.h"
 #include "source/fuzz/transformation_copy_object.h"
 
 namespace spvtools {
@@ -28,120 +29,46 @@
 FuzzerPassCopyObjects::~FuzzerPassCopyObjects() = default;
 
 void FuzzerPassCopyObjects::Apply() {
-  // Consider every block in every function.
-  for (auto& function : *GetIRContext()->module()) {
-    for (auto& block : function) {
-      // We now consider every instruction in the block, randomly deciding
-      // whether to add an object copy before the instruction.
-
-      // In order to insert an object copy instruction, we need to be able to
-      // identify the instruction a copy should be inserted before.  We do this
-      // by tracking a base instruction, which must generate a result id, and an
-      // offset (to allow us to identify instructions that do not generate
-      // result ids).
-
-      // The initial base instruction is the block label.
-      uint32_t base = block.id();
-      uint32_t offset = 0;
-      // Consider every instruction in the block.
-      for (auto inst_it = block.begin(); inst_it != block.end(); ++inst_it) {
-        if (inst_it->HasResultId()) {
-          // In the case that the instruction has a result id, we use the
-          // instruction as its own base, with zero offset.
-          base = inst_it->result_id();
-          offset = 0;
-        } else {
-          // The instruction does not have a result id, so we need to identify
-          // it via the latest instruction that did have a result id (base), and
-          // an incremented offset.
-          offset++;
-        }
-
+  MaybeAddTransformationBeforeEachInstruction(
+      [this](const opt::Function& function, opt::BasicBlock* block,
+             opt::BasicBlock::iterator inst_it, uint32_t base,
+             uint32_t offset) -> uint32_t {
         // Check whether it is legitimate to insert a copy before this
         // instruction.
-        if (!TransformationCopyObject::CanInsertCopyBefore(inst_it)) {
-          continue;
+        if (!fuzzerutil::CanInsertOpcodeBeforeInstruction(SpvOpCopyObject,
+                                                          inst_it)) {
+          return 0;
         }
 
         // Randomly decide whether to try inserting an object copy here.
         if (!GetFuzzerContext()->ChoosePercentage(
                 GetFuzzerContext()->GetChanceOfCopyingObject())) {
-          continue;
+          return 0;
         }
 
-        // Populate list of potential instructions that can be copied.
-        // TODO(afd) The following is (relatively) simple, but may end up being
-        //  prohibitively inefficient, as it walks the whole dominator tree for
-        //  every copy that is added.
-        std::vector<opt::Instruction*> copyable_instructions;
+        std::vector<opt::Instruction*> relevant_instructions =
+            FindAvailableInstructions(function, block, inst_it,
+                                      fuzzerutil::CanMakeSynonymOf);
 
-        // Consider all global declarations
-        for (auto& global : GetIRContext()->module()->types_values()) {
-          if (TransformationCopyObject::IsCopyable(GetIRContext(), &global)) {
-            copyable_instructions.push_back(&global);
-          }
-        }
-
-        // Consider all previous instructions in this block
-        for (auto prev_inst_it = block.begin(); prev_inst_it != inst_it;
-             ++prev_inst_it) {
-          if (TransformationCopyObject::IsCopyable(GetIRContext(),
-                                                   &*prev_inst_it)) {
-            copyable_instructions.push_back(&*prev_inst_it);
-          }
-        }
-
-        // Walk the dominator tree to consider all instructions from dominating
-        // blocks
-        auto dominator_analysis =
-            GetIRContext()->GetDominatorAnalysis(&function);
-        for (auto next_dominator =
-                 dominator_analysis->ImmediateDominator(&block);
-             next_dominator != nullptr;
-             next_dominator =
-                 dominator_analysis->ImmediateDominator(next_dominator)) {
-          for (auto& dominating_inst : *next_dominator) {
-            if (TransformationCopyObject::IsCopyable(GetIRContext(),
-                                                     &dominating_inst)) {
-              copyable_instructions.push_back(&dominating_inst);
-            }
-          }
-        }
-
-        // At this point, |copyable_instructions| contains all the instructions
+        // At this point, |relevant_instructions| contains all the instructions
         // we might think of copying.
-
-        if (!copyable_instructions.empty()) {
-          // Choose a copyable instruction at random, and create and apply an
-          // object copying transformation based on it.
-          uint32_t index =
-              GetFuzzerContext()->RandomIndex(copyable_instructions);
-          TransformationCopyObject transformation(
-              copyable_instructions[index]->result_id(), base, offset,
-              GetFuzzerContext()->GetFreshId());
-          assert(
-              transformation.IsApplicable(GetIRContext(), *GetFactManager()) &&
-              "This transformation should be applicable by construction.");
-          transformation.Apply(GetIRContext(), GetFactManager());
-          *GetTransformations()->add_transformation() =
-              transformation.ToMessage();
-
-          if (!inst_it->HasResultId()) {
-            // We have inserted a new instruction before the current
-            // instruction, and we are tracking the current id-less instruction
-            // via an offset (offset) from a previous instruction (base) that
-            // has an id. We increment |offset| to reflect the newly-inserted
-            // instruction.
-            //
-            // This is slightly preferable to the alternative of setting |base|
-            // to be the result id of the new instruction, since on replay we
-            // might end up eliminating this copy but keeping a subsequent copy.
-            offset++;
-          }
+        if (relevant_instructions.empty()) {
+          return 0;
         }
-      }
-    }
-  }
+
+        // Choose a copyable instruction at random, and create and apply an
+        // object copying transformation based on it.
+        uint32_t index = GetFuzzerContext()->RandomIndex(relevant_instructions);
+        TransformationCopyObject transformation(
+            relevant_instructions[index]->result_id(), base, offset,
+            GetFuzzerContext()->GetFreshId());
+        assert(transformation.IsApplicable(GetIRContext(), *GetFactManager()) &&
+               "This transformation should be applicable by construction.");
+        transformation.Apply(GetIRContext(), GetFactManager());
+        *GetTransformations()->add_transformation() =
+            transformation.ToMessage();
+        return 1;
+      });
 }
 
 }  // namespace fuzz
diff --git a/source/fuzz/fuzzer_util.cpp b/source/fuzz/fuzzer_util.cpp
index 12405fc..e61ccae 100644
--- a/source/fuzz/fuzzer_util.cpp
+++ b/source/fuzz/fuzzer_util.cpp
@@ -314,6 +314,47 @@
       ->Dominates(enclosing_function->entry().get(), bb);
 }
 
+bool CanInsertOpcodeBeforeInstruction(
+    SpvOp opcode, const opt::BasicBlock::iterator& instruction_in_block) {
+  if (instruction_in_block->PreviousNode() &&
+      (instruction_in_block->PreviousNode()->opcode() == SpvOpLoopMerge ||
+       instruction_in_block->PreviousNode()->opcode() == SpvOpSelectionMerge)) {
+    // We cannot insert directly after a merge instruction.
+    return false;
+  }
+  if (opcode != SpvOpVariable &&
+      instruction_in_block->opcode() == SpvOpVariable) {
+    // We cannot insert a non-OpVariable instruction directly before a
+    // variable; variables in a function must be contiguous in the entry block.
+    return false;
+  }
+  // We cannot insert a non-OpPhi instruction directly before an OpPhi, because
+  // OpPhi instructions need to be contiguous at the start of a block.
+  return opcode == SpvOpPhi || instruction_in_block->opcode() != SpvOpPhi;
+}
+
+bool CanMakeSynonymOf(opt::IRContext* ir_context, opt::Instruction* inst) {
+  if (!inst->HasResultId()) {
+    // We can only make a synonym of an instruction that generates an id.
+    return false;
+  }
+  if (!inst->type_id()) {
+    // We can only make a synonym of an instruction that has a type.
+    return false;
+  }
+  // We do not make synonyms of objects that have decorations: if the synonym is
+  // not decorated analogously, using the original object vs. its synonymous
+  // form may not be equivalent.
+  return ir_context->get_decoration_mgr()
+      ->GetDecorationsFor(inst->result_id(), true)
+      .empty();
+}
+
+bool IsCompositeType(const opt::analysis::Type* type) {
+  return type && (type->AsArray() || type->AsMatrix() || type->AsStruct() ||
+                  type->AsVector());
+}
+
 }  // namespace fuzzerutil
 
 }  // namespace fuzz
diff --git a/source/fuzz/fuzzer_util.h b/source/fuzz/fuzzer_util.h
index 7aebc28..890ceac 100644
--- a/source/fuzz/fuzzer_util.h
+++ b/source/fuzz/fuzzer_util.h
@@ -87,6 +87,18 @@
 bool BlockIsReachableInItsFunction(opt::IRContext* context,
                                    opt::BasicBlock* bb);
 
+// Determines whether it is OK to insert an instruction with opcode |opcode|
+// before |instruction_in_block|.
+bool CanInsertOpcodeBeforeInstruction(
+    SpvOp opcode, const opt::BasicBlock::iterator& instruction_in_block);
+
+// Determines whether it is OK to make a synonym of |inst|.
+bool CanMakeSynonymOf(opt::IRContext* ir_context, opt::Instruction* inst);
+
+// Determines whether the given type is a composite; that is: an array, matrix,
+// struct or vector.
+bool IsCompositeType(const opt::analysis::Type* type);
+
 }  // namespace fuzzerutil
 
 }  // namespace fuzz
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index d1c6e73..0989502 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -174,6 +174,7 @@
     TransformationCopyObject copy_object = 13;
     TransformationReplaceIdWithSynonym replace_id_with_synonym = 14;
     TransformationSetSelectionControl set_selection_control = 15;
+    TransformationConstructComposite construct_composite = 16;
     // Add additional option using the next available number.
   }
 }
@@ -318,6 +319,30 @@
 
 }
 
+message TransformationConstructComposite {
+
+  // A transformation that introduces an OpCompositeConstruct instruction to
+  // make a composite object.
+
+  // Id of the type of the composite that is to be constructed
+  uint32 composite_type_id = 1;
+
+  // Ids of the objects that will form the components of the composite
+  repeated uint32 component = 2;
+
+  // The id of an instruction in a block
+  uint32 base_instruction_id = 3;
+
+  // An offset, such that OpCompositeConstruct instruction should be inserted
+  // right before the instruction |offset| instructions after
+  // |base_instruction_id|
+  uint32 offset = 4;
+
+  // A fresh id for the composite object
+  uint32 fresh_id = 5;
+
+}
+
 message TransformationMoveBlockDown {
 
   // A transformation that moves a basic block to be one position lower in
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index 296f71f..84d273e 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -25,6 +25,7 @@
 #include "transformation_add_type_float.h"
 #include "transformation_add_type_int.h"
 #include "transformation_add_type_pointer.h"
+#include "transformation_construct_composite.h"
 #include "transformation_copy_object.h"
 #include "transformation_move_block_down.h"
 #include "transformation_replace_boolean_constant_with_constant_binary.h"
@@ -62,6 +63,9 @@
     case protobufs::Transformation::TransformationCase::kAddTypePointer:
       return MakeUnique<TransformationAddTypePointer>(
           message.add_type_pointer());
+    case protobufs::Transformation::TransformationCase::kConstructComposite:
+      return MakeUnique<TransformationConstructComposite>(
+          message.construct_composite());
     case protobufs::Transformation::TransformationCase::kCopyObject:
       return MakeUnique<TransformationCopyObject>(message.copy_object());
     case protobufs::Transformation::TransformationCase::kMoveBlockDown:
diff --git a/source/fuzz/transformation_construct_composite.cpp b/source/fuzz/transformation_construct_composite.cpp
new file mode 100644
index 0000000..3c6c956
--- /dev/null
+++ b/source/fuzz/transformation_construct_composite.cpp
@@ -0,0 +1,310 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/transformation_construct_composite.h"
+
+#include "source/fuzz/fuzzer_util.h"
+
+namespace spvtools {
+namespace fuzz {
+
+TransformationConstructComposite::TransformationConstructComposite(
+    const protobufs::TransformationConstructComposite& message)
+    : message_(message) {}
+
+TransformationConstructComposite::TransformationConstructComposite(
+    uint32_t composite_type_id, std::vector<uint32_t> component,
+    uint32_t base_instruction_id, uint32_t offset, uint32_t fresh_id) {
+  message_.set_composite_type_id(composite_type_id);
+  for (auto a_component : component) {
+    message_.add_component(a_component);
+  }
+  message_.set_base_instruction_id(base_instruction_id);
+  message_.set_offset(offset);
+  message_.set_fresh_id(fresh_id);
+}
+
+bool TransformationConstructComposite::IsApplicable(
+    opt::IRContext* context, const FactManager& /*fact_manager*/) const {
+  if (!fuzzerutil::IsFreshId(context, message_.fresh_id())) {
+    // We require the id for the composite constructor to be unused.
+    return false;
+  }
+
+  auto base_instruction =
+      context->get_def_use_mgr()->GetDef(message_.base_instruction_id());
+  if (!base_instruction) {
+    // The given id to insert after is not defined.
+    return false;
+  }
+
+  auto destination_block = context->get_instr_block(base_instruction);
+  if (!destination_block) {
+    // The given id to insert after is not in a block.
+    return false;
+  }
+
+  auto insert_before = fuzzerutil::GetIteratorForBaseInstructionAndOffset(
+      destination_block, base_instruction, message_.offset());
+
+  if (insert_before == destination_block->end()) {
+    // The offset was inappropriate.
+    return false;
+  }
+
+  auto composite_type =
+      context->get_type_mgr()->GetType(message_.composite_type_id());
+
+  if (!fuzzerutil::IsCompositeType(composite_type)) {
+    // The type must actually be a composite.
+    return false;
+  }
+
+  // If the type is an array, matrix, struct or vector, the components need to
+  // be suitable for constructing something of that type.
+  if (composite_type->AsArray() && !ComponentsForArrayConstructionAreOK(
+                                       context, *composite_type->AsArray())) {
+    return false;
+  }
+  if (composite_type->AsMatrix() && !ComponentsForMatrixConstructionAreOK(
+                                        context, *composite_type->AsMatrix())) {
+    return false;
+  }
+  if (composite_type->AsStruct() && !ComponentsForStructConstructionAreOK(
+                                        context, *composite_type->AsStruct())) {
+    return false;
+  }
+  if (composite_type->AsVector() && !ComponentsForVectorConstructionAreOK(
+                                        context, *composite_type->AsVector())) {
+    return false;
+  }
+
+  // Now check whether every component being used to initialize the composite is
+  // available at the desired program point.
+  for (auto& component : message_.component()) {
+    auto component_inst = context->get_def_use_mgr()->GetDef(component);
+    if (!context->get_instr_block(component)) {
+      // The component does not have a block; that means it is in global scope,
+      // which is OK. (Whether the component actually corresponds to an
+      // instruction is checked above when determining whether types are
+      // suitable.)
+      continue;
+    }
+    // Check whether the component is available.
+    if (insert_before->HasResultId() &&
+        insert_before->result_id() == component) {
+      // This constitutes trying to use an id right before it is defined.  The
+      // special case is needed due to an instruction always dominating itself.
+      return false;
+    }
+    if (!context
+             ->GetDominatorAnalysis(
+                 context->get_instr_block(&*insert_before)->GetParent())
+             ->Dominates(component_inst, &*insert_before)) {
+      // The instruction defining the component must dominate the instruction we
+      // wish to insert the composite before.
+      return false;
+    }
+  }
+
+  return true;
+}
+
+void TransformationConstructComposite::Apply(opt::IRContext* context,
+                                             FactManager* fact_manager) const {
+  // Use the base and offset information from the transformation to determine
+  // where in the module a new instruction should be inserted.
+  auto base_instruction =
+      context->get_def_use_mgr()->GetDef(message_.base_instruction_id());
+  auto destination_block = context->get_instr_block(base_instruction);
+  auto insert_before = fuzzerutil::GetIteratorForBaseInstructionAndOffset(
+      destination_block, base_instruction, message_.offset());
+
+  // Prepare the input operands for an OpCompositeConstruct instruction.
+  opt::Instruction::OperandList in_operands;
+  for (auto& component_id : message_.component()) {
+    in_operands.push_back({SPV_OPERAND_TYPE_ID, {component_id}});
+  }
+
+  // Insert an OpCompositeConstruct instruction.
+  insert_before.InsertBefore(MakeUnique<opt::Instruction>(
+      context, SpvOpCompositeConstruct, message_.composite_type_id(),
+      message_.fresh_id(), in_operands));
+
+  // 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.
+  auto composite_type =
+      context->get_type_mgr()->GetType(message_.composite_type_id());
+  uint32_t index = 0;
+  for (auto component : message_.component()) {
+    protobufs::Fact fact;
+    fact.mutable_id_synonym_fact()->set_id(component);
+    fact.mutable_id_synonym_fact()->mutable_data_descriptor()->set_object(
+        message_.fresh_id());
+    fact.mutable_id_synonym_fact()->mutable_data_descriptor()->add_index(index);
+    fact_manager->AddFact(fact, context);
+    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());
+        index += component_type->AsVector()->element_count();
+      } else {
+        assert(component_type == composite_type->AsVector()->element_type());
+        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.
+      index++;
+    }
+  }
+
+  fuzzerutil::UpdateModuleIdBound(context, message_.fresh_id());
+  context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+}
+
+bool TransformationConstructComposite::ComponentsForArrayConstructionAreOK(
+    opt::IRContext* context, const opt::analysis::Array& array_type) const {
+  if (array_type.length_info().words[0] !=
+      opt::analysis::Array::LengthInfo::kConstant) {
+    // We only handle constant-sized arrays.
+    return false;
+  }
+  if (array_type.length_info().words.size() != 2) {
+    // We only handle the case where the array size can be captured in a single
+    // word.
+    return false;
+  }
+  // Get the array size.
+  auto array_size = array_type.length_info().words[1];
+  if (static_cast<uint32_t>(message_.component().size()) != array_size) {
+    // The number of components must match the array size.
+    return false;
+  }
+  // Check that each component is the result id of an instruction whose type is
+  // the array's element type.
+  for (auto component_id : message_.component()) {
+    auto inst = context->get_def_use_mgr()->GetDef(component_id);
+    if (inst == nullptr || !inst->type_id()) {
+      // The component does not correspond to an instruction with a result
+      // type.
+      return false;
+    }
+    auto component_type = context->get_type_mgr()->GetType(inst->type_id());
+    assert(component_type);
+    if (component_type != array_type.element_type()) {
+      // The component's type does not match the array's element type.
+      return false;
+    }
+  }
+  return true;
+}
+
+bool TransformationConstructComposite::ComponentsForMatrixConstructionAreOK(
+    opt::IRContext* context, const opt::analysis::Matrix& matrix_type) const {
+  if (static_cast<uint32_t>(message_.component().size()) !=
+      matrix_type.element_count()) {
+    // The number of components must match the number of columns of the matrix.
+    return false;
+  }
+  // Check that each component is the result id of an instruction whose type is
+  // the matrix's column type.
+  for (auto component_id : message_.component()) {
+    auto inst = context->get_def_use_mgr()->GetDef(component_id);
+    if (inst == nullptr || !inst->type_id()) {
+      // The component does not correspond to an instruction with a result
+      // type.
+      return false;
+    }
+    auto component_type = context->get_type_mgr()->GetType(inst->type_id());
+    assert(component_type);
+    if (component_type != matrix_type.element_type()) {
+      // The component's type does not match the matrix's column type.
+      return false;
+    }
+  }
+  return true;
+}
+
+bool TransformationConstructComposite::ComponentsForStructConstructionAreOK(
+    opt::IRContext* context, const opt::analysis::Struct& struct_type) const {
+  if (static_cast<uint32_t>(message_.component().size()) !=
+      struct_type.element_types().size()) {
+    // The number of components must match the number of fields of the struct.
+    return false;
+  }
+  // Check that each component is the result id of an instruction those type
+  // matches the associated field type.
+  for (uint32_t field_index = 0;
+       field_index < struct_type.element_types().size(); field_index++) {
+    auto inst =
+        context->get_def_use_mgr()->GetDef(message_.component()[field_index]);
+    if (inst == nullptr || !inst->type_id()) {
+      // The component does not correspond to an instruction with a result
+      // type.
+      return false;
+    }
+    auto component_type = context->get_type_mgr()->GetType(inst->type_id());
+    assert(component_type);
+    if (component_type != struct_type.element_types()[field_index]) {
+      // The component's type does not match the corresponding field type.
+      return false;
+    }
+  }
+  return true;
+}
+
+bool TransformationConstructComposite::ComponentsForVectorConstructionAreOK(
+    opt::IRContext* context, const opt::analysis::Vector& vector_type) const {
+  uint32_t base_element_count = 0;
+  auto element_type = vector_type.element_type();
+  for (auto& component_id : message_.component()) {
+    auto inst = context->get_def_use_mgr()->GetDef(component_id);
+    if (inst == nullptr || !inst->type_id()) {
+      // The component does not correspond to an instruction with a result
+      // type.
+      return false;
+    }
+    auto component_type = context->get_type_mgr()->GetType(inst->type_id());
+    assert(component_type);
+    if (component_type == element_type) {
+      base_element_count++;
+    } else if (component_type->AsVector() &&
+               component_type->AsVector()->element_type() == element_type) {
+      base_element_count += component_type->AsVector()->element_count();
+    } else {
+      // The component was not appropriate; e.g. no type corresponding to the
+      // given id was found, or the type that was found was not compatible
+      // with the vector being constructed.
+      return false;
+    }
+  }
+  // The number of components provided (when vector components are flattened
+  // out) needs to match the length of the vector being constructed.
+  return base_element_count == vector_type.element_count();
+}
+
+protobufs::Transformation TransformationConstructComposite::ToMessage() const {
+  protobufs::Transformation result;
+  *result.mutable_construct_composite() = message_;
+  return result;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_construct_composite.h b/source/fuzz/transformation_construct_composite.h
new file mode 100644
index 0000000..b712457
--- /dev/null
+++ b/source/fuzz/transformation_construct_composite.h
@@ -0,0 +1,88 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_TRANSFORMATION_CONSTRUCT_COMPOSITE_H_
+#define SOURCE_FUZZ_TRANSFORMATION_CONSTRUCT_COMPOSITE_H_
+
+#include "source/fuzz/fact_manager.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationConstructComposite : public Transformation {
+ public:
+  explicit TransformationConstructComposite(
+      const protobufs::TransformationConstructComposite& message);
+
+  TransformationConstructComposite(uint32_t composite_type_id,
+                                   std::vector<uint32_t> component,
+                                   uint32_t base_instruction_id,
+                                   uint32_t offset, uint32_t fresh_id);
+
+  // - |message_.fresh_id| must not be used by the module.
+  // - |message_.composite_type_id| must be the id of a composite type
+  // - The elements of |message_.component| must be result ids that are
+  //   suitable for constructing an element of the given composite type, in
+  //   order
+  // - The elements of |message_.component| must not be the target of any
+  //   decorations.
+  // - |message_.base_instruction_id| must be the result id of an instruction
+  //   'base' in some block 'blk'.
+  // - 'blk' must contain an instruction 'inst' located |message_.offset|
+  //   instructions after 'base' (if |message_.offset| = 0 then 'inst' =
+  //   'base').
+  // - It must be legal to insert an OpCompositeConstruct instruction directly
+  //   before 'inst'.
+  // - Each element of |message_.component| must be available directly before
+  //   'inst'.
+  bool IsApplicable(opt::IRContext* context,
+                    const FactManager& fact_manager) const override;
+
+  // Inserts a new OpCompositeConstruct instruction, with id
+  // |message_.fresh_id|, directly before the instruction identified by
+  // |message_.base_instruction_id| and |message_.offset|.  The instruction
+  // creates a composite of type |message_.composite_type_id| using the ids of
+  // |message_.component|.
+  void Apply(opt::IRContext* context, FactManager* fact_manager) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+ private:
+  // Helper to decide whether the components of the transformation are suitable
+  // for constructing an array of the given type.
+  bool ComponentsForArrayConstructionAreOK(
+      opt::IRContext* context, const opt::analysis::Array& array_type) const;
+
+  // Similar, but for matrices.
+  bool ComponentsForMatrixConstructionAreOK(
+      opt::IRContext* context, const opt::analysis::Matrix& matrix_type) const;
+
+  // Similar, but for structs.
+  bool ComponentsForStructConstructionAreOK(
+      opt::IRContext* context, const opt::analysis::Struct& struct_type) const;
+
+  // Similar, but for vectors.
+  bool ComponentsForVectorConstructionAreOK(
+      opt::IRContext* context, const opt::analysis::Vector& vector_type) const;
+
+  protobufs::TransformationConstructComposite message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_CONSTRUCT_COMPOSITE_H_
diff --git a/source/fuzz/transformation_copy_object.cpp b/source/fuzz/transformation_copy_object.cpp
index cfd97ae..2c6c2fd 100644
--- a/source/fuzz/transformation_copy_object.cpp
+++ b/source/fuzz/transformation_copy_object.cpp
@@ -46,7 +46,7 @@
   if (!object_inst) {
     return false;
   }
-  if (!IsCopyable(context, object_inst)) {
+  if (!fuzzerutil::CanMakeSynonymOf(context, object_inst)) {
     return false;
   }
 
@@ -71,7 +71,8 @@
     return false;
   }
 
-  if (!CanInsertCopyBefore(insert_before)) {
+  if (!fuzzerutil::CanInsertOpcodeBeforeInstruction(SpvOpCopyObject,
+                                                    insert_before)) {
     return false;
   }
 
@@ -91,13 +92,6 @@
 
 void TransformationCopyObject::Apply(opt::IRContext* context,
                                      FactManager* fact_manager) const {
-  // - A new instruction,
-  //     %|message_.fresh_id| = OpCopyObject %ty %|message_.object|
-  //   is added directly before the instruction at |message_.insert_after_id| +
-  //   |message_|.offset, where %ty is the type of |message_.object|.
-  // - The fact that |message_.fresh_id| and |message_.object| are synonyms
-  //   is added to the fact manager.
-  // The id of the object to be copied must exist
   auto object_inst = context->get_def_use_mgr()->GetDef(message_.object());
   assert(object_inst && "The object to be copied must exist.");
   auto base_instruction =
@@ -132,43 +126,5 @@
   return result;
 }
 
-bool TransformationCopyObject::IsCopyable(opt::IRContext* ir_context,
-                                          opt::Instruction* inst) {
-  if (!inst->HasResultId()) {
-    // We can only apply OpCopyObject to instructions that generate ids.
-    return false;
-  }
-  if (!inst->type_id()) {
-    // We can only apply OpCopyObject to instructions that have types.
-    return false;
-  }
-  // We do not copy objects that have decorations: if the copy is not
-  // decorated analogously, using the original object vs. its copy may not be
-  // equivalent.
-  // TODO(afd): it would be possible to make the copy but not add an id
-  // synonym.
-  return ir_context->get_decoration_mgr()
-      ->GetDecorationsFor(inst->result_id(), true)
-      .empty();
-}
-
-bool TransformationCopyObject::CanInsertCopyBefore(
-    const opt::BasicBlock::iterator& instruction_in_block) {
-  if (instruction_in_block->PreviousNode() &&
-      (instruction_in_block->PreviousNode()->opcode() == SpvOpLoopMerge ||
-       instruction_in_block->PreviousNode()->opcode() == SpvOpSelectionMerge)) {
-    // We cannot insert a copy directly after a merge instruction.
-    return false;
-  }
-  if (instruction_in_block->opcode() == SpvOpVariable) {
-    // We cannot insert a copy directly before a variable; variables in a
-    // function must be contiguous in the entry block.
-    return false;
-  }
-  // We cannot insert a copy directly before OpPhi, because OpPhi instructions
-  // need to be contiguous at the start of a block.
-  return instruction_in_block->opcode() != SpvOpPhi;
-}
-
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/transformation_copy_object.h b/source/fuzz/transformation_copy_object.h
index 20e5874..1b42726 100644
--- a/source/fuzz/transformation_copy_object.h
+++ b/source/fuzz/transformation_copy_object.h
@@ -37,14 +37,14 @@
   //   has a result type
   // - |message_.object| must not be the target of any decoration.
   //   TODO(afd): consider copying decorations along with objects.
-  // - |message_.insert_after_id| must be the result id of an instruction
+  // - |message_.base_instruction_id| must be the result id of an instruction
   //   'base' in some block 'blk'.
   // - 'blk' must contain an instruction 'inst' located |message_.offset|
   //   instructions after 'base' (if |message_.offset| = 0 then 'inst' =
   //   'base').
   // - It must be legal to insert an OpCopyObject instruction directly
   //   before 'inst'.
-  // - |message_object| must be available directly before 'inst'.
+  // - |message_.object| must be available directly before 'inst'.
   bool IsApplicable(opt::IRContext* context,
                     const FactManager& fact_manager) const override;
 
@@ -58,14 +58,6 @@
 
   protobufs::Transformation ToMessage() const override;
 
-  // Determines whether it is OK to make a copy of |inst|.
-  static bool IsCopyable(opt::IRContext* ir_context, opt::Instruction* inst);
-
-  // Determines whether it is OK to insert a copy instruction before the given
-  // instruction.
-  static bool CanInsertCopyBefore(
-      const opt::BasicBlock::iterator& instruction_in_block);
-
  private:
   protobufs::TransformationCopyObject message_;
 };
diff --git a/source/fuzz/transformation_replace_id_with_synonym.cpp b/source/fuzz/transformation_replace_id_with_synonym.cpp
index 0f28540..c96a519 100644
--- a/source/fuzz/transformation_replace_id_with_synonym.cpp
+++ b/source/fuzz/transformation_replace_id_with_synonym.cpp
@@ -20,6 +20,7 @@
 #include "source/fuzz/fuzzer_util.h"
 #include "source/fuzz/id_use_descriptor.h"
 #include "source/opt/types.h"
+#include "source/util/make_unique.h"
 
 namespace spvtools {
 namespace fuzz {
@@ -33,9 +34,7 @@
     protobufs::IdUseDescriptor id_use_descriptor,
     protobufs::DataDescriptor data_descriptor,
     uint32_t fresh_id_for_temporary) {
-  assert(fresh_id_for_temporary == 0 && data_descriptor.index().size() == 0 &&
-         "At present we do not support making an id that is synonymous with an "
-         "index into a composite.");
+  assert((fresh_id_for_temporary == 0) == (data_descriptor.index().empty()));
   *message_.mutable_id_use_descriptor() = std::move(id_use_descriptor);
   *message_.mutable_data_descriptor() = std::move(data_descriptor);
   message_.set_fresh_id_for_temporary(fresh_id_for_temporary);
@@ -61,12 +60,15 @@
     return false;
   }
 
+  // Does the id use descriptor in the transformation identify an instruction?
   auto use_instruction =
       transformation::FindInstruction(message_.id_use_descriptor(), context);
   if (!use_instruction) {
     return false;
   }
 
+  // Is it legitimate to replace the use identified by the id use descriptor
+  // with a synonym?
   if (!ReplacingUseWithSynonymIsOk(
           context, use_instruction,
           message_.id_use_descriptor().in_operand_index(),
@@ -74,8 +76,23 @@
     return false;
   }
 
-  assert(message_.fresh_id_for_temporary() == 0);
-  assert(message_.data_descriptor().index().empty());
+  if (message_.fresh_id_for_temporary() == 0) {
+    if (!message_.data_descriptor().index().empty()) {
+      // If we have no id to use as a temporary variable, we should not have any
+      // indices to extract from.
+      return false;
+    }
+  } else {
+    if (!fuzzerutil::IsFreshId(context, message_.fresh_id_for_temporary())) {
+      // The id to be used as a temporary needs to be fresh.
+      return false;
+    }
+    if (message_.data_descriptor().index_size() != 1) {
+      // At present we support just a single index to allow extracting directly
+      // from a composite.
+      return false;
+    }
+  }
 
   return true;
 }
@@ -83,12 +100,79 @@
 void TransformationReplaceIdWithSynonym::Apply(
     spvtools::opt::IRContext* context,
     spvtools::fuzz::FactManager* /*unused*/) const {
-  assert(message_.data_descriptor().index().empty());
   auto instruction_to_change =
       transformation::FindInstruction(message_.id_use_descriptor(), context);
+
+  // Ultimately we are going to replace the id use identified in the
+  // transformation with |replacement_id|, which will either be the synonym's
+  // id, or the id of a temporary used to extract the synonym from a composite.
+  uint32_t replacement_id;
+
+  if (message_.fresh_id_for_temporary()) {
+    // The transformation having a temporary variable means that we need to
+    // extract the synonym from a composite.
+
+    uint32_t type_id_of_id_to_be_replaced =
+        context->get_def_use_mgr()
+            ->GetDef(message_.id_use_descriptor().id_of_interest())
+            ->type_id();
+    opt::analysis::Type* type_of_id_to_be_replaced =
+        context->get_type_mgr()->GetType(type_id_of_id_to_be_replaced);
+    opt::analysis::Type* type_of_composite = context->get_type_mgr()->GetType(
+        context->get_def_use_mgr()
+            ->GetDef(message_.data_descriptor().object())
+            ->type_id());
+
+    // Intuitively we want to make an OpCompositeExtract instruction, to get the
+    // synonym out of the composite. But in the case of a vector, the synonym
+    // might involve multiple vector indices; e.g. the y and z components of a
+    // vec4 might be synonymous with a vec2, and in that case OpCompositeExtract
+    // doesn't give us what we want; we need to use OpVectorShuffle instead.
+    std::unique_ptr<opt::Instruction> new_instruction;
+    if (type_of_composite->AsVector() &&
+        type_of_composite->AsVector()->element_type() !=
+            type_of_id_to_be_replaced) {
+      // We need to extract a vector from inside a vector, so we will need to
+      // use OpVectorShuffle.
+
+      assert(type_of_id_to_be_replaced->AsVector());
+      assert(type_of_id_to_be_replaced->AsVector()->element_type() ==
+             type_of_composite->AsVector()->element_type());
+      opt::Instruction::OperandList shuffle_operands = {
+          {SPV_OPERAND_TYPE_ID, {message_.data_descriptor().object()}},
+          {SPV_OPERAND_TYPE_ID, {message_.data_descriptor().object()}}};
+      for (uint32_t i = 0;
+           i < type_of_id_to_be_replaced->AsVector()->element_count(); i++) {
+        shuffle_operands.push_back({SPV_OPERAND_TYPE_LITERAL_INTEGER,
+                                    {message_.data_descriptor().index(0) + i}});
+      }
+      new_instruction = MakeUnique<opt::Instruction>(
+          context, SpvOpVectorShuffle, type_id_of_id_to_be_replaced,
+          message_.fresh_id_for_temporary(), shuffle_operands);
+    } else {
+      // We are either extracting from a non-vector, or extracting a scalar from
+      // a vector, so we can use OpCompositeExtract.
+      opt::Instruction::OperandList extract_operands = {
+          {SPV_OPERAND_TYPE_ID, {message_.data_descriptor().object()}},
+          {SPV_OPERAND_TYPE_LITERAL_INTEGER,
+           {message_.data_descriptor().index(0)}}};
+      new_instruction = MakeUnique<opt::Instruction>(
+          context, SpvOpCompositeExtract, type_id_of_id_to_be_replaced,
+          message_.fresh_id_for_temporary(), extract_operands);
+    }
+    instruction_to_change->InsertBefore(std::move(new_instruction));
+
+    // The replacement id is the temporary variable we used to extract the
+    // synonym from a composite.
+    replacement_id = message_.fresh_id_for_temporary();
+    fuzzerutil::UpdateModuleIdBound(context, replacement_id);
+  } else {
+    // The replacement id is the synonym's id.
+    replacement_id = message_.data_descriptor().object();
+  }
+
   instruction_to_change->SetInOperand(
-      message_.id_use_descriptor().in_operand_index(),
-      {message_.data_descriptor().object()});
+      message_.id_use_descriptor().in_operand_index(), {replacement_id});
   context->InvalidateAnalysesExceptFor(opt::IRContext::Analysis::kAnalysisNone);
 }
 
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index f89d990..ca51b48 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -28,6 +28,7 @@
           transformation_add_type_float_test.cpp
           transformation_add_type_int_test.cpp
           transformation_add_type_pointer_test.cpp
+          transformation_construct_composite_test.cpp
           transformation_copy_object_test.cpp
           transformation_move_block_down_test.cpp
           transformation_replace_boolean_constant_with_constant_binary_test.cpp
diff --git a/test/fuzz/fuzzer_replayer_test.cpp b/test/fuzz/fuzzer_replayer_test.cpp
index 5fb71b0..c2a1984 100644
--- a/test/fuzz/fuzzer_replayer_test.cpp
+++ b/test/fuzz/fuzzer_replayer_test.cpp
@@ -21,6 +21,8 @@
 namespace fuzz {
 namespace {
 
+const uint32_t kNumFuzzerRuns = 20;
+
 // Assembles the given |shader| text, and then runs the fuzzer |num_runs|
 // times, using successive seeds starting from |initial_seed|.  Checks that
 // the binary produced after each fuzzer run is valid, and that replaying
@@ -240,9 +242,9 @@
                OpFunctionEnd
   )";
 
-  // Do 5 fuzzer runs, starting from an initial seed of 0 (seed value chosen
+  // Do some fuzzer runs, starting from an initial seed of 0 (seed value chosen
   // arbitrarily).
-  RunFuzzerAndReplayer(shader, protobufs::FactSequence(), 0, 5);
+  RunFuzzerAndReplayer(shader, protobufs::FactSequence(), 0, kNumFuzzerRuns);
 }
 
 TEST(FuzzerReplayerTest, Miscellaneous2) {
@@ -485,9 +487,9 @@
                OpFunctionEnd
   )";
 
-  // Do 5 fuzzer runs, starting from an initial seed of 10 (seed value chosen
+  // Do some fuzzer runs, starting from an initial seed of 10 (seed value chosen
   // arbitrarily).
-  RunFuzzerAndReplayer(shader, protobufs::FactSequence(), 10, 5);
+  RunFuzzerAndReplayer(shader, protobufs::FactSequence(), 10, kNumFuzzerRuns);
 }
 
 TEST(FuzzerReplayerTest, Miscellaneous3) {
@@ -970,9 +972,9 @@
     *facts.mutable_fact()->Add() = temp;
   }
 
-  // Do 5 fuzzer runs, starting from an initial seed of 94 (seed value chosen
+  // Do some fuzzer runs, starting from an initial seed of 94 (seed value chosen
   // arbitrarily).
-  RunFuzzerAndReplayer(shader, facts, 94, 5);
+  RunFuzzerAndReplayer(shader, facts, 94, kNumFuzzerRuns);
 }
 
 }  // namespace
diff --git a/test/fuzz/transformation_construct_composite_test.cpp b/test/fuzz/transformation_construct_composite_test.cpp
new file mode 100644
index 0000000..26988fb
--- /dev/null
+++ b/test/fuzz/transformation_construct_composite_test.cpp
@@ -0,0 +1,1248 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/transformation_construct_composite.h"
+#include "source/fuzz/data_descriptor.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+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));
+  return std::find_if(synonyms.begin(), synonyms.end(),
+                      [&temp](protobufs::DataDescriptor dd) -> bool {
+                        return DataDescriptorEquals()(&dd, &temp);
+                      }) != synonyms.end();
+}
+
+TEST(TransformationConstructCompositeTest, ConstructArrays) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %11 "floats"
+               OpName %22 "x"
+               OpName %39 "vecs"
+               OpName %49 "bools"
+               OpName %60 "many_uvec3s"
+               OpDecorate %60 RelaxedPrecision
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeInt 32 0
+          %8 = OpConstant %7 2
+          %9 = OpTypeArray %6 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpTypeInt 32 1
+         %13 = OpConstant %12 0
+         %14 = OpConstant %6 1
+         %15 = OpTypePointer Function %6
+         %17 = OpConstant %12 1
+         %18 = OpConstant %6 2
+         %20 = OpTypeVector %6 2
+         %21 = OpTypePointer Function %20
+         %32 = OpTypeBool
+         %36 = OpConstant %7 3
+         %37 = OpTypeArray %20 %36
+         %38 = OpTypePointer Private %37
+         %39 = OpVariable %38 Private
+         %40 = OpConstant %6 3
+         %41 = OpConstantComposite %20 %40 %40
+         %42 = OpTypePointer Private %20
+         %44 = OpConstant %12 2
+         %47 = OpTypeArray %32 %36
+         %48 = OpTypePointer Function %47
+         %50 = OpConstantTrue %32
+         %51 = OpTypePointer Function %32
+         %56 = OpTypeVector %7 3
+         %57 = OpTypeArray %56 %8
+         %58 = OpTypeArray %57 %8
+         %59 = OpTypePointer Function %58
+         %61 = OpConstant %7 4
+         %62 = OpConstantComposite %56 %61 %61 %61
+         %63 = OpTypePointer Function %56
+         %65 = OpConstant %7 5
+         %66 = OpConstantComposite %56 %65 %65 %65
+         %67 = OpConstant %7 6
+         %68 = OpConstantComposite %56 %67 %67 %67
+         %69 = OpConstantComposite %57 %66 %68
+        %100 = OpUndef %57
+         %70 = OpTypePointer Function %57
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %22 = OpVariable %21 Function
+         %49 = OpVariable %48 Function
+         %60 = OpVariable %59 Function
+         %16 = OpAccessChain %15 %11 %13
+               OpStore %16 %14
+         %19 = OpAccessChain %15 %11 %17
+               OpStore %19 %18
+         %23 = OpAccessChain %15 %11 %13
+         %24 = OpLoad %6 %23
+         %25 = OpAccessChain %15 %11 %17
+         %26 = OpLoad %6 %25
+         %27 = OpCompositeConstruct %20 %24 %26
+               OpStore %22 %27
+         %28 = OpAccessChain %15 %11 %13
+         %29 = OpLoad %6 %28
+         %30 = OpAccessChain %15 %11 %17
+         %31 = OpLoad %6 %30
+         %33 = OpFOrdGreaterThan %32 %29 %31
+               OpSelectionMerge %35 None
+               OpBranchConditional %33 %34 %35
+         %34 = OpLabel
+         %43 = OpAccessChain %42 %39 %17
+               OpStore %43 %41
+         %45 = OpLoad %20 %22
+         %46 = OpAccessChain %42 %39 %44
+               OpStore %46 %45
+               OpBranch %35
+         %35 = OpLabel
+         %52 = OpAccessChain %51 %49 %13
+               OpStore %52 %50
+         %53 = OpAccessChain %51 %49 %13
+         %54 = OpLoad %32 %53
+         %55 = OpAccessChain %51 %49 %17
+               OpStore %55 %54
+         %64 = OpAccessChain %63 %60 %13 %13
+               OpStore %64 %62
+         %71 = OpAccessChain %70 %60 %17
+               OpStore %71 %69
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  // Make a vec2[3]
+  TransformationConstructComposite make_vec2_array_length_3(37, {41, 45, 27},
+                                                            46, 0, 200);
+  // Bad: there are too many components
+  TransformationConstructComposite make_vec2_array_length_3_bad(
+      37, {41, 45, 27, 27}, 46, 0, 200);
+  ASSERT_TRUE(
+      make_vec2_array_length_3.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(
+      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}));
+
+  // Make a float[2]
+  TransformationConstructComposite make_float_array_length_2(9, {24, 40}, 71, 1,
+                                                             201);
+  // Bad: %41 does not have type float
+  TransformationConstructComposite make_float_array_length_2_bad(9, {41, 40},
+                                                                 71, 1, 201);
+  ASSERT_TRUE(
+      make_float_array_length_2.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(
+      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}));
+
+  // Make a bool[3]
+  TransformationConstructComposite make_bool_array_length_3(47, {33, 50, 50},
+                                                            33, 1, 202);
+  // Bad: %54 is not available at the desired program point.
+  TransformationConstructComposite make_bool_array_length_3_bad(
+      47, {33, 54, 50}, 33, 1, 202);
+  ASSERT_TRUE(
+      make_bool_array_length_3.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(
+      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}));
+
+  // make a uvec3[2][2]
+  TransformationConstructComposite make_uvec3_array_length_2_2(58, {69, 100},
+                                                               64, 1, 203);
+  // Bad: Offset 100 is too large.
+  TransformationConstructComposite make_uvec3_array_length_2_2_bad(
+      58, {33, 54}, 64, 100, 203);
+  ASSERT_TRUE(
+      make_uvec3_array_length_2_2.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(make_uvec3_array_length_2_2_bad.IsApplicable(context.get(),
+                                                            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}));
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %11 "floats"
+               OpName %22 "x"
+               OpName %39 "vecs"
+               OpName %49 "bools"
+               OpName %60 "many_uvec3s"
+               OpDecorate %60 RelaxedPrecision
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeInt 32 0
+          %8 = OpConstant %7 2
+          %9 = OpTypeArray %6 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpTypeInt 32 1
+         %13 = OpConstant %12 0
+         %14 = OpConstant %6 1
+         %15 = OpTypePointer Function %6
+         %17 = OpConstant %12 1
+         %18 = OpConstant %6 2
+         %20 = OpTypeVector %6 2
+         %21 = OpTypePointer Function %20
+         %32 = OpTypeBool
+         %36 = OpConstant %7 3
+         %37 = OpTypeArray %20 %36
+         %38 = OpTypePointer Private %37
+         %39 = OpVariable %38 Private
+         %40 = OpConstant %6 3
+         %41 = OpConstantComposite %20 %40 %40
+         %42 = OpTypePointer Private %20
+         %44 = OpConstant %12 2
+         %47 = OpTypeArray %32 %36
+         %48 = OpTypePointer Function %47
+         %50 = OpConstantTrue %32
+         %51 = OpTypePointer Function %32
+         %56 = OpTypeVector %7 3
+         %57 = OpTypeArray %56 %8
+         %58 = OpTypeArray %57 %8
+         %59 = OpTypePointer Function %58
+         %61 = OpConstant %7 4
+         %62 = OpConstantComposite %56 %61 %61 %61
+         %63 = OpTypePointer Function %56
+         %65 = OpConstant %7 5
+         %66 = OpConstantComposite %56 %65 %65 %65
+         %67 = OpConstant %7 6
+         %68 = OpConstantComposite %56 %67 %67 %67
+         %69 = OpConstantComposite %57 %66 %68
+        %100 = OpUndef %57
+         %70 = OpTypePointer Function %57
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %22 = OpVariable %21 Function
+         %49 = OpVariable %48 Function
+         %60 = OpVariable %59 Function
+         %16 = OpAccessChain %15 %11 %13
+               OpStore %16 %14
+         %19 = OpAccessChain %15 %11 %17
+               OpStore %19 %18
+         %23 = OpAccessChain %15 %11 %13
+         %24 = OpLoad %6 %23
+         %25 = OpAccessChain %15 %11 %17
+         %26 = OpLoad %6 %25
+         %27 = OpCompositeConstruct %20 %24 %26
+               OpStore %22 %27
+         %28 = OpAccessChain %15 %11 %13
+         %29 = OpLoad %6 %28
+         %30 = OpAccessChain %15 %11 %17
+         %31 = OpLoad %6 %30
+         %33 = OpFOrdGreaterThan %32 %29 %31
+        %202 = OpCompositeConstruct %47 %33 %50 %50
+               OpSelectionMerge %35 None
+               OpBranchConditional %33 %34 %35
+         %34 = OpLabel
+         %43 = OpAccessChain %42 %39 %17
+               OpStore %43 %41
+         %45 = OpLoad %20 %22
+        %200 = OpCompositeConstruct %37 %41 %45 %27
+         %46 = OpAccessChain %42 %39 %44
+               OpStore %46 %45
+               OpBranch %35
+         %35 = OpLabel
+         %52 = OpAccessChain %51 %49 %13
+               OpStore %52 %50
+         %53 = OpAccessChain %51 %49 %13
+         %54 = OpLoad %32 %53
+         %55 = OpAccessChain %51 %49 %17
+               OpStore %55 %54
+         %64 = OpAccessChain %63 %60 %13 %13
+        %203 = OpCompositeConstruct %58 %69 %100
+               OpStore %64 %62
+         %71 = OpAccessChain %70 %60 %17
+        %201 = OpCompositeConstruct %9 %24 %40
+               OpStore %71 %69
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationConstructCompositeTest, ConstructMatrices) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "v1"
+               OpName %12 "v2"
+               OpName %14 "v3"
+               OpName %19 "v4"
+               OpName %26 "v5"
+               OpName %29 "v6"
+               OpName %34 "m34"
+               OpName %37 "m43"
+               OpName %43 "vecs"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 3
+          %8 = OpTypePointer Function %7
+         %10 = OpConstant %6 1
+         %11 = OpConstantComposite %7 %10 %10 %10
+         %17 = OpTypeVector %6 4
+         %18 = OpTypePointer Function %17
+         %21 = OpConstant %6 2
+         %32 = OpTypeMatrix %17 3
+         %33 = OpTypePointer Private %32
+         %34 = OpVariable %33 Private
+         %35 = OpTypeMatrix %7 4
+         %36 = OpTypePointer Private %35
+         %37 = OpVariable %36 Private
+         %38 = OpTypeVector %6 2
+         %39 = OpTypeInt 32 0
+         %40 = OpConstant %39 3
+         %41 = OpTypeArray %38 %40
+         %42 = OpTypePointer Private %41
+         %43 = OpVariable %42 Private
+        %100 = OpUndef %7
+        %101 = OpUndef %17
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+         %12 = OpVariable %8 Function
+         %14 = OpVariable %8 Function
+         %19 = OpVariable %18 Function
+         %26 = OpVariable %18 Function
+         %29 = OpVariable %18 Function
+               OpStore %9 %11
+         %13 = OpLoad %7 %9
+               OpStore %12 %13
+         %15 = OpLoad %7 %12
+         %16 = OpVectorShuffle %7 %15 %15 2 1 0
+               OpStore %14 %16
+         %20 = OpLoad %7 %14
+         %22 = OpCompositeExtract %6 %20 0
+         %23 = OpCompositeExtract %6 %20 1
+         %24 = OpCompositeExtract %6 %20 2
+         %25 = OpCompositeConstruct %17 %22 %23 %24 %21
+               OpStore %19 %25
+         %27 = OpLoad %17 %19
+         %28 = OpVectorShuffle %17 %27 %27 3 2 1 0
+               OpStore %26 %28
+         %30 = OpLoad %7 %9
+         %31 = OpVectorShuffle %17 %30 %30 0 0 1 1
+               OpStore %29 %31
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  // make a mat3x4
+  TransformationConstructComposite make_mat34(32, {25, 28, 31}, 31, 2, 200);
+  // Bad: %35 is mat4x3, not mat3x4.
+  TransformationConstructComposite make_mat34_bad(35, {25, 28, 31}, 31, 2, 200);
+  ASSERT_TRUE(make_mat34.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  // make a mat4x3
+  TransformationConstructComposite make_mat43(35, {11, 13, 16, 100}, 31, 1,
+                                              201);
+  // Bad: %25 does not match the matrix's column type.
+  TransformationConstructComposite make_mat43_bad(35, {25, 13, 16, 100}, 31, 1,
+                                                  201);
+  ASSERT_TRUE(make_mat43.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "v1"
+               OpName %12 "v2"
+               OpName %14 "v3"
+               OpName %19 "v4"
+               OpName %26 "v5"
+               OpName %29 "v6"
+               OpName %34 "m34"
+               OpName %37 "m43"
+               OpName %43 "vecs"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 3
+          %8 = OpTypePointer Function %7
+         %10 = OpConstant %6 1
+         %11 = OpConstantComposite %7 %10 %10 %10
+         %17 = OpTypeVector %6 4
+         %18 = OpTypePointer Function %17
+         %21 = OpConstant %6 2
+         %32 = OpTypeMatrix %17 3
+         %33 = OpTypePointer Private %32
+         %34 = OpVariable %33 Private
+         %35 = OpTypeMatrix %7 4
+         %36 = OpTypePointer Private %35
+         %37 = OpVariable %36 Private
+         %38 = OpTypeVector %6 2
+         %39 = OpTypeInt 32 0
+         %40 = OpConstant %39 3
+         %41 = OpTypeArray %38 %40
+         %42 = OpTypePointer Private %41
+         %43 = OpVariable %42 Private
+        %100 = OpUndef %7
+        %101 = OpUndef %17
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+         %12 = OpVariable %8 Function
+         %14 = OpVariable %8 Function
+         %19 = OpVariable %18 Function
+         %26 = OpVariable %18 Function
+         %29 = OpVariable %18 Function
+               OpStore %9 %11
+         %13 = OpLoad %7 %9
+               OpStore %12 %13
+         %15 = OpLoad %7 %12
+         %16 = OpVectorShuffle %7 %15 %15 2 1 0
+               OpStore %14 %16
+         %20 = OpLoad %7 %14
+         %22 = OpCompositeExtract %6 %20 0
+         %23 = OpCompositeExtract %6 %20 1
+         %24 = OpCompositeExtract %6 %20 2
+         %25 = OpCompositeConstruct %17 %22 %23 %24 %21
+               OpStore %19 %25
+         %27 = OpLoad %17 %19
+         %28 = OpVectorShuffle %17 %27 %27 3 2 1 0
+               OpStore %26 %28
+         %30 = OpLoad %7 %9
+         %31 = OpVectorShuffle %17 %30 %30 0 0 1 1
+        %201 = OpCompositeConstruct %35 %11 %13 %16 %100
+               OpStore %29 %31
+        %200 = OpCompositeConstruct %32 %25 %28 %31
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationConstructCompositeTest, ConstructStructs) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "Inner"
+               OpMemberName %9 0 "a"
+               OpMemberName %9 1 "b"
+               OpName %11 "i1"
+               OpName %22 "i2"
+               OpName %33 "Outer"
+               OpMemberName %33 0 "c"
+               OpMemberName %33 1 "d"
+               OpMemberName %33 2 "e"
+               OpName %35 "o"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 2
+          %8 = OpTypeInt 32 1
+          %9 = OpTypeStruct %7 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpConstant %8 0
+         %13 = OpConstant %6 2
+         %14 = OpTypeInt 32 0
+         %15 = OpConstant %14 0
+         %16 = OpTypePointer Function %6
+         %18 = OpConstant %8 1
+         %19 = OpConstant %8 3
+         %20 = OpTypePointer Function %8
+         %23 = OpTypePointer Function %7
+         %31 = OpConstant %14 2
+         %32 = OpTypeArray %9 %31
+         %33 = OpTypeStruct %32 %9 %6
+         %34 = OpTypePointer Function %33
+         %36 = OpConstant %6 1
+         %37 = OpConstantComposite %7 %36 %13
+         %38 = OpConstant %8 2
+         %39 = OpConstantComposite %9 %37 %38
+         %40 = OpConstant %6 3
+         %41 = OpConstant %6 4
+         %42 = OpConstantComposite %7 %40 %41
+         %56 = OpConstant %6 5
+        %100 = OpUndef %9
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %22 = OpVariable %10 Function
+         %35 = OpVariable %34 Function
+         %17 = OpAccessChain %16 %11 %12 %15
+               OpStore %17 %13
+         %21 = OpAccessChain %20 %11 %18
+               OpStore %21 %19
+         %24 = OpAccessChain %23 %11 %12
+         %25 = OpLoad %7 %24
+         %26 = OpAccessChain %23 %22 %12
+               OpStore %26 %25
+         %27 = OpAccessChain %20 %11 %18
+         %28 = OpLoad %8 %27
+         %29 = OpIAdd %8 %28 %18
+         %30 = OpAccessChain %20 %22 %18
+               OpStore %30 %29
+         %43 = OpAccessChain %20 %11 %18
+         %44 = OpLoad %8 %43
+         %45 = OpCompositeConstruct %9 %42 %44
+         %46 = OpCompositeConstruct %32 %39 %45
+         %47 = OpLoad %9 %22
+         %48 = OpCompositeConstruct %33 %46 %47 %40
+               OpStore %35 %48
+         %49 = OpLoad %9 %11
+         %50 = OpAccessChain %10 %35 %12 %12
+               OpStore %50 %49
+         %51 = OpLoad %9 %22
+         %52 = OpAccessChain %10 %35 %12 %18
+               OpStore %52 %51
+         %53 = OpAccessChain %10 %35 %12 %12
+         %54 = OpLoad %9 %53
+         %55 = OpAccessChain %10 %35 %18
+               OpStore %55 %54
+         %57 = OpAccessChain %16 %35 %38
+               OpStore %57 %56
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  // make an Inner
+  TransformationConstructComposite make_inner(9, {25, 19}, 57, 0, 200);
+  // Bad: Too few fields to make the struct.
+  TransformationConstructComposite make_inner_bad(9, {25}, 57, 0, 200);
+  ASSERT_TRUE(make_inner.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  // make an Outer
+  TransformationConstructComposite make_outer(33, {46, 200, 56}, 200, 1, 201);
+  // Bad: %200 is not available at the desired program point.
+  TransformationConstructComposite make_outer_bad(33, {46, 200, 56}, 200, 0,
+                                                  201);
+  ASSERT_TRUE(make_outer.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "Inner"
+               OpMemberName %9 0 "a"
+               OpMemberName %9 1 "b"
+               OpName %11 "i1"
+               OpName %22 "i2"
+               OpName %33 "Outer"
+               OpMemberName %33 0 "c"
+               OpMemberName %33 1 "d"
+               OpMemberName %33 2 "e"
+               OpName %35 "o"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 2
+          %8 = OpTypeInt 32 1
+          %9 = OpTypeStruct %7 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpConstant %8 0
+         %13 = OpConstant %6 2
+         %14 = OpTypeInt 32 0
+         %15 = OpConstant %14 0
+         %16 = OpTypePointer Function %6
+         %18 = OpConstant %8 1
+         %19 = OpConstant %8 3
+         %20 = OpTypePointer Function %8
+         %23 = OpTypePointer Function %7
+         %31 = OpConstant %14 2
+         %32 = OpTypeArray %9 %31
+         %33 = OpTypeStruct %32 %9 %6
+         %34 = OpTypePointer Function %33
+         %36 = OpConstant %6 1
+         %37 = OpConstantComposite %7 %36 %13
+         %38 = OpConstant %8 2
+         %39 = OpConstantComposite %9 %37 %38
+         %40 = OpConstant %6 3
+         %41 = OpConstant %6 4
+         %42 = OpConstantComposite %7 %40 %41
+         %56 = OpConstant %6 5
+        %100 = OpUndef %9
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %22 = OpVariable %10 Function
+         %35 = OpVariable %34 Function
+         %17 = OpAccessChain %16 %11 %12 %15
+               OpStore %17 %13
+         %21 = OpAccessChain %20 %11 %18
+               OpStore %21 %19
+         %24 = OpAccessChain %23 %11 %12
+         %25 = OpLoad %7 %24
+         %26 = OpAccessChain %23 %22 %12
+               OpStore %26 %25
+         %27 = OpAccessChain %20 %11 %18
+         %28 = OpLoad %8 %27
+         %29 = OpIAdd %8 %28 %18
+         %30 = OpAccessChain %20 %22 %18
+               OpStore %30 %29
+         %43 = OpAccessChain %20 %11 %18
+         %44 = OpLoad %8 %43
+         %45 = OpCompositeConstruct %9 %42 %44
+         %46 = OpCompositeConstruct %32 %39 %45
+         %47 = OpLoad %9 %22
+         %48 = OpCompositeConstruct %33 %46 %47 %40
+               OpStore %35 %48
+         %49 = OpLoad %9 %11
+         %50 = OpAccessChain %10 %35 %12 %12
+               OpStore %50 %49
+         %51 = OpLoad %9 %22
+         %52 = OpAccessChain %10 %35 %12 %18
+               OpStore %52 %51
+         %53 = OpAccessChain %10 %35 %12 %12
+         %54 = OpLoad %9 %53
+         %55 = OpAccessChain %10 %35 %18
+               OpStore %55 %54
+        %200 = OpCompositeConstruct %9 %25 %19
+        %201 = OpCompositeConstruct %33 %46 %200 %56
+         %57 = OpAccessChain %16 %35 %38
+               OpStore %57 %56
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationConstructCompositeTest, ConstructVectors) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "v2"
+               OpName %27 "v3"
+               OpName %46 "v4"
+               OpName %53 "iv2"
+               OpName %61 "uv3"
+               OpName %72 "bv4"
+               OpName %88 "uv2"
+               OpName %95 "bv3"
+               OpName %104 "bv2"
+               OpName %116 "iv3"
+               OpName %124 "iv4"
+               OpName %133 "uv4"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 2
+          %8 = OpTypePointer Function %7
+         %10 = OpConstant %6 1
+         %11 = OpConstant %6 2
+         %12 = OpConstantComposite %7 %10 %11
+         %13 = OpTypeInt 32 0
+         %14 = OpConstant %13 0
+         %15 = OpTypePointer Function %6
+         %18 = OpConstant %13 1
+         %21 = OpTypeBool
+         %25 = OpTypeVector %6 3
+         %26 = OpTypePointer Function %25
+         %33 = OpConstant %6 3
+         %34 = OpConstant %6 -0.756802499
+         %38 = OpConstant %13 2
+         %44 = OpTypeVector %6 4
+         %45 = OpTypePointer Function %44
+         %50 = OpTypeInt 32 1
+         %51 = OpTypeVector %50 2
+         %52 = OpTypePointer Function %51
+         %57 = OpTypePointer Function %50
+         %59 = OpTypeVector %13 3
+         %60 = OpTypePointer Function %59
+         %65 = OpConstant %13 3
+         %67 = OpTypePointer Function %13
+         %70 = OpTypeVector %21 4
+         %71 = OpTypePointer Function %70
+         %73 = OpConstantTrue %21
+         %74 = OpTypePointer Function %21
+         %86 = OpTypeVector %13 2
+         %87 = OpTypePointer Function %86
+         %93 = OpTypeVector %21 3
+         %94 = OpTypePointer Function %93
+        %102 = OpTypeVector %21 2
+        %103 = OpTypePointer Function %102
+        %111 = OpConstantFalse %21
+        %114 = OpTypeVector %50 3
+        %115 = OpTypePointer Function %114
+        %117 = OpConstant %50 3
+        %122 = OpTypeVector %50 4
+        %123 = OpTypePointer Function %122
+        %131 = OpTypeVector %13 4
+        %132 = OpTypePointer Function %131
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+         %27 = OpVariable %26 Function
+         %46 = OpVariable %45 Function
+         %53 = OpVariable %52 Function
+         %61 = OpVariable %60 Function
+         %72 = OpVariable %71 Function
+         %88 = OpVariable %87 Function
+         %95 = OpVariable %94 Function
+        %104 = OpVariable %103 Function
+        %116 = OpVariable %115 Function
+        %124 = OpVariable %123 Function
+        %133 = OpVariable %132 Function
+               OpStore %9 %12
+         %16 = OpAccessChain %15 %9 %14
+         %17 = OpLoad %6 %16
+         %19 = OpAccessChain %15 %9 %18
+         %20 = OpLoad %6 %19
+         %22 = OpFOrdGreaterThan %21 %17 %20
+               OpSelectionMerge %24 None
+               OpBranchConditional %22 %23 %101
+         %23 = OpLabel
+         %28 = OpAccessChain %15 %9 %14
+         %29 = OpLoad %6 %28
+         %30 = OpAccessChain %15 %9 %18
+         %31 = OpLoad %6 %30
+         %32 = OpFAdd %6 %29 %31
+         %35 = OpCompositeConstruct %25 %32 %33 %34
+               OpStore %27 %35
+         %36 = OpAccessChain %15 %27 %14
+         %37 = OpLoad %6 %36
+         %39 = OpAccessChain %15 %27 %38
+         %40 = OpLoad %6 %39
+         %41 = OpFOrdLessThan %21 %37 %40
+               OpSelectionMerge %43 None
+               OpBranchConditional %41 %42 %69
+         %42 = OpLabel
+         %47 = OpAccessChain %15 %9 %18
+         %48 = OpLoad %6 %47
+         %49 = OpAccessChain %15 %46 %14
+               OpStore %49 %48
+         %54 = OpAccessChain %15 %27 %38
+         %55 = OpLoad %6 %54
+         %56 = OpConvertFToS %50 %55
+         %58 = OpAccessChain %57 %53 %14
+               OpStore %58 %56
+         %62 = OpAccessChain %15 %46 %14
+         %63 = OpLoad %6 %62
+         %64 = OpConvertFToU %13 %63
+         %66 = OpIAdd %13 %64 %65
+         %68 = OpAccessChain %67 %61 %14
+               OpStore %68 %66
+               OpBranch %43
+         %69 = OpLabel
+         %75 = OpAccessChain %74 %72 %14
+               OpStore %75 %73
+         %76 = OpAccessChain %74 %72 %14
+         %77 = OpLoad %21 %76
+         %78 = OpLogicalNot %21 %77
+         %79 = OpAccessChain %74 %72 %18
+               OpStore %79 %78
+         %80 = OpAccessChain %74 %72 %14
+         %81 = OpLoad %21 %80
+         %82 = OpAccessChain %74 %72 %18
+         %83 = OpLoad %21 %82
+         %84 = OpLogicalAnd %21 %81 %83
+         %85 = OpAccessChain %74 %72 %38
+               OpStore %85 %84
+         %89 = OpAccessChain %67 %88 %14
+         %90 = OpLoad %13 %89
+         %91 = OpINotEqual %21 %90 %14
+         %92 = OpAccessChain %74 %72 %65
+               OpStore %92 %91
+               OpBranch %43
+         %43 = OpLabel
+         %96 = OpLoad %70 %72
+         %97 = OpCompositeExtract %21 %96 0
+         %98 = OpCompositeExtract %21 %96 1
+         %99 = OpCompositeExtract %21 %96 2
+        %100 = OpCompositeConstruct %93 %97 %98 %99
+               OpStore %95 %100
+               OpBranch %24
+        %101 = OpLabel
+        %105 = OpAccessChain %67 %88 %14
+        %106 = OpLoad %13 %105
+        %107 = OpINotEqual %21 %106 %14
+        %108 = OpCompositeConstruct %102 %107 %107
+               OpStore %104 %108
+               OpBranch %24
+         %24 = OpLabel
+        %109 = OpAccessChain %74 %104 %18
+        %110 = OpLoad %21 %109
+        %112 = OpLogicalOr %21 %110 %111
+        %113 = OpAccessChain %74 %104 %14
+               OpStore %113 %112
+        %118 = OpAccessChain %57 %116 %14
+               OpStore %118 %117
+        %119 = OpAccessChain %57 %116 %14
+        %120 = OpLoad %50 %119
+        %121 = OpAccessChain %57 %53 %18
+               OpStore %121 %120
+        %125 = OpAccessChain %57 %116 %14
+        %126 = OpLoad %50 %125
+        %127 = OpAccessChain %57 %53 %18
+        %128 = OpLoad %50 %127
+        %129 = OpIAdd %50 %126 %128
+        %130 = OpAccessChain %57 %124 %65
+               OpStore %130 %129
+        %134 = OpAccessChain %57 %116 %14
+        %135 = OpLoad %50 %134
+        %136 = OpBitcast %13 %135
+        %137 = OpAccessChain %67 %133 %14
+               OpStore %137 %136
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  TransformationConstructComposite make_vec2(7, {17, 11}, 100, 1, 200);
+  // Bad: not enough data for a vec2
+  TransformationConstructComposite make_vec2_bad(7, {11}, 100, 1, 200);
+  ASSERT_TRUE(make_vec2.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_vec3(25, {12, 32}, 35, 0, 201);
+  // Bad: too much data for a vec3
+  TransformationConstructComposite make_vec3_bad(25, {12, 32, 32}, 35, 0, 201);
+  ASSERT_TRUE(make_vec3.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_vec4(44, {32, 32, 10, 11}, 75, 0, 202);
+  // Bad: id 48 is not available at the insertion points
+  TransformationConstructComposite make_vec4_bad(44, {48, 32, 10, 11}, 75, 0,
+                                                 202);
+  ASSERT_TRUE(make_vec4.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_ivec2(51, {126, 120}, 128, 0, 203);
+  // Bad: if 128 is not available at the instruction that defines 128
+  TransformationConstructComposite make_ivec2_bad(51, {128, 120}, 128, 0, 203);
+  ASSERT_TRUE(make_ivec2.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_ivec3(114, {56, 117, 56}, 66, 1, 204);
+  // Bad because 1300 is not an id
+  TransformationConstructComposite make_ivec3_bad(114, {56, 117, 1300}, 66, 1,
+                                                  204);
+  ASSERT_TRUE(make_ivec3.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_ivec4(122, {56, 117, 117, 117}, 66, 0,
+                                              205);
+  // Bad because 86 is the wrong type.
+  TransformationConstructComposite make_ivec4_bad(86, {56, 117, 117, 117}, 66,
+                                                  0, 205);
+  ASSERT_TRUE(make_ivec4.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_uvec2(86, {18, 38}, 133, 2, 206);
+  TransformationConstructComposite make_uvec2_bad(86, {18, 38}, 133, 200, 206);
+  ASSERT_TRUE(make_uvec2.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_uvec3(59, {14, 18, 136}, 137, 2, 207);
+  // Bad because 1300 is not an id
+  TransformationConstructComposite make_uvec3_bad(59, {14, 18, 1300}, 137, 2,
+                                                  207);
+  ASSERT_TRUE(make_uvec3.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_uvec4(131, {14, 18, 136, 136}, 137, 0,
+                                              208);
+  // Bad because 86 is the wrong type.
+  TransformationConstructComposite make_uvec4_bad(86, {14, 18, 136, 136}, 137,
+                                                  0, 208);
+  ASSERT_TRUE(make_uvec4.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_bvec2(102,
+                                              {
+                                                  111,
+                                                  41,
+                                              },
+                                              75, 0, 209);
+  // Bad because 0 is not a valid base instruction id
+  TransformationConstructComposite make_bvec2_bad(102,
+                                                  {
+                                                      111,
+                                                      41,
+                                                  },
+                                                  0, 0, 209);
+  ASSERT_TRUE(make_bvec2.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_bvec3(93, {108, 73}, 108, 1, 210);
+  // Bad because there are too many components for a bvec3
+  TransformationConstructComposite make_bvec3_bad(93, {108, 108}, 108, 1, 210);
+  ASSERT_TRUE(make_bvec3.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  TransformationConstructComposite make_bvec4(70, {108, 108}, 108, 3, 211);
+  // Bad because 21 is a type, not a result id
+  TransformationConstructComposite make_bvec4_bad(70, {21, 108}, 108, 3, 211);
+  ASSERT_TRUE(make_bvec4.IsApplicable(context.get(), fact_manager));
+  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}));
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "v2"
+               OpName %27 "v3"
+               OpName %46 "v4"
+               OpName %53 "iv2"
+               OpName %61 "uv3"
+               OpName %72 "bv4"
+               OpName %88 "uv2"
+               OpName %95 "bv3"
+               OpName %104 "bv2"
+               OpName %116 "iv3"
+               OpName %124 "iv4"
+               OpName %133 "uv4"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 2
+          %8 = OpTypePointer Function %7
+         %10 = OpConstant %6 1
+         %11 = OpConstant %6 2
+         %12 = OpConstantComposite %7 %10 %11
+         %13 = OpTypeInt 32 0
+         %14 = OpConstant %13 0
+         %15 = OpTypePointer Function %6
+         %18 = OpConstant %13 1
+         %21 = OpTypeBool
+         %25 = OpTypeVector %6 3
+         %26 = OpTypePointer Function %25
+         %33 = OpConstant %6 3
+         %34 = OpConstant %6 -0.756802499
+         %38 = OpConstant %13 2
+         %44 = OpTypeVector %6 4
+         %45 = OpTypePointer Function %44
+         %50 = OpTypeInt 32 1
+         %51 = OpTypeVector %50 2
+         %52 = OpTypePointer Function %51
+         %57 = OpTypePointer Function %50
+         %59 = OpTypeVector %13 3
+         %60 = OpTypePointer Function %59
+         %65 = OpConstant %13 3
+         %67 = OpTypePointer Function %13
+         %70 = OpTypeVector %21 4
+         %71 = OpTypePointer Function %70
+         %73 = OpConstantTrue %21
+         %74 = OpTypePointer Function %21
+         %86 = OpTypeVector %13 2
+         %87 = OpTypePointer Function %86
+         %93 = OpTypeVector %21 3
+         %94 = OpTypePointer Function %93
+        %102 = OpTypeVector %21 2
+        %103 = OpTypePointer Function %102
+        %111 = OpConstantFalse %21
+        %114 = OpTypeVector %50 3
+        %115 = OpTypePointer Function %114
+        %117 = OpConstant %50 3
+        %122 = OpTypeVector %50 4
+        %123 = OpTypePointer Function %122
+        %131 = OpTypeVector %13 4
+        %132 = OpTypePointer Function %131
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %9 = OpVariable %8 Function
+         %27 = OpVariable %26 Function
+         %46 = OpVariable %45 Function
+         %53 = OpVariable %52 Function
+         %61 = OpVariable %60 Function
+         %72 = OpVariable %71 Function
+         %88 = OpVariable %87 Function
+         %95 = OpVariable %94 Function
+        %104 = OpVariable %103 Function
+        %116 = OpVariable %115 Function
+        %124 = OpVariable %123 Function
+        %133 = OpVariable %132 Function
+               OpStore %9 %12
+        %206 = OpCompositeConstruct %86 %18 %38
+         %16 = OpAccessChain %15 %9 %14
+         %17 = OpLoad %6 %16
+         %19 = OpAccessChain %15 %9 %18
+         %20 = OpLoad %6 %19
+         %22 = OpFOrdGreaterThan %21 %17 %20
+               OpSelectionMerge %24 None
+               OpBranchConditional %22 %23 %101
+         %23 = OpLabel
+         %28 = OpAccessChain %15 %9 %14
+         %29 = OpLoad %6 %28
+         %30 = OpAccessChain %15 %9 %18
+         %31 = OpLoad %6 %30
+         %32 = OpFAdd %6 %29 %31
+        %201 = OpCompositeConstruct %25 %12 %32
+         %35 = OpCompositeConstruct %25 %32 %33 %34
+               OpStore %27 %35
+         %36 = OpAccessChain %15 %27 %14
+         %37 = OpLoad %6 %36
+         %39 = OpAccessChain %15 %27 %38
+         %40 = OpLoad %6 %39
+         %41 = OpFOrdLessThan %21 %37 %40
+               OpSelectionMerge %43 None
+               OpBranchConditional %41 %42 %69
+         %42 = OpLabel
+         %47 = OpAccessChain %15 %9 %18
+         %48 = OpLoad %6 %47
+         %49 = OpAccessChain %15 %46 %14
+               OpStore %49 %48
+         %54 = OpAccessChain %15 %27 %38
+         %55 = OpLoad %6 %54
+         %56 = OpConvertFToS %50 %55
+         %58 = OpAccessChain %57 %53 %14
+               OpStore %58 %56
+         %62 = OpAccessChain %15 %46 %14
+         %63 = OpLoad %6 %62
+         %64 = OpConvertFToU %13 %63
+        %205 = OpCompositeConstruct %122 %56 %117 %117 %117
+         %66 = OpIAdd %13 %64 %65
+        %204 = OpCompositeConstruct %114 %56 %117 %56
+         %68 = OpAccessChain %67 %61 %14
+               OpStore %68 %66
+               OpBranch %43
+         %69 = OpLabel
+        %202 = OpCompositeConstruct %44 %32 %32 %10 %11
+        %209 = OpCompositeConstruct %102 %111 %41
+         %75 = OpAccessChain %74 %72 %14
+               OpStore %75 %73
+         %76 = OpAccessChain %74 %72 %14
+         %77 = OpLoad %21 %76
+         %78 = OpLogicalNot %21 %77
+         %79 = OpAccessChain %74 %72 %18
+               OpStore %79 %78
+         %80 = OpAccessChain %74 %72 %14
+         %81 = OpLoad %21 %80
+         %82 = OpAccessChain %74 %72 %18
+         %83 = OpLoad %21 %82
+         %84 = OpLogicalAnd %21 %81 %83
+         %85 = OpAccessChain %74 %72 %38
+               OpStore %85 %84
+         %89 = OpAccessChain %67 %88 %14
+         %90 = OpLoad %13 %89
+         %91 = OpINotEqual %21 %90 %14
+         %92 = OpAccessChain %74 %72 %65
+               OpStore %92 %91
+               OpBranch %43
+         %43 = OpLabel
+         %96 = OpLoad %70 %72
+         %97 = OpCompositeExtract %21 %96 0
+         %98 = OpCompositeExtract %21 %96 1
+         %99 = OpCompositeExtract %21 %96 2
+        %100 = OpCompositeConstruct %93 %97 %98 %99
+        %200 = OpCompositeConstruct %7 %17 %11
+               OpStore %95 %100
+               OpBranch %24
+        %101 = OpLabel
+        %105 = OpAccessChain %67 %88 %14
+        %106 = OpLoad %13 %105
+        %107 = OpINotEqual %21 %106 %14
+        %108 = OpCompositeConstruct %102 %107 %107
+        %210 = OpCompositeConstruct %93 %108 %73
+               OpStore %104 %108
+        %211 = OpCompositeConstruct %70 %108 %108
+               OpBranch %24
+         %24 = OpLabel
+        %109 = OpAccessChain %74 %104 %18
+        %110 = OpLoad %21 %109
+        %112 = OpLogicalOr %21 %110 %111
+        %113 = OpAccessChain %74 %104 %14
+               OpStore %113 %112
+        %118 = OpAccessChain %57 %116 %14
+               OpStore %118 %117
+        %119 = OpAccessChain %57 %116 %14
+        %120 = OpLoad %50 %119
+        %121 = OpAccessChain %57 %53 %18
+               OpStore %121 %120
+        %125 = OpAccessChain %57 %116 %14
+        %126 = OpLoad %50 %125
+        %127 = OpAccessChain %57 %53 %18
+        %203 = OpCompositeConstruct %51 %126 %120
+        %128 = OpLoad %50 %127
+        %129 = OpIAdd %50 %126 %128
+        %130 = OpAccessChain %57 %124 %65
+               OpStore %130 %129
+        %134 = OpAccessChain %57 %116 %14
+        %135 = OpLoad %50 %134
+        %136 = OpBitcast %13 %135
+        %208 = OpCompositeConstruct %131 %14 %18 %136 %136
+        %137 = OpAccessChain %67 %133 %14
+               OpStore %137 %136
+        %207 = OpCompositeConstruct %59 %14 %18 %136
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/test/fuzz/transformation_replace_id_with_synonym_test.cpp b/test/fuzz/transformation_replace_id_with_synonym_test.cpp
index 479eb16..cd79bbe 100644
--- a/test/fuzz/transformation_replace_id_with_synonym_test.cpp
+++ b/test/fuzz/transformation_replace_id_with_synonym_test.cpp
@@ -185,10 +185,14 @@
                OpFunctionEnd
 )";
 
-protobufs::Fact MakeFact(uint32_t id, uint32_t copy_id) {
+protobufs::Fact MakeSynonymFact(uint32_t id, uint32_t synonym_object,
+                                std::vector<uint32_t> indices = {}) {
   protobufs::FactIdSynonym id_synonym_fact;
   id_synonym_fact.set_id(id);
-  id_synonym_fact.mutable_data_descriptor()->set_object(copy_id);
+  id_synonym_fact.mutable_data_descriptor()->set_object(synonym_object);
+  for (auto index : indices) {
+    id_synonym_fact.mutable_data_descriptor()->add_index(index);
+  }
   protobufs::Fact result;
   *result.mutable_id_synonym_fact() = id_synonym_fact;
   return result;
@@ -196,17 +200,17 @@
 
 // Equips the fact manager with synonym facts for the above shader.
 void SetUpIdSynonyms(FactManager* fact_manager, opt::IRContext* context) {
-  fact_manager->AddFact(MakeFact(15, 200), context);
-  fact_manager->AddFact(MakeFact(15, 201), context);
-  fact_manager->AddFact(MakeFact(15, 202), context);
-  fact_manager->AddFact(MakeFact(55, 203), context);
-  fact_manager->AddFact(MakeFact(54, 204), context);
-  fact_manager->AddFact(MakeFact(74, 205), context);
-  fact_manager->AddFact(MakeFact(78, 206), context);
-  fact_manager->AddFact(MakeFact(84, 207), context);
-  fact_manager->AddFact(MakeFact(33, 208), context);
-  fact_manager->AddFact(MakeFact(12, 209), context);
-  fact_manager->AddFact(MakeFact(19, 210), context);
+  fact_manager->AddFact(MakeSynonymFact(15, 200), context);
+  fact_manager->AddFact(MakeSynonymFact(15, 201), context);
+  fact_manager->AddFact(MakeSynonymFact(15, 202), context);
+  fact_manager->AddFact(MakeSynonymFact(55, 203), context);
+  fact_manager->AddFact(MakeSynonymFact(54, 204), context);
+  fact_manager->AddFact(MakeSynonymFact(74, 205), context);
+  fact_manager->AddFact(MakeSynonymFact(78, 206), context);
+  fact_manager->AddFact(MakeSynonymFact(84, 207), context);
+  fact_manager->AddFact(MakeSynonymFact(33, 208), context);
+  fact_manager->AddFact(MakeSynonymFact(12, 209), context);
+  fact_manager->AddFact(MakeSynonymFact(19, 210), context);
 }
 
 TEST(TransformationReplaceIdWithSynonymTest, IllegalTransformations) {
@@ -493,8 +497,8 @@
 
   FactManager fact_manager;
 
-  fact_manager.AddFact(MakeFact(10, 100), context.get());
-  fact_manager.AddFact(MakeFact(8, 101), context.get());
+  fact_manager.AddFact(MakeSynonymFact(10, 100), context.get());
+  fact_manager.AddFact(MakeSynonymFact(8, 101), context.get());
 
   // Replace %10 with %100 in:
   // %11 = OpLoad %6 %10
@@ -622,7 +626,7 @@
 
   FactManager fact_manager;
 
-  fact_manager.AddFact(MakeFact(14, 100), context.get());
+  fact_manager.AddFact(MakeSynonymFact(14, 100), context.get());
 
   // Replace %14 with %100 in:
   // %16 = OpFunctionCall %2 %10 %14
@@ -785,19 +789,19 @@
 
   // Add synonym facts corresponding to the OpCopyObject operations that have
   // been applied to all constants in the module.
-  fact_manager.AddFact(MakeFact(16, 100), context.get());
-  fact_manager.AddFact(MakeFact(21, 101), context.get());
-  fact_manager.AddFact(MakeFact(17, 102), context.get());
-  fact_manager.AddFact(MakeFact(57, 103), context.get());
-  fact_manager.AddFact(MakeFact(18, 104), context.get());
-  fact_manager.AddFact(MakeFact(40, 105), context.get());
-  fact_manager.AddFact(MakeFact(32, 106), context.get());
-  fact_manager.AddFact(MakeFact(43, 107), context.get());
-  fact_manager.AddFact(MakeFact(55, 108), context.get());
-  fact_manager.AddFact(MakeFact(8, 109), context.get());
-  fact_manager.AddFact(MakeFact(47, 110), context.get());
-  fact_manager.AddFact(MakeFact(28, 111), context.get());
-  fact_manager.AddFact(MakeFact(45, 112), context.get());
+  fact_manager.AddFact(MakeSynonymFact(16, 100), context.get());
+  fact_manager.AddFact(MakeSynonymFact(21, 101), context.get());
+  fact_manager.AddFact(MakeSynonymFact(17, 102), context.get());
+  fact_manager.AddFact(MakeSynonymFact(57, 103), context.get());
+  fact_manager.AddFact(MakeSynonymFact(18, 104), context.get());
+  fact_manager.AddFact(MakeSynonymFact(40, 105), context.get());
+  fact_manager.AddFact(MakeSynonymFact(32, 106), context.get());
+  fact_manager.AddFact(MakeSynonymFact(43, 107), context.get());
+  fact_manager.AddFact(MakeSynonymFact(55, 108), context.get());
+  fact_manager.AddFact(MakeSynonymFact(8, 109), context.get());
+  fact_manager.AddFact(MakeSynonymFact(47, 110), context.get());
+  fact_manager.AddFact(MakeSynonymFact(28, 111), context.get());
+  fact_manager.AddFact(MakeSynonymFact(45, 112), context.get());
 
   // Replacements of the form %16 -> %100
 
@@ -1171,6 +1175,965 @@
   ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
 }
 
+TEST(TransformationReplaceIdWithSynonymTest, ArrayCompositeSynonyms) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %11 "A"
+               OpName %20 "B"
+               OpName %31 "g"
+               OpName %35 "h"
+               OpDecorate %11 RelaxedPrecision
+               OpDecorate %22 RelaxedPrecision
+               OpDecorate %27 RelaxedPrecision
+               OpDecorate %35 RelaxedPrecision
+               OpDecorate %36 RelaxedPrecision
+               OpDecorate %40 RelaxedPrecision
+               OpDecorate %41 RelaxedPrecision
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeInt 32 0
+          %8 = OpConstant %7 3
+          %9 = OpTypeArray %6 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpConstant %6 0
+         %13 = OpConstant %6 3
+         %14 = OpTypePointer Function %6
+         %16 = OpTypeFloat 32
+         %17 = OpConstant %7 4
+         %18 = OpTypeArray %16 %17
+         %19 = OpTypePointer Function %18
+         %24 = OpTypePointer Function %16
+         %28 = OpConstant %16 42
+         %30 = OpConstant %6 2
+         %34 = OpConstant %6 1
+         %38 = OpConstant %6 42
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %20 = OpVariable %19 Function
+         %31 = OpVariable %24 Function
+         %35 = OpVariable %14 Function
+         %15 = OpAccessChain %14 %11 %12
+         %21 = OpAccessChain %14 %11 %12
+         %22 = OpLoad %6 %21
+        %100 = OpCompositeConstruct %9 %12 %13 %22
+               OpStore %15 %13
+         %23 = OpConvertSToF %16 %22
+         %25 = OpAccessChain %24 %20 %12
+               OpStore %25 %23
+         %26 = OpAccessChain %14 %11 %12
+         %27 = OpLoad %6 %26
+         %29 = OpAccessChain %24 %20 %27
+               OpStore %29 %28
+         %32 = OpLoad %16 %31
+        %101 = OpCompositeConstruct %18 %28 %23 %32 %23
+         %50 = OpCopyObject %16 %23
+         %51 = OpCopyObject %16 %23
+         %33 = OpAccessChain %24 %20 %30
+               OpStore %33 %28
+               OpStore %33 %32
+         %36 = OpLoad %6 %35
+         %37 = OpAccessChain %14 %11 %34
+               OpStore %37 %36
+         %39 = OpAccessChain %14 %11 %12
+         %40 = OpLoad %6 %39
+         %41 = OpIAdd %6 %38 %40
+         %42 = OpAccessChain %14 %11 %30
+               OpStore %42 %41
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  fact_manager.AddFact(MakeSynonymFact(12, 100, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(13, 100, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(22, 100, {2}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(28, 101, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(23, 101, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(32, 101, {2}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(23, 101, {3}), context.get());
+
+  // Replace %12 with %100[0] in '%25 = OpAccessChain %24 %20 %12'
+  auto good_replacement_1 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(12, SpvOpAccessChain, 1, 25, 0),
+      MakeDataDescriptor(100, {0}), 102);
+  // Bad: id already in use
+  auto bad_replacement_1 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(12, SpvOpAccessChain, 1, 25, 0),
+      MakeDataDescriptor(100, {0}), 25);
+  ASSERT_TRUE(good_replacement_1.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_1.IsApplicable(context.get(), fact_manager));
+  good_replacement_1.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %13 with %100[1] in 'OpStore %15 %13'
+  auto good_replacement_2 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(13, SpvOpStore, 1, 100, 0),
+      MakeDataDescriptor(100, {1}), 103);
+  // Bad: too many indices
+  auto bad_replacement_2 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(13, SpvOpStore, 1, 100, 0),
+      MakeDataDescriptor(100, {1, 0}), 103);
+  ASSERT_TRUE(good_replacement_2.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_2.IsApplicable(context.get(), fact_manager));
+  good_replacement_2.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %22 with %100[2] in '%23 = OpConvertSToF %16 %22'
+  auto good_replacement_3 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(22, SpvOpConvertSToF, 0, 23, 0),
+      MakeDataDescriptor(100, {2}), 104);
+  // Bad: wrong input operand index
+  auto bad_replacement_3 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(22, SpvOpConvertSToF, 1, 23, 0),
+      MakeDataDescriptor(100, {2}), 104);
+  ASSERT_TRUE(good_replacement_3.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_3.IsApplicable(context.get(), fact_manager));
+  good_replacement_3.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %28 with %101[0] in 'OpStore %33 %28'
+  auto good_replacement_4 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(28, SpvOpStore, 1, 33, 0),
+      MakeDataDescriptor(101, {0}), 105);
+  // Bad: id use descriptor does not identify an appropriate instruction
+  auto bad_replacement_4 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(28, SpvOpCopyObject, 1, 33, 0),
+      MakeDataDescriptor(101, {0}), 105);
+  ASSERT_TRUE(good_replacement_4.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_4.IsApplicable(context.get(), fact_manager));
+  good_replacement_4.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %23 with %101[1] in '%50 = OpCopyObject %16 %23'
+  auto good_replacement_5 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(23, SpvOpCopyObject, 0, 50, 0),
+      MakeDataDescriptor(101, {1}), 106);
+  // Bad: wrong synonym fact being used
+  auto bad_replacement_5 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(23, SpvOpCopyObject, 0, 50, 0),
+      MakeDataDescriptor(101, {0}), 106);
+  ASSERT_TRUE(good_replacement_5.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_5.IsApplicable(context.get(), fact_manager));
+  good_replacement_5.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %32 with %101[2] in 'OpStore %33 %32'
+  auto good_replacement_6 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(32, SpvOpStore, 1, 33, 1),
+      MakeDataDescriptor(101, {2}), 107);
+  // Bad: id 1001 does not exist
+  auto bad_replacement_6 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(32, SpvOpStore, 1, 33, 1),
+      MakeDataDescriptor(1001, {2}), 107);
+  ASSERT_TRUE(good_replacement_6.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_6.IsApplicable(context.get(), fact_manager));
+  good_replacement_6.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %23 with %101[3] in '%51 = OpCopyObject %16 %23'
+  auto good_replacement_7 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(23, SpvOpCopyObject, 0, 51, 0),
+      MakeDataDescriptor(101, {3}), 108);
+  // Bad: id 0 is invalid
+  auto bad_replacement_7 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(0, SpvOpCopyObject, 0, 51, 0),
+      MakeDataDescriptor(101, {3}), 108);
+  ASSERT_TRUE(good_replacement_7.IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(bad_replacement_7.IsApplicable(context.get(), fact_manager));
+  good_replacement_7.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  const std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %11 "A"
+               OpName %20 "B"
+               OpName %31 "g"
+               OpName %35 "h"
+               OpDecorate %11 RelaxedPrecision
+               OpDecorate %22 RelaxedPrecision
+               OpDecorate %27 RelaxedPrecision
+               OpDecorate %35 RelaxedPrecision
+               OpDecorate %36 RelaxedPrecision
+               OpDecorate %40 RelaxedPrecision
+               OpDecorate %41 RelaxedPrecision
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeInt 32 0
+          %8 = OpConstant %7 3
+          %9 = OpTypeArray %6 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpConstant %6 0
+         %13 = OpConstant %6 3
+         %14 = OpTypePointer Function %6
+         %16 = OpTypeFloat 32
+         %17 = OpConstant %7 4
+         %18 = OpTypeArray %16 %17
+         %19 = OpTypePointer Function %18
+         %24 = OpTypePointer Function %16
+         %28 = OpConstant %16 42
+         %30 = OpConstant %6 2
+         %34 = OpConstant %6 1
+         %38 = OpConstant %6 42
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %20 = OpVariable %19 Function
+         %31 = OpVariable %24 Function
+         %35 = OpVariable %14 Function
+         %15 = OpAccessChain %14 %11 %12
+         %21 = OpAccessChain %14 %11 %12
+         %22 = OpLoad %6 %21
+        %100 = OpCompositeConstruct %9 %12 %13 %22
+        %103 = OpCompositeExtract %6 %100 1
+               OpStore %15 %103
+        %104 = OpCompositeExtract %6 %100 2
+         %23 = OpConvertSToF %16 %104
+        %102 = OpCompositeExtract %6 %100 0
+         %25 = OpAccessChain %24 %20 %102
+               OpStore %25 %23
+         %26 = OpAccessChain %14 %11 %12
+         %27 = OpLoad %6 %26
+         %29 = OpAccessChain %24 %20 %27
+               OpStore %29 %28
+         %32 = OpLoad %16 %31
+        %101 = OpCompositeConstruct %18 %28 %23 %32 %23
+        %106 = OpCompositeExtract %16 %101 1
+         %50 = OpCopyObject %16 %106
+        %108 = OpCompositeExtract %16 %101 3
+         %51 = OpCopyObject %16 %108
+         %33 = OpAccessChain %24 %20 %30
+        %105 = OpCompositeExtract %16 %101 0
+               OpStore %33 %105
+        %107 = OpCompositeExtract %16 %101 2
+               OpStore %33 %107
+         %36 = OpLoad %6 %35
+         %37 = OpAccessChain %14 %11 %34
+               OpStore %37 %36
+         %39 = OpAccessChain %14 %11 %12
+         %40 = OpLoad %6 %39
+         %41 = OpIAdd %6 %38 %40
+         %42 = OpAccessChain %14 %11 %30
+               OpStore %42 %41
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationReplaceIdWithSynonymTest, MatrixCompositeSynonyms) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %10 "m"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 4
+         %50 = OpUndef %7
+          %8 = OpTypeMatrix %7 3
+          %9 = OpTypePointer Function %8
+         %11 = OpTypeInt 32 1
+         %12 = OpConstant %11 0
+         %13 = OpConstant %6 1
+         %14 = OpConstantComposite %7 %13 %13 %13 %13
+         %15 = OpTypePointer Function %7
+         %17 = OpConstant %11 1
+         %18 = OpConstant %6 2
+         %19 = OpConstantComposite %7 %18 %18 %18 %18
+         %21 = OpConstant %11 2
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %10 = OpVariable %9 Function
+         %16 = OpAccessChain %15 %10 %12
+               OpStore %16 %14
+         %20 = OpAccessChain %15 %10 %17
+               OpStore %20 %19
+         %22 = OpAccessChain %15 %10 %12
+         %23 = OpLoad %7 %22
+         %24 = OpAccessChain %15 %10 %17
+         %25 = OpLoad %7 %24
+        %100 = OpCompositeConstruct %8 %23 %25 %50
+         %26 = OpFAdd %7 %23 %25
+         %27 = OpAccessChain %15 %10 %21
+               OpStore %27 %26
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  fact_manager.AddFact(MakeSynonymFact(23, 100, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(25, 100, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(50, 100, {2}), context.get());
+
+  // Replace %23 with %100[0] in '%26 = OpFAdd %7 %23 %25'
+  auto replacement_1 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(23, SpvOpFAdd, 0, 26, 0),
+      MakeDataDescriptor(100, {0}), 101);
+  ASSERT_TRUE(replacement_1.IsApplicable(context.get(), fact_manager));
+  replacement_1.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %25 with %100[1] in '%26 = OpFAdd %7 %23 %25'
+  auto replacement_2 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(25, SpvOpFAdd, 1, 26, 0),
+      MakeDataDescriptor(100, {1}), 102);
+  ASSERT_TRUE(replacement_2.IsApplicable(context.get(), fact_manager));
+  replacement_2.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  const std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %10 "m"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypeVector %6 4
+         %50 = OpUndef %7
+          %8 = OpTypeMatrix %7 3
+          %9 = OpTypePointer Function %8
+         %11 = OpTypeInt 32 1
+         %12 = OpConstant %11 0
+         %13 = OpConstant %6 1
+         %14 = OpConstantComposite %7 %13 %13 %13 %13
+         %15 = OpTypePointer Function %7
+         %17 = OpConstant %11 1
+         %18 = OpConstant %6 2
+         %19 = OpConstantComposite %7 %18 %18 %18 %18
+         %21 = OpConstant %11 2
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %10 = OpVariable %9 Function
+         %16 = OpAccessChain %15 %10 %12
+               OpStore %16 %14
+         %20 = OpAccessChain %15 %10 %17
+               OpStore %20 %19
+         %22 = OpAccessChain %15 %10 %12
+         %23 = OpLoad %7 %22
+         %24 = OpAccessChain %15 %10 %17
+         %25 = OpLoad %7 %24
+        %100 = OpCompositeConstruct %8 %23 %25 %50
+        %101 = OpCompositeExtract %7 %100 0
+        %102 = OpCompositeExtract %7 %100 1
+         %26 = OpFAdd %7 %101 %102
+         %27 = OpAccessChain %15 %10 %21
+               OpStore %27 %26
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationReplaceIdWithSynonymTest, StructCompositeSynonyms) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "Inner"
+               OpMemberName %9 0 "a"
+               OpMemberName %9 1 "b"
+               OpName %11 "i1"
+               OpName %17 "i2"
+               OpName %31 "Point"
+               OpMemberName %31 0 "x"
+               OpMemberName %31 1 "y"
+               OpMemberName %31 2 "z"
+               OpName %32 "Outer"
+               OpMemberName %32 0 "c"
+               OpMemberName %32 1 "d"
+               OpName %34 "o1"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeFloat 32
+          %8 = OpTypeVector %7 2
+          %9 = OpTypeStruct %6 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpConstant %6 1
+         %13 = OpConstant %7 2
+         %14 = OpConstant %7 3
+         %15 = OpConstantComposite %8 %13 %14
+         %16 = OpConstantComposite %9 %12 %15
+         %18 = OpConstant %6 0
+         %19 = OpTypePointer Function %6
+         %24 = OpTypePointer Function %8
+         %27 = OpConstant %7 4
+         %31 = OpTypeStruct %7 %7 %7
+         %32 = OpTypeStruct %9 %31
+         %33 = OpTypePointer Function %32
+         %36 = OpConstant %7 10
+         %37 = OpTypeInt 32 0
+         %38 = OpConstant %37 0
+         %39 = OpTypePointer Function %7
+         %42 = OpConstant %37 1
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %17 = OpVariable %10 Function
+         %34 = OpVariable %33 Function
+        %101 = OpCompositeConstruct %31 %27 %36 %27
+               OpStore %11 %16
+         %20 = OpAccessChain %19 %11 %18
+         %21 = OpLoad %6 %20
+         %22 = OpIAdd %6 %21 %12
+        %102 = OpCompositeConstruct %9 %22 %15
+         %23 = OpAccessChain %19 %17 %18
+               OpStore %23 %22
+         %25 = OpAccessChain %24 %17 %12
+         %26 = OpLoad %8 %25
+         %28 = OpCompositeConstruct %8 %27 %27
+         %29 = OpFAdd %8 %26 %28
+         %30 = OpAccessChain %24 %17 %12
+               OpStore %30 %29
+         %35 = OpLoad %9 %11
+         %40 = OpAccessChain %39 %11 %12 %38
+         %41 = OpLoad %7 %40
+         %43 = OpAccessChain %39 %11 %12 %42
+         %44 = OpLoad %7 %43
+         %45 = OpCompositeConstruct %31 %36 %41 %44
+        %100 = OpCompositeConstruct %32 %16 %45
+         %46 = OpCompositeConstruct %32 %35 %45
+               OpStore %34 %46
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  fact_manager.AddFact(MakeSynonymFact(16, 100, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(45, 100, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(27, 101, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(36, 101, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(27, 101, {2}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(22, 102, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(15, 102, {1}), context.get());
+
+  // Replace %45 with %100[1] in '%46 = OpCompositeConstruct %32 %35 %45'
+  auto replacement_1 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(45, SpvOpCompositeConstruct, 1, 46,
+                                          0),
+      MakeDataDescriptor(100, {1}), 201);
+  ASSERT_TRUE(replacement_1.IsApplicable(context.get(), fact_manager));
+  replacement_1.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace second occurrence of %27 with %101[0] in '%28 =
+  // OpCompositeConstruct %8 %27 %27'
+  auto replacement_2 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(27, SpvOpCompositeConstruct, 1, 28,
+                                          0),
+      MakeDataDescriptor(101, {0}), 202);
+  ASSERT_TRUE(replacement_2.IsApplicable(context.get(), fact_manager));
+  replacement_2.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %36 with %101[1] in '%45 = OpCompositeConstruct %31 %36 %41 %44'
+  auto replacement_3 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(36, SpvOpCompositeConstruct, 0, 45,
+                                          0),
+      MakeDataDescriptor(101, {1}), 203);
+  ASSERT_TRUE(replacement_3.IsApplicable(context.get(), fact_manager));
+  replacement_3.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace first occurrence of %27 with %101[2] in '%28 = OpCompositeConstruct
+  // %8 %27 %27'
+  auto replacement_4 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(27, SpvOpCompositeConstruct, 0, 28,
+                                          0),
+      MakeDataDescriptor(101, {2}), 204);
+  ASSERT_TRUE(replacement_4.IsApplicable(context.get(), fact_manager));
+  replacement_4.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %22 with %102[0] in 'OpStore %23 %22'
+  auto replacement_5 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(22, SpvOpStore, 1, 23, 0),
+      MakeDataDescriptor(102, {0}), 205);
+  ASSERT_TRUE(replacement_5.IsApplicable(context.get(), fact_manager));
+  replacement_5.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  const std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %9 "Inner"
+               OpMemberName %9 0 "a"
+               OpMemberName %9 1 "b"
+               OpName %11 "i1"
+               OpName %17 "i2"
+               OpName %31 "Point"
+               OpMemberName %31 0 "x"
+               OpMemberName %31 1 "y"
+               OpMemberName %31 2 "z"
+               OpName %32 "Outer"
+               OpMemberName %32 0 "c"
+               OpMemberName %32 1 "d"
+               OpName %34 "o1"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeFloat 32
+          %8 = OpTypeVector %7 2
+          %9 = OpTypeStruct %6 %8
+         %10 = OpTypePointer Function %9
+         %12 = OpConstant %6 1
+         %13 = OpConstant %7 2
+         %14 = OpConstant %7 3
+         %15 = OpConstantComposite %8 %13 %14
+         %16 = OpConstantComposite %9 %12 %15
+         %18 = OpConstant %6 0
+         %19 = OpTypePointer Function %6
+         %24 = OpTypePointer Function %8
+         %27 = OpConstant %7 4
+         %31 = OpTypeStruct %7 %7 %7
+         %32 = OpTypeStruct %9 %31
+         %33 = OpTypePointer Function %32
+         %36 = OpConstant %7 10
+         %37 = OpTypeInt 32 0
+         %38 = OpConstant %37 0
+         %39 = OpTypePointer Function %7
+         %42 = OpConstant %37 1
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %11 = OpVariable %10 Function
+         %17 = OpVariable %10 Function
+         %34 = OpVariable %33 Function
+        %101 = OpCompositeConstruct %31 %27 %36 %27
+               OpStore %11 %16
+         %20 = OpAccessChain %19 %11 %18
+         %21 = OpLoad %6 %20
+         %22 = OpIAdd %6 %21 %12
+        %102 = OpCompositeConstruct %9 %22 %15
+         %23 = OpAccessChain %19 %17 %18
+        %205 = OpCompositeExtract %6 %102 0
+               OpStore %23 %205
+         %25 = OpAccessChain %24 %17 %12
+         %26 = OpLoad %8 %25
+        %202 = OpCompositeExtract %7 %101 0
+        %204 = OpCompositeExtract %7 %101 2
+         %28 = OpCompositeConstruct %8 %204 %202
+         %29 = OpFAdd %8 %26 %28
+         %30 = OpAccessChain %24 %17 %12
+               OpStore %30 %29
+         %35 = OpLoad %9 %11
+         %40 = OpAccessChain %39 %11 %12 %38
+         %41 = OpLoad %7 %40
+         %43 = OpAccessChain %39 %11 %12 %42
+         %44 = OpLoad %7 %43
+        %203 = OpCompositeExtract %7 %101 1
+         %45 = OpCompositeConstruct %31 %203 %41 %44
+        %100 = OpCompositeConstruct %32 %16 %45
+        %201 = OpCompositeExtract %31 %100 1
+         %46 = OpCompositeConstruct %32 %35 %201
+               OpStore %34 %46
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationReplaceIdWithSynonymTest, VectorCompositeSynonyms) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %8 "f"
+               OpName %12 "v2"
+               OpName %18 "v3"
+               OpName %23 "v4"
+               OpName %32 "b"
+               OpName %36 "bv2"
+               OpName %41 "bv3"
+               OpName %50 "bv4"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 42
+         %10 = OpTypeVector %6 2
+         %11 = OpTypePointer Function %10
+         %16 = OpTypeVector %6 3
+         %17 = OpTypePointer Function %16
+         %21 = OpTypeVector %6 4
+         %22 = OpTypePointer Function %21
+         %30 = OpTypeBool
+         %31 = OpTypePointer Function %30
+         %33 = OpConstantFalse %30
+         %34 = OpTypeVector %30 2
+         %35 = OpTypePointer Function %34
+         %37 = OpConstantTrue %30
+         %38 = OpConstantComposite %34 %37 %37
+         %39 = OpTypeVector %30 3
+         %40 = OpTypePointer Function %39
+         %48 = OpTypeVector %30 4
+         %49 = OpTypePointer Function %48
+         %51 = OpTypeInt 32 0
+         %52 = OpConstant %51 2
+         %55 = OpConstant %6 0
+         %57 = OpConstant %51 1
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %12 = OpVariable %11 Function
+         %18 = OpVariable %17 Function
+         %23 = OpVariable %22 Function
+         %32 = OpVariable %31 Function
+         %36 = OpVariable %35 Function
+         %41 = OpVariable %40 Function
+         %50 = OpVariable %49 Function
+               OpStore %8 %9
+         %13 = OpLoad %6 %8
+         %14 = OpLoad %6 %8
+         %15 = OpCompositeConstruct %10 %13 %14
+               OpStore %12 %15
+         %19 = OpLoad %10 %12
+         %20 = OpVectorShuffle %16 %19 %19 0 0 1
+               OpStore %18 %20
+         %24 = OpLoad %16 %18
+         %25 = OpLoad %6 %8
+         %26 = OpCompositeExtract %6 %24 0
+         %27 = OpCompositeExtract %6 %24 1
+         %28 = OpCompositeExtract %6 %24 2
+         %29 = OpCompositeConstruct %21 %26 %27 %28 %25
+               OpStore %23 %29
+               OpStore %32 %33
+               OpStore %36 %38
+         %42 = OpLoad %30 %32
+         %43 = OpLoad %34 %36
+         %44 = OpVectorShuffle %34 %43 %43 0 0
+         %45 = OpCompositeExtract %30 %44 0
+         %46 = OpCompositeExtract %30 %44 1
+         %47 = OpCompositeConstruct %39 %42 %45 %46
+               OpStore %41 %47
+         %53 = OpAccessChain %7 %23 %52
+         %54 = OpLoad %6 %53
+
+        %100 = OpCompositeConstruct %21 %20 %54
+        %101 = OpCompositeConstruct %21 %15 %19
+        %102 = OpCompositeConstruct %16 %27 %15
+        %103 = OpCompositeConstruct %48 %33 %47
+        %104 = OpCompositeConstruct %34 %42 %45
+        %105 = OpCompositeConstruct %39 %38 %46
+
+         %86 = OpCopyObject %30 %33
+         %56 = OpFOrdNotEqual %30 %54 %55
+         %80 = OpCopyObject %16 %20
+         %58 = OpAccessChain %7 %18 %57
+         %59 = OpLoad %6 %58
+         %60 = OpFOrdNotEqual %30 %59 %55
+         %61 = OpLoad %34 %36
+         %62 = OpLogicalAnd %30 %45 %46
+         %63 = OpLogicalOr %30 %45 %46
+         %64 = OpCompositeConstruct %48 %56 %60 %62 %63
+               OpStore %12 %15
+         %81 = OpVectorShuffle %16 %19 %19 0 0 1
+         %82 = OpCompositeConstruct %21 %26 %27 %28 %25
+         %83 = OpCopyObject %10 %15
+         %84 = OpCopyObject %39 %47
+               OpStore %50 %64
+         %85 = OpCopyObject %30 %42
+               OpStore %36 %38
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  fact_manager.AddFact(MakeSynonymFact(20, 100, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(54, 100, {3}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(15, 101, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(19, 101, {2}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(27, 102, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(15, 102, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(33, 103, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(47, 103, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(42, 104, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(45, 104, {1}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(38, 105, {0}), context.get());
+  fact_manager.AddFact(MakeSynonymFact(46, 105, {2}), context.get());
+
+  // Replace %20 with %100[0] in '%80 = OpCopyObject %16 %20'
+  auto replacement_1 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(20, SpvOpCopyObject, 0, 80, 0),
+      MakeDataDescriptor(100, {0}), 200);
+  ASSERT_TRUE(replacement_1.IsApplicable(context.get(), fact_manager));
+  replacement_1.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %54 with %100[3] in '%56 = OpFOrdNotEqual %30 %54 %55'
+  auto replacement_2 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(54, SpvOpFOrdNotEqual, 0, 56, 0),
+      MakeDataDescriptor(100, {3}), 201);
+  ASSERT_TRUE(replacement_2.IsApplicable(context.get(), fact_manager));
+  replacement_2.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %15 with %101[0] in 'OpStore %12 %15'
+  auto replacement_3 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(15, SpvOpStore, 1, 64, 0),
+      MakeDataDescriptor(101, {0}), 202);
+  ASSERT_TRUE(replacement_3.IsApplicable(context.get(), fact_manager));
+  replacement_3.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %19 with %101[2] in '%81 = OpVectorShuffle %16 %19 %19 0 0 1'
+  auto replacement_4 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(19, SpvOpVectorShuffle, 0, 81, 0),
+      MakeDataDescriptor(101, {2}), 203);
+  ASSERT_TRUE(replacement_4.IsApplicable(context.get(), fact_manager));
+  replacement_4.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %27 with %102[0] in '%82 = OpCompositeConstruct %21 %26 %27 %28
+  // %25'
+  auto replacement_5 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(27, SpvOpCompositeConstruct, 1, 82,
+                                          0),
+      MakeDataDescriptor(102, {0}), 204);
+  ASSERT_TRUE(replacement_5.IsApplicable(context.get(), fact_manager));
+  replacement_5.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %15 with %102[1] in '%83 = OpCopyObject %10 %15'
+  auto replacement_6 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(15, SpvOpCopyObject, 0, 83, 0),
+      MakeDataDescriptor(102, {1}), 205);
+  ASSERT_TRUE(replacement_6.IsApplicable(context.get(), fact_manager));
+  replacement_6.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %33 with %103[0] in '%86 = OpCopyObject %30 %33'
+  auto replacement_7 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(33, SpvOpCopyObject, 0, 86, 0),
+      MakeDataDescriptor(103, {0}), 206);
+  ASSERT_TRUE(replacement_7.IsApplicable(context.get(), fact_manager));
+  replacement_7.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %47 with %103[1] in '%84 = OpCopyObject %39 %47'
+  auto replacement_8 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(47, SpvOpCopyObject, 0, 84, 0),
+      MakeDataDescriptor(103, {1}), 207);
+  ASSERT_TRUE(replacement_8.IsApplicable(context.get(), fact_manager));
+  replacement_8.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %42 with %104[0] in '%85 = OpCopyObject %30 %42'
+  auto replacement_9 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(42, SpvOpCopyObject, 0, 85, 0),
+      MakeDataDescriptor(104, {0}), 208);
+  ASSERT_TRUE(replacement_9.IsApplicable(context.get(), fact_manager));
+  replacement_9.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %45 with %104[1] in '%63 = OpLogicalOr %30 %45 %46'
+  auto replacement_10 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(45, SpvOpLogicalOr, 0, 63, 0),
+      MakeDataDescriptor(104, {1}), 209);
+  ASSERT_TRUE(replacement_10.IsApplicable(context.get(), fact_manager));
+  replacement_10.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %38 with %105[0] in 'OpStore %36 %38'
+  auto replacement_11 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(38, SpvOpStore, 1, 85, 0),
+      MakeDataDescriptor(105, {0}), 210);
+  ASSERT_TRUE(replacement_11.IsApplicable(context.get(), fact_manager));
+  replacement_11.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  // Replace %46 with %105[2] in '%62 = OpLogicalAnd %30 %45 %46'
+  auto replacement_12 = TransformationReplaceIdWithSynonym(
+      transformation::MakeIdUseDescriptor(46, SpvOpLogicalAnd, 1, 62, 0),
+      MakeDataDescriptor(105, {2}), 211);
+  ASSERT_TRUE(replacement_12.IsApplicable(context.get(), fact_manager));
+  replacement_12.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  const std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %8 "f"
+               OpName %12 "v2"
+               OpName %18 "v3"
+               OpName %23 "v4"
+               OpName %32 "b"
+               OpName %36 "bv2"
+               OpName %41 "bv3"
+               OpName %50 "bv4"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 42
+         %10 = OpTypeVector %6 2
+         %11 = OpTypePointer Function %10
+         %16 = OpTypeVector %6 3
+         %17 = OpTypePointer Function %16
+         %21 = OpTypeVector %6 4
+         %22 = OpTypePointer Function %21
+         %30 = OpTypeBool
+         %31 = OpTypePointer Function %30
+         %33 = OpConstantFalse %30
+         %34 = OpTypeVector %30 2
+         %35 = OpTypePointer Function %34
+         %37 = OpConstantTrue %30
+         %38 = OpConstantComposite %34 %37 %37
+         %39 = OpTypeVector %30 3
+         %40 = OpTypePointer Function %39
+         %48 = OpTypeVector %30 4
+         %49 = OpTypePointer Function %48
+         %51 = OpTypeInt 32 0
+         %52 = OpConstant %51 2
+         %55 = OpConstant %6 0
+         %57 = OpConstant %51 1
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %12 = OpVariable %11 Function
+         %18 = OpVariable %17 Function
+         %23 = OpVariable %22 Function
+         %32 = OpVariable %31 Function
+         %36 = OpVariable %35 Function
+         %41 = OpVariable %40 Function
+         %50 = OpVariable %49 Function
+               OpStore %8 %9
+         %13 = OpLoad %6 %8
+         %14 = OpLoad %6 %8
+         %15 = OpCompositeConstruct %10 %13 %14
+               OpStore %12 %15
+         %19 = OpLoad %10 %12
+         %20 = OpVectorShuffle %16 %19 %19 0 0 1
+               OpStore %18 %20
+         %24 = OpLoad %16 %18
+         %25 = OpLoad %6 %8
+         %26 = OpCompositeExtract %6 %24 0
+         %27 = OpCompositeExtract %6 %24 1
+         %28 = OpCompositeExtract %6 %24 2
+         %29 = OpCompositeConstruct %21 %26 %27 %28 %25
+               OpStore %23 %29
+               OpStore %32 %33
+               OpStore %36 %38
+         %42 = OpLoad %30 %32
+         %43 = OpLoad %34 %36
+         %44 = OpVectorShuffle %34 %43 %43 0 0
+         %45 = OpCompositeExtract %30 %44 0
+         %46 = OpCompositeExtract %30 %44 1
+         %47 = OpCompositeConstruct %39 %42 %45 %46
+               OpStore %41 %47
+         %53 = OpAccessChain %7 %23 %52
+         %54 = OpLoad %6 %53
+
+        %100 = OpCompositeConstruct %21 %20 %54
+        %101 = OpCompositeConstruct %21 %15 %19
+        %102 = OpCompositeConstruct %16 %27 %15
+        %103 = OpCompositeConstruct %48 %33 %47
+        %104 = OpCompositeConstruct %34 %42 %45
+        %105 = OpCompositeConstruct %39 %38 %46
+
+        %206 = OpCompositeExtract %30 %103 0
+         %86 = OpCopyObject %30 %206
+        %201 = OpCompositeExtract %6 %100 3
+         %56 = OpFOrdNotEqual %30 %201 %55
+        %200 = OpVectorShuffle %16 %100 %100 0 1 2
+         %80 = OpCopyObject %16 %200
+         %58 = OpAccessChain %7 %18 %57
+         %59 = OpLoad %6 %58
+         %60 = OpFOrdNotEqual %30 %59 %55
+         %61 = OpLoad %34 %36
+        %211 = OpCompositeExtract %30 %105 2
+         %62 = OpLogicalAnd %30 %45 %211
+        %209 = OpCompositeExtract %30 %104 1
+         %63 = OpLogicalOr %30 %209 %46
+         %64 = OpCompositeConstruct %48 %56 %60 %62 %63
+        %202 = OpVectorShuffle %10 %101 %101 0 1
+               OpStore %12 %202
+        %203 = OpVectorShuffle %10 %101 %101 2 3
+         %81 = OpVectorShuffle %16 %203 %19 0 0 1
+        %204 = OpCompositeExtract %6 %102 0
+         %82 = OpCompositeConstruct %21 %26 %204 %28 %25
+        %205 = OpVectorShuffle %10 %102 %102 1 2
+         %83 = OpCopyObject %10 %205
+        %207 = OpVectorShuffle %39 %103 %103 1 2 3
+         %84 = OpCopyObject %39 %207
+               OpStore %50 %64
+        %208 = OpCompositeExtract %30 %104 0
+         %85 = OpCopyObject %30 %208
+        %210 = OpVectorShuffle %34 %105 %105 0 1
+               OpStore %36 %210
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
 }  // namespace
 }  // namespace fuzz
 }  // namespace spvtools