spirv-fuzz: TransformationPropagateInstructionDown (#3692)

Fixes #3691.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 5d4f428..2a9a95d 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -100,6 +100,7 @@
         fuzzer_pass_permute_function_parameters.h
         fuzzer_pass_permute_instructions.h
         fuzzer_pass_permute_phi_operands.h
+        fuzzer_pass_propagate_instructions_down.h
         fuzzer_pass_propagate_instructions_up.h
         fuzzer_pass_push_ids_through_variables.h
         fuzzer_pass_replace_adds_subs_muls_with_carrying_extended.h
@@ -190,6 +191,7 @@
         transformation_outline_function.h
         transformation_permute_function_parameters.h
         transformation_permute_phi_operands.h
+        transformation_propagate_instruction_down.h
         transformation_propagate_instruction_up.h
         transformation_push_id_through_variable.h
         transformation_record_synonymous_constants.h
@@ -282,6 +284,7 @@
         fuzzer_pass_permute_function_parameters.cpp
         fuzzer_pass_permute_instructions.cpp
         fuzzer_pass_permute_phi_operands.cpp
+        fuzzer_pass_propagate_instructions_down.cpp
         fuzzer_pass_propagate_instructions_up.cpp
         fuzzer_pass_push_ids_through_variables.cpp
         fuzzer_pass_replace_adds_subs_muls_with_carrying_extended.cpp
@@ -370,6 +373,7 @@
         transformation_outline_function.cpp
         transformation_permute_function_parameters.cpp
         transformation_permute_phi_operands.cpp
+        transformation_propagate_instruction_down.cpp
         transformation_propagate_instruction_up.cpp
         transformation_push_id_through_variable.cpp
         transformation_record_synonymous_constants.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index 6fa542c..7d9c9c4 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -68,6 +68,7 @@
 #include "source/fuzz/fuzzer_pass_permute_function_parameters.h"
 #include "source/fuzz/fuzzer_pass_permute_instructions.h"
 #include "source/fuzz/fuzzer_pass_permute_phi_operands.h"
+#include "source/fuzz/fuzzer_pass_propagate_instructions_down.h"
 #include "source/fuzz/fuzzer_pass_propagate_instructions_up.h"
 #include "source/fuzz/fuzzer_pass_push_ids_through_variables.h"
 #include "source/fuzz/fuzzer_pass_replace_adds_subs_muls_with_carrying_extended.h"
@@ -276,6 +277,7 @@
     MaybeAddRepeatedPass<FuzzerPassPermuteBlocks>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassPermuteFunctionParameters>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassPermuteInstructions>(&pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassPropagateInstructionsDown>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassPropagateInstructionsUp>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassPushIdsThroughVariables>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceAddsSubsMulsWithCarryingExtended>(
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 34f25cd..1250cba 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -107,6 +107,8 @@
 const std::pair<uint32_t, uint32_t> kChanceOfPermutingInstructions = {20, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfPermutingParameters = {30, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfPermutingPhiOperands = {30, 90};
+const std::pair<uint32_t, uint32_t> kChanceOfPropagatingInstructionsDown = {20,
+                                                                            70};
 const std::pair<uint32_t, uint32_t> kChanceOfPropagatingInstructionsUp = {20,
                                                                           70};
 const std::pair<uint32_t, uint32_t> kChanceOfPushingIdThroughVariable = {5, 50};
@@ -294,6 +296,8 @@
       ChooseBetweenMinAndMax(kChanceOfPermutingParameters);
   chance_of_permuting_phi_operands_ =
       ChooseBetweenMinAndMax(kChanceOfPermutingPhiOperands);
+  chance_of_propagating_instructions_down_ =
+      ChooseBetweenMinAndMax(kChanceOfPropagatingInstructionsDown);
   chance_of_propagating_instructions_up_ =
       ChooseBetweenMinAndMax(kChanceOfPropagatingInstructionsUp);
   chance_of_pushing_id_through_variable_ =
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index 4436b2b..01a92f7 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -264,6 +264,9 @@
   uint32_t GetChanceOfPermutingPhiOperands() {
     return chance_of_permuting_phi_operands_;
   }
+  uint32_t GetChanceOfPropagatingInstructionsDown() {
+    return chance_of_propagating_instructions_down_;
+  }
   uint32_t GetChanceOfPropagatingInstructionsUp() {
     return chance_of_propagating_instructions_up_;
   }
@@ -463,6 +466,7 @@
   uint32_t chance_of_permuting_instructions_;
   uint32_t chance_of_permuting_parameters_;
   uint32_t chance_of_permuting_phi_operands_;
+  uint32_t chance_of_propagating_instructions_down_;
   uint32_t chance_of_propagating_instructions_up_;
   uint32_t chance_of_pushing_id_through_variable_;
   uint32_t chance_of_replacing_add_sub_mul_with_carrying_extended_;
diff --git a/source/fuzz/fuzzer_pass_propagate_instructions_down.cpp b/source/fuzz/fuzzer_pass_propagate_instructions_down.cpp
new file mode 100644
index 0000000..7a115ae
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_propagate_instructions_down.cpp
@@ -0,0 +1,68 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// 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_propagate_instructions_down.h"
+
+#include "source/fuzz/fuzzer_context.h"
+#include "source/fuzz/transformation_propagate_instruction_down.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassPropagateInstructionsDown::FuzzerPassPropagateInstructionsDown(
+    opt::IRContext* ir_context, TransformationContext* transformation_context,
+    FuzzerContext* fuzzer_context,
+    protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, transformation_context, fuzzer_context,
+                 transformations) {}
+
+FuzzerPassPropagateInstructionsDown::~FuzzerPassPropagateInstructionsDown() =
+    default;
+
+void FuzzerPassPropagateInstructionsDown::Apply() {
+  for (const auto& function : *GetIRContext()->module()) {
+    std::vector<const opt::BasicBlock*> reachable_blocks;
+    for (const auto& block : function) {
+      if (GetIRContext()->GetDominatorAnalysis(&function)->IsReachable(
+              &block)) {
+        reachable_blocks.push_back(&block);
+      }
+    }
+
+    for (const auto* block : reachable_blocks) {
+      if (!GetFuzzerContext()->ChoosePercentage(
+              GetFuzzerContext()->GetChanceOfPropagatingInstructionsDown())) {
+        continue;
+      }
+
+      if (TransformationPropagateInstructionDown::IsApplicableToBlock(
+              GetIRContext(), block->id())) {
+        // Record fresh ids for every successor of the |block| that we can
+        // propagate an instruction into.
+        std::map<uint32_t, uint32_t> fresh_ids;
+        for (auto id :
+             TransformationPropagateInstructionDown::GetAcceptableSuccessors(
+                 GetIRContext(), block->id())) {
+          fresh_ids[id] = GetFuzzerContext()->GetFreshId();
+        }
+
+        ApplyTransformation(TransformationPropagateInstructionDown(
+            block->id(), GetFuzzerContext()->GetFreshId(), fresh_ids));
+      }
+    }
+  }
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_propagate_instructions_down.h b/source/fuzz/fuzzer_pass_propagate_instructions_down.h
new file mode 100644
index 0000000..536bf00
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_propagate_instructions_down.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// 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_PROPAGATE_INSTRUCTIONS_DOWN_H_
+#define SOURCE_FUZZ_FUZZER_PASS_PROPAGATE_INSTRUCTIONS_DOWN_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// Randomly propagates instructions from some block into the block's successors.
+class FuzzerPassPropagateInstructionsDown : public FuzzerPass {
+ public:
+  FuzzerPassPropagateInstructionsDown(
+      opt::IRContext* ir_context, TransformationContext* transformation_context,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassPropagateInstructionsDown() override;
+
+  void Apply() override;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_PROPAGATE_INSTRUCTIONS_DOWN_H_
diff --git a/source/fuzz/pass_management/repeated_pass_instances.h b/source/fuzz/pass_management/repeated_pass_instances.h
index 51ff2ae..8d2c0f8 100644
--- a/source/fuzz/pass_management/repeated_pass_instances.h
+++ b/source/fuzz/pass_management/repeated_pass_instances.h
@@ -54,6 +54,7 @@
 #include "source/fuzz/fuzzer_pass_permute_blocks.h"
 #include "source/fuzz/fuzzer_pass_permute_function_parameters.h"
 #include "source/fuzz/fuzzer_pass_permute_instructions.h"
+#include "source/fuzz/fuzzer_pass_propagate_instructions_down.h"
 #include "source/fuzz/fuzzer_pass_propagate_instructions_up.h"
 #include "source/fuzz/fuzzer_pass_push_ids_through_variables.h"
 #include "source/fuzz/fuzzer_pass_replace_adds_subs_muls_with_carrying_extended.h"
@@ -145,6 +146,7 @@
   REPEATED_PASS_INSTANCE(PermuteBlocks);
   REPEATED_PASS_INSTANCE(PermuteFunctionParameters);
   REPEATED_PASS_INSTANCE(PermuteInstructions);
+  REPEATED_PASS_INSTANCE(PropagateInstructionsDown);
   REPEATED_PASS_INSTANCE(PropagateInstructionsUp);
   REPEATED_PASS_INSTANCE(PushIdsThroughVariables);
   REPEATED_PASS_INSTANCE(ReplaceAddsSubsMulsWithCarryingExtended);
diff --git a/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp b/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
index 8894353..a6f024b 100644
--- a/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
+++ b/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
@@ -255,6 +255,13 @@
     // No obvious follow-on passes
     return {};
   }
+  if (&pass == pass_instances_->GetPropagateInstructionsDown()) {
+    // - This fuzzer pass might create new synonyms that can later be applied.
+    // - This fuzzer pass might create irrelevant ids that can later be
+    //   replaced.
+    return RandomOrderAndNonNull({pass_instances_->GetApplyIdSynonyms(),
+                                  pass_instances_->GetReplaceIrrelevantIds()});
+  }
   if (&pass == pass_instances_->GetPropagateInstructionsUp()) {
     // No obvious follow-on passes
     return {};
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index eb01b97..20ab98e 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -552,6 +552,7 @@
     TransformationAddLoopToCreateIntConstantSynonym add_loop_to_create_int_constant_synonym = 78;
     TransformationWrapRegionInSelection wrap_region_in_selection = 79;
     TransformationAddEarlyTerminatorWrapper add_early_terminator_wrapper = 80;
+    TransformationPropagateInstructionDown propagate_instruction_down = 81;
     // Add additional option using the next available number.
   }
 }
@@ -1745,6 +1746,38 @@
 
 }
 
+message TransformationPropagateInstructionDown {
+
+  // Propagates an instruction from |block_id| into its successors.
+  // Concretely, the transformation clones the propagated instruction
+  // into some of the successors of |block_id| and removes the original
+  // instruction. Additionally, an OpPhi instruction may be added to make sure
+  // that the transformation can be applied in various scenarios.
+  //
+  // Note that the instruction might not be propagated down into every successor
+  // of |block_id| since it might make the module invalid.
+
+  // Id of the block to propagate an instruction from. The decision on what
+  // instruction to propagate is made based on whether the instruction interacts
+  // with memory, whether that instruction is used in its block etc (see the
+  // transformation class for more details).
+  uint32 block_id = 1;
+
+  // A fresh id for an OpPhi instruction. This might not be used by the
+  // transformation since an OpPhi instruction is created only if needed
+  // (e.g. an instruction is propagated into divergent blocks).
+  uint32 phi_fresh_id = 2;
+
+  // A map from the id of some successor of the |block_id| to the fresh id.
+  // The map contains a fresh id for at least every successor of the |block_id|.
+  // Every fresh id in the map corresponds to the result id of the clone,
+  // propagated into the corresponding successor block. This transformation
+  // might use overflow ids if they are available and this field doesn't account
+  // for every successor of |block_id|.
+  repeated UInt32Pair successor_id_to_fresh_id = 3;
+
+}
+
 message TransformationPropagateInstructionUp {
 
   // Propagates an instruction in the block into the block's predecessors.
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index f03d6a9..cc3d010 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -70,6 +70,7 @@
 #include "source/fuzz/transformation_outline_function.h"
 #include "source/fuzz/transformation_permute_function_parameters.h"
 #include "source/fuzz/transformation_permute_phi_operands.h"
+#include "source/fuzz/transformation_propagate_instruction_down.h"
 #include "source/fuzz/transformation_propagate_instruction_up.h"
 #include "source/fuzz/transformation_push_id_through_variable.h"
 #include "source/fuzz/transformation_record_synonymous_constants.h"
@@ -259,6 +260,10 @@
     case protobufs::Transformation::TransformationCase::kPermutePhiOperands:
       return MakeUnique<TransformationPermutePhiOperands>(
           message.permute_phi_operands());
+    case protobufs::Transformation::TransformationCase::
+        kPropagateInstructionDown:
+      return MakeUnique<TransformationPropagateInstructionDown>(
+          message.propagate_instruction_down());
     case protobufs::Transformation::TransformationCase::kPropagateInstructionUp:
       return MakeUnique<TransformationPropagateInstructionUp>(
           message.propagate_instruction_up());
diff --git a/source/fuzz/transformation_propagate_instruction_down.cpp b/source/fuzz/transformation_propagate_instruction_down.cpp
new file mode 100644
index 0000000..ba22e39
--- /dev/null
+++ b/source/fuzz/transformation_propagate_instruction_down.cpp
@@ -0,0 +1,592 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// 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_propagate_instruction_down.h"
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/instruction_descriptor.h"
+
+namespace spvtools {
+namespace fuzz {
+
+TransformationPropagateInstructionDown::TransformationPropagateInstructionDown(
+    const protobufs::TransformationPropagateInstructionDown& message)
+    : message_(message) {}
+
+TransformationPropagateInstructionDown::TransformationPropagateInstructionDown(
+    uint32_t block_id, uint32_t phi_fresh_id,
+    const std::map<uint32_t, uint32_t>& successor_id_to_fresh_id) {
+  message_.set_block_id(block_id);
+  message_.set_phi_fresh_id(phi_fresh_id);
+  *message_.mutable_successor_id_to_fresh_id() =
+      fuzzerutil::MapToRepeatedUInt32Pair(successor_id_to_fresh_id);
+}
+
+bool TransformationPropagateInstructionDown::IsApplicable(
+    opt::IRContext* ir_context,
+    const TransformationContext& transformation_context) const {
+  // Check that we can apply this transformation to the |block_id|.
+  if (!IsApplicableToBlock(ir_context, message_.block_id())) {
+    return false;
+  }
+
+  const auto successor_id_to_fresh_id =
+      fuzzerutil::RepeatedUInt32PairToMap(message_.successor_id_to_fresh_id());
+
+  for (auto id : GetAcceptableSuccessors(ir_context, message_.block_id())) {
+    // Each successor must have a fresh id in the |successor_id_to_fresh_id|
+    // map, unless overflow ids are available.
+    if (!successor_id_to_fresh_id.count(id) &&
+        !transformation_context.GetOverflowIdSource()->HasOverflowIds()) {
+      return false;
+    }
+  }
+
+  std::vector<uint32_t> maybe_fresh_ids = {message_.phi_fresh_id()};
+  maybe_fresh_ids.reserve(successor_id_to_fresh_id.size());
+  for (const auto& entry : successor_id_to_fresh_id) {
+    maybe_fresh_ids.push_back(entry.second);
+  }
+
+  // All ids must be unique and fresh.
+  return !fuzzerutil::HasDuplicates(maybe_fresh_ids) &&
+         std::all_of(maybe_fresh_ids.begin(), maybe_fresh_ids.end(),
+                     [ir_context](uint32_t id) {
+                       return fuzzerutil::IsFreshId(ir_context, id);
+                     });
+}
+
+void TransformationPropagateInstructionDown::Apply(
+    opt::IRContext* ir_context,
+    TransformationContext* transformation_context) const {
+  // Get instruction to propagate down. There must be one.
+  auto* inst_to_propagate =
+      GetInstructionToPropagate(ir_context, message_.block_id());
+  assert(inst_to_propagate && "There must be an instruction to propagate");
+
+  auto successor_id_to_fresh_id =
+      fuzzerutil::RepeatedUInt32PairToMap(message_.successor_id_to_fresh_id());
+  std::vector<uint32_t> created_inst_ids;
+  auto successor_ids = GetAcceptableSuccessors(ir_context, message_.block_id());
+
+  // Clone |inst_to_propagate| into every successor.
+  for (auto successor_id : successor_ids) {
+    std::unique_ptr<opt::Instruction> clone(
+        inst_to_propagate->Clone(ir_context));
+
+    uint32_t new_result_id;
+    if (successor_id_to_fresh_id.count(successor_id)) {
+      new_result_id = successor_id_to_fresh_id.at(successor_id);
+    } else {
+      assert(transformation_context->GetOverflowIdSource()->HasOverflowIds() &&
+             "Overflow ids must be available");
+      new_result_id =
+          transformation_context->GetOverflowIdSource()->GetNextOverflowId();
+      successor_id_to_fresh_id[successor_id] = new_result_id;
+    }
+
+    clone->SetResultId(new_result_id);
+    fuzzerutil::UpdateModuleIdBound(ir_context, new_result_id);
+
+    auto* insert_before_inst = GetFirstInsertBeforeInstruction(
+        ir_context, successor_id, clone->opcode());
+    assert(insert_before_inst && "Can't insert into one of the successors");
+
+    insert_before_inst->InsertBefore(std::move(clone));
+    created_inst_ids.push_back(new_result_id);
+  }
+
+  // Add an OpPhi instruction into the module if possible.
+  if (auto merge_block_id = GetOpPhiBlockId(
+          ir_context, message_.block_id(), *inst_to_propagate, successor_ids)) {
+    opt::Instruction::OperandList in_operands;
+    std::unordered_set<uint32_t> visited_predecessors;
+    for (auto predecessor_id : ir_context->cfg()->preds(merge_block_id)) {
+      if (visited_predecessors.count(predecessor_id)) {
+        // Merge block might have multiple identical predecessors.
+        continue;
+      }
+
+      visited_predecessors.insert(predecessor_id);
+
+      const auto* dominator_analysis = ir_context->GetDominatorAnalysis(
+          ir_context->cfg()->block(message_.block_id())->GetParent());
+
+      // Find the successor of |source_block| that dominates the predecessor of
+      // the merge block |predecessor_id|.
+      auto it = std::find_if(
+          successor_ids.begin(), successor_ids.end(),
+          [predecessor_id, dominator_analysis](uint32_t successor_id) {
+            return dominator_analysis->Dominates(successor_id, predecessor_id);
+          });
+
+      // OpPhi requires a single operand pair for every predecessor of the
+      // OpPhi's block.
+      assert(it != successor_ids.end() && "Unable to insert OpPhi");
+
+      in_operands.push_back(
+          {SPV_OPERAND_TYPE_ID, {successor_id_to_fresh_id.at(*it)}});
+      in_operands.push_back({SPV_OPERAND_TYPE_ID, {predecessor_id}});
+    }
+
+    ir_context->cfg()
+        ->block(merge_block_id)
+        ->begin()
+        ->InsertBefore(MakeUnique<opt::Instruction>(
+            ir_context, SpvOpPhi, inst_to_propagate->type_id(),
+            message_.phi_fresh_id(), std::move(in_operands)));
+
+    fuzzerutil::UpdateModuleIdBound(ir_context, message_.phi_fresh_id());
+    created_inst_ids.push_back(message_.phi_fresh_id());
+  }
+
+  // Make sure analyses are updated when we adjust users of |inst_to_propagate|.
+  ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+
+  // Copy decorations from the original instructions to its propagated copies.
+  for (auto id : created_inst_ids) {
+    ir_context->get_decoration_mgr()->CloneDecorations(
+        inst_to_propagate->result_id(), id);
+  }
+
+  // Remove all decorations from the original instruction.
+  ir_context->get_decoration_mgr()->RemoveDecorationsFrom(
+      inst_to_propagate->result_id());
+
+  // Update every use of the |inst_to_propagate| with a result id of some of the
+  // newly created instructions.
+  ir_context->get_def_use_mgr()->ForEachUse(
+      inst_to_propagate, [ir_context, &created_inst_ids](
+                             opt::Instruction* user, uint32_t operand_index) {
+        assert(ir_context->get_instr_block(user) &&
+               "All decorations should have already been adjusted");
+
+        auto in_operand_index =
+            fuzzerutil::InOperandIndexFromOperandIndex(*user, operand_index);
+        for (auto id : created_inst_ids) {
+          if (fuzzerutil::IdIsAvailableAtUse(ir_context, user, in_operand_index,
+                                             id)) {
+            user->SetInOperand(in_operand_index, {id});
+            return;
+          }
+        }
+
+        // Every user of |inst_to_propagate| must be updated since we will
+        // remove that instruction from the module.
+        assert(false && "Every user of |inst_to_propagate| must be updated");
+      });
+
+  // Add synonyms about newly created instructions.
+  assert(inst_to_propagate->HasResultId() &&
+         "Result id is required to add facts");
+  if (transformation_context->GetFactManager()->IdIsIrrelevant(
+          inst_to_propagate->result_id())) {
+    for (auto id : created_inst_ids) {
+      transformation_context->GetFactManager()->AddFactIdIsIrrelevant(id);
+    }
+  } else {
+    std::vector<uint32_t> non_irrelevant_ids;
+    for (auto id : created_inst_ids) {
+      // |id| can be irrelevant implicitly (e.g. if we propagate it into a dead
+      // block).
+      if (!transformation_context->GetFactManager()->IdIsIrrelevant(id)) {
+        non_irrelevant_ids.push_back(id);
+      }
+    }
+
+    if (transformation_context->GetFactManager()->PointeeValueIsIrrelevant(
+            inst_to_propagate->result_id())) {
+      for (auto id : non_irrelevant_ids) {
+        transformation_context->GetFactManager()
+            ->AddFactValueOfPointeeIsIrrelevant(id);
+      }
+    }
+
+    for (auto id : non_irrelevant_ids) {
+      transformation_context->GetFactManager()->AddFactDataSynonym(
+          MakeDataDescriptor(id, {}),
+          MakeDataDescriptor(non_irrelevant_ids[0], {}));
+    }
+  }
+
+  // Remove the propagated instruction from the module.
+  ir_context->KillInst(inst_to_propagate);
+
+  // We've adjusted all users - make sure these changes are analyzed.
+  ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+}
+
+protobufs::Transformation TransformationPropagateInstructionDown::ToMessage()
+    const {
+  protobufs::Transformation result;
+  *result.mutable_propagate_instruction_down() = message_;
+  return result;
+}
+
+bool TransformationPropagateInstructionDown::IsOpcodeSupported(SpvOp opcode) {
+  // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3605):
+  //  We only support "simple" instructions that don't work with memory.
+  //  We should extend this so that we support the ones that modify the memory
+  //  too.
+  switch (opcode) {
+    case SpvOpUndef:
+    case SpvOpAccessChain:
+    case SpvOpInBoundsAccessChain:
+    case SpvOpArrayLength:
+    case SpvOpVectorExtractDynamic:
+    case SpvOpVectorInsertDynamic:
+    case SpvOpVectorShuffle:
+    case SpvOpCompositeConstruct:
+    case SpvOpCompositeExtract:
+    case SpvOpCompositeInsert:
+    case SpvOpCopyObject:
+    case SpvOpTranspose:
+    case SpvOpConvertFToU:
+    case SpvOpConvertFToS:
+    case SpvOpConvertSToF:
+    case SpvOpConvertUToF:
+    case SpvOpUConvert:
+    case SpvOpSConvert:
+    case SpvOpFConvert:
+    case SpvOpQuantizeToF16:
+    case SpvOpSatConvertSToU:
+    case SpvOpSatConvertUToS:
+    case SpvOpBitcast:
+    case SpvOpSNegate:
+    case SpvOpFNegate:
+    case SpvOpIAdd:
+    case SpvOpFAdd:
+    case SpvOpISub:
+    case SpvOpFSub:
+    case SpvOpIMul:
+    case SpvOpFMul:
+    case SpvOpUDiv:
+    case SpvOpSDiv:
+    case SpvOpFDiv:
+    case SpvOpUMod:
+    case SpvOpSRem:
+    case SpvOpSMod:
+    case SpvOpFRem:
+    case SpvOpFMod:
+    case SpvOpVectorTimesScalar:
+    case SpvOpMatrixTimesScalar:
+    case SpvOpVectorTimesMatrix:
+    case SpvOpMatrixTimesVector:
+    case SpvOpMatrixTimesMatrix:
+    case SpvOpOuterProduct:
+    case SpvOpDot:
+    case SpvOpIAddCarry:
+    case SpvOpISubBorrow:
+    case SpvOpUMulExtended:
+    case SpvOpSMulExtended:
+    case SpvOpAny:
+    case SpvOpAll:
+    case SpvOpIsNan:
+    case SpvOpIsInf:
+    case SpvOpIsFinite:
+    case SpvOpIsNormal:
+    case SpvOpSignBitSet:
+    case SpvOpLessOrGreater:
+    case SpvOpOrdered:
+    case SpvOpUnordered:
+    case SpvOpLogicalEqual:
+    case SpvOpLogicalNotEqual:
+    case SpvOpLogicalOr:
+    case SpvOpLogicalAnd:
+    case SpvOpLogicalNot:
+    case SpvOpSelect:
+    case SpvOpIEqual:
+    case SpvOpINotEqual:
+    case SpvOpUGreaterThan:
+    case SpvOpSGreaterThan:
+    case SpvOpUGreaterThanEqual:
+    case SpvOpSGreaterThanEqual:
+    case SpvOpULessThan:
+    case SpvOpSLessThan:
+    case SpvOpULessThanEqual:
+    case SpvOpSLessThanEqual:
+    case SpvOpFOrdEqual:
+    case SpvOpFUnordEqual:
+    case SpvOpFOrdNotEqual:
+    case SpvOpFUnordNotEqual:
+    case SpvOpFOrdLessThan:
+    case SpvOpFUnordLessThan:
+    case SpvOpFOrdGreaterThan:
+    case SpvOpFUnordGreaterThan:
+    case SpvOpFOrdLessThanEqual:
+    case SpvOpFUnordLessThanEqual:
+    case SpvOpFOrdGreaterThanEqual:
+    case SpvOpFUnordGreaterThanEqual:
+    case SpvOpShiftRightLogical:
+    case SpvOpShiftRightArithmetic:
+    case SpvOpShiftLeftLogical:
+    case SpvOpBitwiseOr:
+    case SpvOpBitwiseXor:
+    case SpvOpBitwiseAnd:
+    case SpvOpNot:
+    case SpvOpBitFieldInsert:
+    case SpvOpBitFieldSExtract:
+    case SpvOpBitFieldUExtract:
+    case SpvOpBitReverse:
+    case SpvOpBitCount:
+    case SpvOpCopyLogical:
+    case SpvOpPtrEqual:
+    case SpvOpPtrNotEqual:
+      return true;
+    default:
+      return false;
+  }
+}
+
+opt::Instruction*
+TransformationPropagateInstructionDown::GetInstructionToPropagate(
+    opt::IRContext* ir_context, uint32_t block_id) {
+  auto* block = ir_context->cfg()->block(block_id);
+  assert(block && "|block_id| is invalid");
+
+  for (auto it = block->rbegin(); it != block->rend(); ++it) {
+    if (!it->result_id() || !it->type_id() ||
+        !IsOpcodeSupported(it->opcode())) {
+      continue;
+    }
+
+    auto all_users_from_different_blocks =
+        ir_context->get_def_use_mgr()->WhileEachUser(
+            &*it, [ir_context, block](opt::Instruction* user) {
+              return ir_context->get_instr_block(user) != block;
+            });
+
+    if (!all_users_from_different_blocks) {
+      // We can't propagate an instruction if it's used in the same block.
+      continue;
+    }
+
+    return &*it;
+  }
+
+  return nullptr;
+}
+
+bool TransformationPropagateInstructionDown::IsApplicableToBlock(
+    opt::IRContext* ir_context, uint32_t block_id) {
+  // Check that |block_id| is valid.
+  const auto* block = fuzzerutil::MaybeFindBlock(ir_context, block_id);
+  if (!block) {
+    return false;
+  }
+
+  const auto* dominator_analysis =
+      ir_context->GetDominatorAnalysis(block->GetParent());
+
+  // |block| must be reachable.
+  if (!dominator_analysis->IsReachable(block)) {
+    return false;
+  }
+
+  // The block must have an instruction to propagate.
+  const auto* inst_to_propagate =
+      GetInstructionToPropagate(ir_context, block_id);
+  if (!inst_to_propagate) {
+    return false;
+  }
+
+  // Check that |block| has successors.
+  auto successor_ids = GetAcceptableSuccessors(ir_context, block_id);
+  if (successor_ids.empty()) {
+    return false;
+  }
+
+  // Check that |successor_block| doesn't have any OpPhi instructions that
+  // use |inst|.
+  for (auto successor_id : successor_ids) {
+    for (const auto& maybe_phi_inst : *ir_context->cfg()->block(successor_id)) {
+      if (maybe_phi_inst.opcode() != SpvOpPhi) {
+        // OpPhis can be intermixed with OpLine and OpNoLine.
+        continue;
+      }
+
+      for (uint32_t i = 0; i < maybe_phi_inst.NumInOperands(); i += 2) {
+        if (maybe_phi_inst.GetSingleWordInOperand(i) ==
+            inst_to_propagate->result_id()) {
+          return false;
+        }
+      }
+    }
+  }
+
+  // Get the result id of the block we will insert OpPhi instruction into.
+  // This is either 0 or a result id of some merge block in the function.
+  auto phi_block_id =
+      GetOpPhiBlockId(ir_context, block_id, *inst_to_propagate, successor_ids);
+
+  // Make sure we can adjust all users of the propagated instruction.
+  return ir_context->get_def_use_mgr()->WhileEachUse(
+      inst_to_propagate,
+      [ir_context, &successor_ids, dominator_analysis, phi_block_id](
+          opt::Instruction* user, uint32_t index) {
+        const auto* user_block = ir_context->get_instr_block(user);
+
+        if (!user_block) {
+          // |user| might be a global instruction (e.g. OpDecorate).
+          return true;
+        }
+
+        // Check that at least one of the ids in |successor_ids| or a
+        // |phi_block_id| dominates |user|'s block (or its predecessor if the
+        // user is an OpPhi). We can't use fuzzerutil::IdIsAvailableAtUse since
+        // the id in question hasn't yet been created in the module.
+        auto block_id_to_dominate = user->opcode() == SpvOpPhi
+                                        ? user->GetSingleWordOperand(index + 1)
+                                        : user_block->id();
+
+        if (phi_block_id != 0 &&
+            dominator_analysis->Dominates(phi_block_id, block_id_to_dominate)) {
+          return true;
+        }
+
+        return std::any_of(
+            successor_ids.begin(), successor_ids.end(),
+            [dominator_analysis, block_id_to_dominate](uint32_t id) {
+              return dominator_analysis->Dominates(id, block_id_to_dominate);
+            });
+      });
+}
+
+opt::Instruction*
+TransformationPropagateInstructionDown::GetFirstInsertBeforeInstruction(
+    opt::IRContext* ir_context, uint32_t block_id, SpvOp opcode) {
+  auto* block = ir_context->cfg()->block(block_id);
+
+  auto it = block->begin();
+
+  while (it != block->end() &&
+         !fuzzerutil::CanInsertOpcodeBeforeInstruction(opcode, it)) {
+    ++it;
+  }
+
+  return it == block->end() ? nullptr : &*it;
+}
+
+std::unordered_set<uint32_t>
+TransformationPropagateInstructionDown::GetAcceptableSuccessors(
+    opt::IRContext* ir_context, uint32_t block_id) {
+  const auto* block = ir_context->cfg()->block(block_id);
+  assert(block && "|block_id| is invalid");
+
+  const auto* inst = GetInstructionToPropagate(ir_context, block_id);
+  assert(inst && "The block must have an instruction to propagate");
+
+  std::unordered_set<uint32_t> result;
+  block->ForEachSuccessorLabel([ir_context, &result,
+                                inst](uint32_t successor_id) {
+    if (result.count(successor_id)) {
+      return;
+    }
+
+    auto* successor_block = ir_context->cfg()->block(successor_id);
+
+    // We can't propagate |inst| into |successor_block| if the latter is not
+    // dominated by the |inst|'s dependencies.
+    if (!inst->WhileEachInId([ir_context, successor_block](const uint32_t* id) {
+          return fuzzerutil::IdIsAvailableBeforeInstruction(
+              ir_context, &*successor_block->begin(), *id);
+        })) {
+      return;
+    }
+
+    // We don't propagate any "special" instructions (e.g. OpSelectionMerge
+    // etc), thus, insertion point must always exist if the module is valid.
+    assert(GetFirstInsertBeforeInstruction(ir_context, successor_id,
+                                           inst->opcode()) &&
+           "There must exist an insertion point.");
+
+    result.insert(successor_id);
+  });
+
+  return result;
+}
+
+uint32_t TransformationPropagateInstructionDown::GetOpPhiBlockId(
+    opt::IRContext* ir_context, uint32_t block_id,
+    const opt::Instruction& inst_to_propagate,
+    const std::unordered_set<uint32_t>& successor_ids) {
+  const auto* block = ir_context->cfg()->block(block_id);
+
+  // |block_id| must belong to some construct.
+  auto merge_block_id =
+      block->GetMergeInst()
+          ? block->GetMergeInst()->GetSingleWordInOperand(0)
+          : ir_context->GetStructuredCFGAnalysis()->MergeBlock(block_id);
+  if (!merge_block_id) {
+    return 0;
+  }
+
+  const auto* dominator_analysis =
+      ir_context->GetDominatorAnalysis(block->GetParent());
+
+  // Check that |merge_block_id| is reachable in the CFG and |block_id|
+  // dominates |merge_block_id|.
+  if (!dominator_analysis->IsReachable(merge_block_id) ||
+      !dominator_analysis->Dominates(block_id, merge_block_id)) {
+    return 0;
+  }
+
+  // We can't insert an OpPhi into |merge_block_id| if it's an acceptable
+  // successor of |block_id|.
+  if (successor_ids.count(merge_block_id)) {
+    return 0;
+  }
+
+  // All predecessors of the merge block must be dominated by at least one
+  // successor of the |block_id|.
+  assert(!ir_context->cfg()->preds(merge_block_id).empty() &&
+         "Merge block must be reachable");
+  for (auto predecessor_id : ir_context->cfg()->preds(merge_block_id)) {
+    if (std::none_of(
+            successor_ids.begin(), successor_ids.end(),
+            [dominator_analysis, predecessor_id](uint32_t successor_id) {
+              return dominator_analysis->Dominates(successor_id,
+                                                   predecessor_id);
+            })) {
+      return 0;
+    }
+  }
+
+  const auto* propagate_type =
+      ir_context->get_type_mgr()->GetType(inst_to_propagate.type_id());
+  assert(propagate_type && "|inst_to_propagate| must have a valid type");
+
+  // VariablePointers capability implicitly declares
+  // VariablePointersStorageBuffer. We need those capabilities since otherwise
+  // OpPhi instructions cannot have operands of pointer types.
+  if (propagate_type->AsPointer() &&
+      !ir_context->get_feature_mgr()->HasCapability(
+          SpvCapabilityVariablePointersStorageBuffer)) {
+    return 0;
+  }
+
+  return merge_block_id;
+}
+
+std::unordered_set<uint32_t>
+TransformationPropagateInstructionDown::GetFreshIds() const {
+  std::unordered_set<uint32_t> result = {message_.phi_fresh_id()};
+  for (const auto& pair : message_.successor_id_to_fresh_id()) {
+    result.insert(pair.second());
+  }
+  return result;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_propagate_instruction_down.h b/source/fuzz/transformation_propagate_instruction_down.h
new file mode 100644
index 0000000..7eca1ad
--- /dev/null
+++ b/source/fuzz/transformation_propagate_instruction_down.h
@@ -0,0 +1,184 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// 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_PROPAGATE_INSTRUCTION_DOWN_H_
+#define SOURCE_FUZZ_TRANSFORMATION_PROPAGATE_INSTRUCTION_DOWN_H_
+
+#include <map>
+
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/fuzz/transformation_context.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationPropagateInstructionDown : public Transformation {
+ public:
+  explicit TransformationPropagateInstructionDown(
+      const protobufs::TransformationPropagateInstructionDown& message);
+
+  TransformationPropagateInstructionDown(
+      uint32_t block_id, uint32_t phi_fresh_id,
+      const std::map<uint32_t, uint32_t>& successor_id_to_fresh_id);
+
+  // - It should be possible to apply this transformation to |block_id| (see
+  //   IsApplicableToBlock method).
+  // - Every acceptable successor of |block_id| (see GetAcceptableSuccessors
+  //   method) must have an entry in the |successor_id_to_fresh_id| map unless
+  //   overflow ids are available.
+  // - All values in |successor_id_to_fresh_id| and |phi_fresh_id| must be
+  //   unique and fresh.
+  bool IsApplicable(
+      opt::IRContext* ir_context,
+      const TransformationContext& transformation_context) const override;
+
+  // - Adds a clone of the propagated instruction into every acceptable
+  //   successor of |block_id|.
+  // - Removes the original instruction.
+  // - Creates an OpPhi instruction if possible, that tries to group created
+  //   clones.
+  // - If the original instruction's id was irrelevant - marks created
+  //   instructions as irrelevant. Otherwise, marks the created instructions as
+  //   synonymous to each other if possible (i.e. skips instructions, copied
+  //   into dead blocks).
+  void Apply(opt::IRContext* ir_context,
+             TransformationContext* transformation_context) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+  // Returns true if this transformation can be applied to the block with id
+  // |block_id|. Concretely, returns true iff:
+  // - |block_id| is a result id of some reachable basic block in the module.
+  // - the block has an instruction to propagate (see
+  //   GetInstructionToPropagate method).
+  // - the block has at least one acceptable successor (see
+  //   GetAcceptableSuccessors method).
+  // - none of the acceptable successors have OpPhi instructions that use the
+  //   original instruction.
+  // - it is possible to replace every use of the original instruction with some
+  //   of the propagated instructions (or an OpPhi if we can create it - see
+  //   GetOpPhiBlockId method).
+  static bool IsApplicableToBlock(opt::IRContext* ir_context,
+                                  uint32_t block_id);
+
+  // Returns ids of successors of |block_id|, that can be used to propagate an
+  // instruction into. Concretely, a successor block is acceptable if all
+  // dependencies of the propagated instruction dominate it. Note that this
+  // implies that an acceptable successor must be reachable in the CFG.
+  // For example:
+  //    %1 = OpLabel
+  //         OpSelectionMerge %2 None
+  //         OpBranchConditional %cond %2 %3
+  //    %3 = OpLabel
+  //    %4 = OpUndef %int
+  //    %5 = OpCopyObject %int %4
+  //         OpBranch %2
+  //    %2 = OpLabel
+  //    ...
+  // In this example, %2 is not an acceptable successor of %3 since one of the
+  // dependencies (%4) of the propagated instruction (%5) does not dominate it.
+  static std::unordered_set<uint32_t> GetAcceptableSuccessors(
+      opt::IRContext* ir_context, uint32_t block_id);
+
+  std::unordered_set<uint32_t> GetFreshIds() const override;
+
+ private:
+  // Returns the last possible instruction in the |block_id| that satisfies the
+  // following properties:
+  // - has result id
+  // - has type id
+  // - has supported opcode (see IsOpcodeSupported method)
+  // - has no users in its basic block.
+  // Returns nullptr if no such an instruction exists. For example:
+  //    %1 = OpLabel
+  //    %2 = OpUndef %int
+  //    %3 = OpUndef %int
+  //         OpStore %var %3
+  //         OpBranch %some_block
+  // In this example:
+  // - We cannot propagate OpBranch nor OpStore since they both have unsupported
+  //   opcodes and have neither result ids nor type ids.
+  // - We cannot propagate %3 either since it is used by OpStore.
+  // - We can propagate %2 since it satisfies all our conditions.
+  // The basic idea behind this method it to make sure that the returned
+  // instruction will not break domination rules in its original block when
+  // propagated.
+  static opt::Instruction* GetInstructionToPropagate(opt::IRContext* ir_context,
+                                                     uint32_t block_id);
+
+  // Returns true if |opcode| is supported by this transformation.
+  static bool IsOpcodeSupported(SpvOp opcode);
+
+  // Returns the first instruction in the |block| that allows us to insert
+  // |opcode| above itself. Returns nullptr is no such instruction exists.
+  static opt::Instruction* GetFirstInsertBeforeInstruction(
+      opt::IRContext* ir_context, uint32_t block_id, SpvOp opcode);
+
+  // Returns a result id of a basic block, where an OpPhi instruction can be
+  // inserted. Returns nullptr if it's not possible to create an OpPhi. The
+  // created OpPhi instruction groups all the propagated clones of the original
+  // instruction. |block_id| is a result id of the block we propagate the
+  // instruction from. |successor_ids| contains result ids of the successors we
+  // propagate the instruction into. Concretely, returns a non-null value if:
+  // - |block_id| is in some construct.
+  // - The merge block of that construct is reachable.
+  // - |block_id| dominates that merge block.
+  // - That merge block may not be an acceptable successor of |block_id|.
+  // - There must be at least one |block_id|'s acceptable successor for every
+  //   predecessor of the merge block, dominating that predecessor.
+  // - We can't create an OpPhi if the module has neither VariablePointers nor
+  //   VariablePointersStorageBuffer capabilities.
+  // A simple example of when we can insert an OpPhi instruction is:
+  // - This snippet of code:
+  //    %1 = OpLabel
+  //    %2 = OpUndef %int
+  //         OpSelectionMerge %5 None
+  //         OpBranchConditional %cond %3 %4
+  //    %3 = OpLabel
+  //         OpBranch %5
+  //    %4 = OpLabel
+  //         OpBranch %5
+  //    %5 = OpLabel
+  //         ...
+  //   will be transformed into the following one (if %2 is propagated):
+  //    %1 = OpLabel
+  //         OpSelectionMerge %5 None
+  //         OpBranchConditional %cond %3 %4
+  //    %3 = OpLabel
+  //    %6 = OpUndef %int
+  //         OpBranch %5
+  //    %4 = OpLabel
+  //    %7 = OpUndef %int
+  //         OpBranch %5
+  //    %5 = OpLabel
+  //    %8 = OpPhi %int %6 %3 %7 %4
+  //         ...
+  // The fact that we introduce an OpPhi allows us to increase the applicability
+  // of the transformation. Concretely, we wouldn't be able to apply it in the
+  // example above if %2 were used in %5. Some more complicated examples can be
+  // found in unit tests.
+  static uint32_t GetOpPhiBlockId(
+      opt::IRContext* ir_context, uint32_t block_id,
+      const opt::Instruction& inst_to_propagate,
+      const std::unordered_set<uint32_t>& successor_ids);
+
+  protobufs::TransformationPropagateInstructionDown message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_PROPAGATE_INSTRUCTION_DOWN_H_
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index ecccaa3..c42bd2d 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -87,6 +87,7 @@
           transformation_outline_function_test.cpp
           transformation_permute_function_parameters_test.cpp
           transformation_permute_phi_operands_test.cpp
+          transformation_propagate_instruction_down_test.cpp
           transformation_propagate_instruction_up_test.cpp
           transformation_push_id_through_variable_test.cpp
           transformation_replace_add_sub_mul_with_carrying_extended_test.cpp
diff --git a/test/fuzz/fuzz_test_util.cpp b/test/fuzz/fuzz_test_util.cpp
index c48e5fc..b875402 100644
--- a/test/fuzz/fuzz_test_util.cpp
+++ b/test/fuzz/fuzz_test_util.cpp
@@ -158,12 +158,5 @@
   }
 }
 
-void ApplyAndCheckFreshIds(const Transformation& transformation,
-                           opt::IRContext* ir_context,
-                           TransformationContext* transformation_context) {
-  ApplyAndCheckFreshIds(transformation, ir_context, transformation_context,
-                        std::unordered_set<uint32_t>());
-}
-
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/test/fuzz/fuzz_test_util.h b/test/fuzz/fuzz_test_util.h
index b094a1d..5d02ef6 100644
--- a/test/fuzz/fuzz_test_util.h
+++ b/test/fuzz/fuzz_test_util.h
@@ -120,12 +120,7 @@
 void ApplyAndCheckFreshIds(
     const Transformation& transformation, opt::IRContext* ir_context,
     TransformationContext* transformation_context,
-    const std::unordered_set<uint32_t>& issued_overflow_ids);
-
-// Invokes ApplyAndCheckFreshIds above, with an empty set of overflow ids.
-void ApplyAndCheckFreshIds(const Transformation& transformation,
-                           opt::IRContext* ir_context,
-                           TransformationContext* transformation_context);
+    const std::unordered_set<uint32_t>& issued_overflow_ids = {{}});
 
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/test/fuzz/transformation_propagate_instruction_down_test.cpp b/test/fuzz/transformation_propagate_instruction_down_test.cpp
new file mode 100644
index 0000000..ecb4a67
--- /dev/null
+++ b/test/fuzz/transformation_propagate_instruction_down_test.cpp
@@ -0,0 +1,1124 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// 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_propagate_instruction_down.h"
+
+#include "source/fuzz/counter_overflow_id_source.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationPropagateInstructionDownTest, BasicTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %9 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+
+          ; Has no instruction to propagate
+          %5 = OpLabel
+         %10 = OpVariable %9 Function
+          %8 = OpCopyObject %6 %7
+               OpStore %10 %8
+               OpBranch %11
+
+        ; Unreachable block
+        %100 = OpLabel
+        %101 = OpCopyObject %6 %7
+               OpBranch %11
+
+         ; Selection header
+         ;
+         ; One of acceptable successors has an OpPhi that uses propagated
+         ; instruction's id
+         %11 = OpLabel
+         %19 = OpCopyObject %6 %7
+               OpSelectionMerge %18 None
+               OpBranchConditional %13 %14 %18
+
+         ; %16 has no acceptable successors
+         %14 = OpLabel
+         %20 = OpPhi %6 %19 %11
+         %15 = OpCopyObject %6 %7 ; dependency
+               OpBranch %16
+         %16 = OpLabel
+         %17 = OpCopyObject %6 %15
+               OpBranch %18
+
+         ; Can be applied
+         %18 = OpLabel
+         %21 = OpCopyObject %6 %7
+               OpSelectionMerge %24 None
+               OpBranchConditional %13 %22 %23
+         %22 = OpLabel
+         %29 = OpPhi %6 %7 %18
+               OpStore %10 %21
+               OpBranch %24
+         %23 = OpLabel
+               OpStore %10 %21
+               OpBranch %24
+         %24 = OpLabel
+               OpStore %10 %21
+               OpBranch %32
+
+         ; Can't replace all uses of the propagated instruction: %30 is
+         ; propagated into %27.
+         %32 = OpLabel
+               OpLoopMerge %28 %27 None
+               OpBranchConditional %13 %26 %28
+         %26 = OpLabel
+         %25 = OpCopyObject %6 %7
+         %30 = OpCopyObject %6 %25
+               OpBranchConditional %13 %27 %28
+         %27 = OpLabel
+               OpBranch %32
+         %28 = OpLabel
+         %31 = OpPhi %6 %30 %26 %7 %32 ; Can't replace this use
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  // Invalid block id.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(200, 200, {{}})
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationPropagateInstructionDown(101, 200, {{}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The block is unreachable.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(100, 200, {{}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The block has no instruction to propagate.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(5, 200, {{{11, 201}}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The block has no acceptable successors.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(16, 200, {{{18, 201}}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  // One of acceptable successors has an OpPhi that uses propagated
+  // instruction's id.
+  ASSERT_FALSE(
+      TransformationPropagateInstructionDown(11, 200, {{{14, 201}, {18, 202}}})
+          .IsApplicable(context.get(), transformation_context));
+
+#ifndef NDEBUG
+  // Not all fresh ids are provided.
+  ASSERT_DEATH(
+      TransformationPropagateInstructionDown(18, 200, {{{22, 201}, {202, 203}}})
+          .IsApplicable(context.get(), transformation_context),
+      "Bad attempt to query whether overflow ids are available.");
+#endif
+
+  // Not all fresh ids are fresh.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(
+                   18, 18, {{{22, 201}, {23, 202}, {202, 203}}})
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationPropagateInstructionDown(
+                   18, 200, {{{22, 22}, {23, 202}, {202, 203}}})
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationPropagateInstructionDown(
+                   18, 18, {{{22, 22}, {23, 202}, {202, 203}}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Not all fresh ids are unique.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(
+                   18, 200, {{{22, 200}, {23, 202}, {202, 200}}})
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationPropagateInstructionDown(
+                   18, 200, {{{22, 201}, {23, 202}, {202, 200}}})
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationPropagateInstructionDown(
+                   18, 200, {{{22, 201}, {23, 201}, {202, 203}}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Can't replace all uses of the propagated instruction: %30 is propagated
+  // into %27.
+  ASSERT_FALSE(TransformationPropagateInstructionDown(26, 200, {{{27, 201}}})
+                   .IsApplicable(context.get(), transformation_context));
+
+  {
+    TransformationPropagateInstructionDown transformation(
+        18, 200, {{{22, 201}, {23, 202}, {202, 203}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+
+    ASSERT_TRUE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(201, {}), MakeDataDescriptor(202, {})));
+    ASSERT_TRUE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(201, {}), MakeDataDescriptor(200, {})));
+  }
+
+  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
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %9 = OpTypePointer Function %6
+          %4 = OpFunction %2 None %3
+
+          ; Has no instruction to propagate
+          %5 = OpLabel
+         %10 = OpVariable %9 Function
+          %8 = OpCopyObject %6 %7
+               OpStore %10 %8
+               OpBranch %11
+
+        ; Unreachable block
+        %100 = OpLabel
+        %101 = OpCopyObject %6 %7
+               OpBranch %11
+
+         ; Selection header
+         ;
+         ; One of acceptable successors has an OpPhi that uses propagated
+         ; instruction's id
+         %11 = OpLabel
+         %19 = OpCopyObject %6 %7
+               OpSelectionMerge %18 None
+               OpBranchConditional %13 %14 %18
+
+         ; %16 has no acceptable successors
+         %14 = OpLabel
+         %20 = OpPhi %6 %19 %11
+         %15 = OpCopyObject %6 %7 ; dependency
+               OpBranch %16
+         %16 = OpLabel
+         %17 = OpCopyObject %6 %15
+               OpBranch %18
+
+         ; Can be applied
+         %18 = OpLabel
+               OpSelectionMerge %24 None
+               OpBranchConditional %13 %22 %23
+         %22 = OpLabel
+         %29 = OpPhi %6 %7 %18
+        %201 = OpCopyObject %6 %7
+               OpStore %10 %201
+               OpBranch %24
+         %23 = OpLabel
+        %202 = OpCopyObject %6 %7
+               OpStore %10 %202
+               OpBranch %24
+         %24 = OpLabel
+        %200 = OpPhi %6 %201 %22 %202 %23
+               OpStore %10 %200
+               OpBranch %32
+
+         ; Can't replace all uses of the propagated instruction: %30 is
+         ; propagated into %27.
+         %32 = OpLabel
+               OpLoopMerge %28 %27 None
+               OpBranchConditional %13 %26 %28
+         %26 = OpLabel
+         %25 = OpCopyObject %6 %7
+         %30 = OpCopyObject %6 %25
+               OpBranchConditional %13 %27 %28
+         %27 = OpLabel
+               OpBranch %32
+         %28 = OpLabel
+         %31 = OpPhi %6 %30 %26 %7 %32 ; Can't replace this use
+               OpReturn
+
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, CantCreateOpPhiTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+
+          ; %5 doesn't belong to any construct
+          %5 = OpLabel
+         %15 = OpCopyObject %6 %7
+               OpBranch %16
+
+         ; The merge block (%19) is unreachable
+         %16 = OpLabel
+         %17 = OpCopyObject %6 %7
+               OpSelectionMerge %19 None
+               OpBranchConditional %13 %18 %18
+
+         ; %21 doesn't dominate the merge block - %20
+         %18 = OpLabel
+               OpSelectionMerge %20 None
+               OpBranchConditional %13 %20 %21
+         %21 = OpLabel
+         %22 = OpCopyObject %6 %7
+               OpBranch %20
+
+         ; The merge block (%24) is an acceptable successor of the propagated
+         ; instruction's block
+         %20 = OpLabel
+         %23 = OpCopyObject %6 %7
+               OpSelectionMerge %24 None
+               OpBranchConditional %13 %24 %30
+         %30 = OpLabel
+               OpBranch %24
+
+         ; One of the predecessors of the merge block is not dominated by any
+         ; successor of the propagated instruction's block
+         %24 = OpLabel
+         %26 = OpCopyObject %6 %7
+               OpLoopMerge %29 %25 None
+               OpBranch %25
+         %25 = OpLabel
+               OpBranchConditional %13 %24 %29
+         %28 = OpLabel ; unreachable predecessor of %29
+               OpBranch %29
+         %29 = OpLabel
+               OpReturn
+
+         %19 = OpLabel
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  TransformationPropagateInstructionDown transformations[] = {
+      // %5 doesn't belong to any construct.
+      {5, 200, {{{16, 201}}}},
+
+      // The merge block (%19) is unreachable.
+      {16, 200, {{{18, 202}}}},
+
+      // %21 doesn't dominate the merge block - %20.
+      {21, 200, {{{20, 203}}}},
+
+      // The merge block (%24) is an acceptable successor of the propagated
+      // instruction's block.
+      {20, 200, {{{24, 204}, {30, 205}}}},
+
+      // One of the predecessors of the merge block is not dominated by any
+      // successor of the propagated instruction's block.
+      {24, 200, {{{25, 206}}}},
+  };
+
+  for (const auto& transformation : transformations) {
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  // No transformation has introduced an OpPhi instruction.
+  ASSERT_FALSE(context->get_def_use_mgr()->GetDef(200));
+
+  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
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+
+          ; %5 doesn't belong to any construct
+          %5 = OpLabel
+               OpBranch %16
+
+         ; The merge block (%19) is unreachable
+         %16 = OpLabel
+        %201 = OpCopyObject %6 %7
+               OpSelectionMerge %19 None
+               OpBranchConditional %13 %18 %18
+
+         ; %21 doesn't dominate the merge block - %20
+         %18 = OpLabel
+        %202 = OpCopyObject %6 %7
+               OpSelectionMerge %20 None
+               OpBranchConditional %13 %20 %21
+         %21 = OpLabel
+               OpBranch %20
+
+         ; The merge block (%24) is an acceptable successor of the propagated
+         ; instruction's block
+         %20 = OpLabel
+        %203 = OpCopyObject %6 %7
+               OpSelectionMerge %24 None
+               OpBranchConditional %13 %24 %30
+         %30 = OpLabel
+        %205 = OpCopyObject %6 %7
+               OpBranch %24
+
+         ; One of the predecessors of the merge block is not dominated by any
+         ; successor of the propagated instruction's block
+         %24 = OpLabel
+        %204 = OpCopyObject %6 %7
+               OpLoopMerge %29 %25 None
+               OpBranch %25
+         %25 = OpLabel
+        %206 = OpCopyObject %6 %7
+               OpBranchConditional %13 %24 %29
+         %28 = OpLabel ; unreachable predecessor of %29
+               OpBranch %29
+         %29 = OpLabel
+               OpReturn
+
+         %19 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, VariablePointersCapability) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+         %10 = OpTypePointer Workgroup %6
+         %11 = OpVariable %10 Workgroup
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %18 = OpCopyObject %10 %11
+         %14 = OpCopyObject %10 %11
+               OpSelectionMerge %17 None
+               OpBranchConditional %13 %15 %16
+         %15 = OpLabel
+               OpBranch %17
+         %16 = OpLabel
+               OpBranch %17
+         %17 = OpLabel
+               OpStore %18 %7
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  {
+    // Can propagate a pointer only if we don't have to create an OpPhi.
+    TransformationPropagateInstructionDown transformation(
+        5, 200, {{{15, 201}, {16, 202}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+    ASSERT_FALSE(context->get_def_use_mgr()->GetDef(200));
+  }
+  {
+    // Can't propagate a pointer if there is no VariablePointersStorageBuffer
+    // capability and we need to create an OpPhi.
+    TransformationPropagateInstructionDown transformation(
+        5, 200, {{{15, 203}, {16, 204}}});
+    ASSERT_FALSE(context->get_feature_mgr()->HasCapability(
+        SpvCapabilityVariablePointersStorageBuffer));
+    ASSERT_FALSE(
+        transformation.IsApplicable(context.get(), transformation_context));
+
+    context->AddCapability(SpvCapabilityVariablePointers);
+    ASSERT_TRUE(context->get_feature_mgr()->HasCapability(
+        SpvCapabilityVariablePointersStorageBuffer));
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+               OpCapability VariablePointers
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+         %10 = OpTypePointer Workgroup %6
+         %11 = OpVariable %10 Workgroup
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpSelectionMerge %17 None
+               OpBranchConditional %13 %15 %16
+         %15 = OpLabel
+        %203 = OpCopyObject %10 %11
+        %201 = OpCopyObject %10 %11
+               OpBranch %17
+         %16 = OpLabel
+        %204 = OpCopyObject %10 %11
+        %202 = OpCopyObject %10 %11
+               OpBranch %17
+         %17 = OpLabel
+        %200 = OpPhi %10 %203 %15 %204 %16
+               OpStore %200 %7
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, UseOverflowIdsTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+         %10 = OpTypePointer Private %6
+         %11 = OpVariable %10 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %20 = OpCopyObject %6 %7
+               OpSelectionMerge %23 None
+               OpBranchConditional %13 %21 %22
+         %21 = OpLabel
+               OpStore %11 %20
+               OpBranch %23
+         %22 = OpLabel
+               OpStore %11 %20
+               OpBranch %23
+         %23 = OpLabel
+               OpStore %11 %20
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options,
+      MakeUnique<CounterOverflowIdSource>(300));
+
+  TransformationPropagateInstructionDown transformation(5, 200, {{{21, 201}}});
+  ASSERT_TRUE(
+      transformation.IsApplicable(context.get(), transformation_context));
+  ApplyAndCheckFreshIds(transformation, context.get(), &transformation_context,
+                        {300});
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  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
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+         %10 = OpTypePointer Private %6
+         %11 = OpVariable %10 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpSelectionMerge %23 None
+               OpBranchConditional %13 %21 %22
+         %21 = OpLabel
+        %201 = OpCopyObject %6 %7
+               OpStore %11 %201
+               OpBranch %23
+         %22 = OpLabel
+        %300 = OpCopyObject %6 %7
+               OpStore %11 %300
+               OpBranch %23
+         %23 = OpLabel
+        %200 = OpPhi %6 %201 %21 %300 %22
+               OpStore %11 %200
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, TestCreatedFacts) {
+  std::string shader = R"(
+               OpCapability VariablePointers
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+         %10 = OpTypePointer Private %6
+         %11 = OpVariable %10 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %20 = OpCopyObject %6 %7
+         %24 = OpCopyObject %6 %7 ; Irrelevant id
+         %25 = OpCopyObject %10 %11 ; Pointee is irrelevant
+               OpSelectionMerge %23 None
+               OpBranchConditional %13 %21 %22
+         %21 = OpLabel
+               OpStore %25 %20
+               OpBranch %23
+         %22 = OpLabel ; Dead block
+               OpStore %25 %20
+               OpBranch %23
+         %23 = OpLabel
+               OpStore %25 %20
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  transformation_context.GetFactManager()->AddFactBlockIsDead(22);
+  transformation_context.GetFactManager()->AddFactIdIsIrrelevant(24);
+  transformation_context.GetFactManager()->AddFactValueOfPointeeIsIrrelevant(
+      25);
+
+  {
+    // Propagate pointer with PointeeIsIrrelevant fact.
+    TransformationPropagateInstructionDown transformation(
+        5, 200, {{{21, 201}, {22, 202}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+
+    ASSERT_FALSE(transformation_context.GetFactManager()->IdIsIrrelevant(201));
+    ASSERT_FALSE(transformation_context.GetFactManager()->IdIsIrrelevant(202));
+    ASSERT_FALSE(transformation_context.GetFactManager()->IdIsIrrelevant(200));
+
+    ASSERT_TRUE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(201));
+    ASSERT_TRUE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(202));
+    ASSERT_TRUE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(200));
+
+    ASSERT_TRUE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(201, {}), MakeDataDescriptor(202, {})));
+    ASSERT_TRUE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(201, {}), MakeDataDescriptor(200, {})));
+  }
+  {
+    // Propagate an irrelevant id.
+    TransformationPropagateInstructionDown transformation(
+        5, 203, {{{21, 204}, {22, 205}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+
+    ASSERT_TRUE(transformation_context.GetFactManager()->IdIsIrrelevant(203));
+    ASSERT_TRUE(transformation_context.GetFactManager()->IdIsIrrelevant(204));
+    ASSERT_TRUE(transformation_context.GetFactManager()->IdIsIrrelevant(205));
+
+    ASSERT_FALSE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(203));
+    ASSERT_FALSE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(204));
+    ASSERT_FALSE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(205));
+
+    ASSERT_FALSE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(204, {}), MakeDataDescriptor(205, {})));
+    ASSERT_FALSE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(204, {}), MakeDataDescriptor(203, {})));
+  }
+  {
+    // Propagate a regular id.
+    TransformationPropagateInstructionDown transformation(
+        5, 206, {{{21, 207}, {22, 208}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+
+    ASSERT_FALSE(transformation_context.GetFactManager()->IdIsIrrelevant(206));
+    ASSERT_FALSE(transformation_context.GetFactManager()->IdIsIrrelevant(207));
+    ASSERT_TRUE(transformation_context.GetFactManager()->IdIsIrrelevant(208));
+
+    ASSERT_FALSE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(206));
+    ASSERT_FALSE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(207));
+    ASSERT_FALSE(
+        transformation_context.GetFactManager()->PointeeValueIsIrrelevant(208));
+
+    ASSERT_TRUE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(206, {}), MakeDataDescriptor(207, {})));
+    ASSERT_FALSE(transformation_context.GetFactManager()->IsSynonymous(
+        MakeDataDescriptor(206, {}), MakeDataDescriptor(208, {})));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability VariablePointers
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+         %10 = OpTypePointer Private %6
+         %11 = OpVariable %10 Private
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpSelectionMerge %23 None
+               OpBranchConditional %13 %21 %22
+         %21 = OpLabel
+        %207 = OpCopyObject %6 %7
+        %204 = OpCopyObject %6 %7 ; Irrelevant id
+        %201 = OpCopyObject %10 %11 ; Pointee is irrelevant
+               OpStore %201 %207
+               OpBranch %23
+         %22 = OpLabel ; Dead block
+        %208 = OpCopyObject %6 %7
+        %205 = OpCopyObject %6 %7 ; Irrelevant id
+        %202 = OpCopyObject %10 %11 ; Pointee is irrelevant
+               OpStore %202 %208
+               OpBranch %23
+         %23 = OpLabel
+        %206 = OpPhi %6 %207 %21 %208 %22
+        %203 = OpPhi %6 %204 %21 %205 %22 ; Irrelevant id
+        %200 = OpPhi %10 %201 %21 %202 %22 ; Pointee is irrelevant
+               OpStore %200 %206
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, TestLoops1) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+               OpLoopMerge %26 %25 None
+               OpBranch %21
+
+         %21 = OpLabel
+         %22 = OpCopyObject %6 %7
+         %31 = OpCopyObject %6 %7
+               OpSelectionMerge %35 None
+               OpBranchConditional %13 %23 %24
+
+         %23 = OpLabel
+         %27 = OpCopyObject %6 %22
+         %32 = OpCopyObject %6 %31
+               OpBranch %26
+         %24 = OpLabel
+         %28 = OpCopyObject %6 %22
+         %33 = OpCopyObject %6 %31
+               OpBranchConditional %13 %26 %25
+
+         %35 = OpLabel
+               OpBranch %25
+
+         %25 = OpLabel
+         %29 = OpCopyObject %6 %22
+         %34 = OpCopyObject %6 %31
+               OpBranch %20
+         %26 = OpLabel
+         %30 = OpCopyObject %6 %22
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  {
+    TransformationPropagateInstructionDown transformation(
+        21, 200, {{{23, 201}, {24, 202}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  // Can't replace usage of %22 in %26.
+  ASSERT_FALSE(
+      TransformationPropagateInstructionDown(21, 200, {{{23, 201}, {24, 202}}})
+          .IsApplicable(context.get(), transformation_context));
+
+  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
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+               OpLoopMerge %26 %25 None
+               OpBranch %21
+
+         %21 = OpLabel
+         %22 = OpCopyObject %6 %7
+               OpSelectionMerge %35 None
+               OpBranchConditional %13 %23 %24
+         %23 = OpLabel
+        %201 = OpCopyObject %6 %7
+         %27 = OpCopyObject %6 %22
+         %32 = OpCopyObject %6 %201
+               OpBranch %26
+         %24 = OpLabel
+        %202 = OpCopyObject %6 %7
+         %28 = OpCopyObject %6 %22
+         %33 = OpCopyObject %6 %202
+               OpBranchConditional %13 %26 %25
+
+         %35 = OpLabel
+               OpBranch %25
+
+         %25 = OpLabel
+         %29 = OpCopyObject %6 %22
+         %34 = OpCopyObject %6 %202
+               OpBranch %20
+         %26 = OpLabel
+         %30 = OpCopyObject %6 %22
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, TestLoops2) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+         %23 = OpPhi %6 %7 %5 %24 %21
+               OpLoopMerge %22 %21 None
+               OpBranch %21
+
+         %21 = OpLabel
+         %24 = OpCopyObject %6 %23
+         %25 = OpCopyObject %6 %7
+               OpBranchConditional %13 %22 %20
+
+         %22 = OpLabel
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  {
+    // Can propagate %25 from %21 into %20.
+    TransformationPropagateInstructionDown transformation(
+        21, 200, {{{20, 201}, {22, 202}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    // Can propagate %201 from %20 into %21.
+    TransformationPropagateInstructionDown transformation(20, 200,
+                                                          {{{21, 203}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  // Can't propagate %24 from %21 into %20.
+  ASSERT_FALSE(
+      TransformationPropagateInstructionDown(21, 200, {{{20, 204}, {22, 205}}})
+          .IsApplicable(context.get(), transformation_context));
+
+  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
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+         %23 = OpPhi %6 %7 %5 %24 %21
+               OpLoopMerge %22 %21 None
+               OpBranch %21
+
+         %21 = OpLabel
+        %203 = OpCopyObject %6 %7
+         %24 = OpCopyObject %6 %23
+               OpBranchConditional %13 %22 %20
+
+         %22 = OpLabel
+        %200 = OpPhi %6 %203 %21
+        %202 = OpCopyObject %6 %7
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationPropagateInstructionDownTest, TestLoops3) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+         %27 = OpPhi %6 %7 %5 %26 %20
+         %25 = OpCopyObject %6 %7
+         %26 = OpCopyObject %6 %7
+               OpLoopMerge %22 %20 None
+               OpBranchConditional %13 %20 %22
+
+         %22 = OpLabel
+               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()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  {
+    // Propagate %25 into %20 and %22. Not that we are skipping %26 since not
+    // all of its users are in different blocks (%27).h
+    TransformationPropagateInstructionDown transformation(
+        20, 200, {{{20, 201}, {22, 202}}});
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  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
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpConstant %6 1
+         %12 = OpTypeBool
+         %13 = OpConstantTrue %12
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+         %27 = OpPhi %6 %7 %5 %26 %20
+        %201 = OpCopyObject %6 %7
+         %26 = OpCopyObject %6 %7
+               OpLoopMerge %22 %20 None
+               OpBranchConditional %13 %20 %22
+
+         %22 = OpLabel
+        %202 = OpCopyObject %6 %7
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools