spirv-fuzz: Replace dead-block terminators with OpKill etc. (#3882)

Fixes #3615.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 2a9a95d..6843eb8 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -104,6 +104,7 @@
         fuzzer_pass_propagate_instructions_up.h
         fuzzer_pass_push_ids_through_variables.h
         fuzzer_pass_replace_adds_subs_muls_with_carrying_extended.h
+        fuzzer_pass_replace_branches_from_dead_blocks_with_exits.h
         fuzzer_pass_replace_copy_memories_with_loads_stores.h
         fuzzer_pass_replace_copy_objects_with_stores_loads.h
         fuzzer_pass_replace_irrelevant_ids.h
@@ -197,6 +198,7 @@
         transformation_record_synonymous_constants.h
         transformation_replace_add_sub_mul_with_carrying_extended.h
         transformation_replace_boolean_constant_with_constant_binary.h
+        transformation_replace_branch_from_dead_block_with_exit.h
         transformation_replace_constant_with_uniform.h
         transformation_replace_copy_memory_with_load_store.h
         transformation_replace_copy_object_with_store_load.h
@@ -288,6 +290,7 @@
         fuzzer_pass_propagate_instructions_up.cpp
         fuzzer_pass_push_ids_through_variables.cpp
         fuzzer_pass_replace_adds_subs_muls_with_carrying_extended.cpp
+        fuzzer_pass_replace_branches_from_dead_blocks_with_exits.cpp
         fuzzer_pass_replace_copy_memories_with_loads_stores.cpp
         fuzzer_pass_replace_copy_objects_with_stores_loads.cpp
         fuzzer_pass_replace_irrelevant_ids.cpp
@@ -379,6 +382,7 @@
         transformation_record_synonymous_constants.cpp
         transformation_replace_add_sub_mul_with_carrying_extended.cpp
         transformation_replace_boolean_constant_with_constant_binary.cpp
+        transformation_replace_branch_from_dead_block_with_exit.cpp
         transformation_replace_constant_with_uniform.cpp
         transformation_replace_copy_memory_with_load_store.cpp
         transformation_replace_copy_object_with_store_load.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index 7d9c9c4..218c059 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -72,6 +72,7 @@
 #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"
+#include "source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.h"
 #include "source/fuzz/fuzzer_pass_replace_copy_memories_with_loads_stores.h"
 #include "source/fuzz/fuzzer_pass_replace_copy_objects_with_stores_loads.h"
 #include "source/fuzz/fuzzer_pass_replace_irrelevant_ids.h"
@@ -282,6 +283,8 @@
     MaybeAddRepeatedPass<FuzzerPassPushIdsThroughVariables>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceAddsSubsMulsWithCarryingExtended>(
         &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassReplaceBranchesFromDeadBlocksWithExits>(
+        &pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceCopyMemoriesWithLoadsStores>(
         &pass_instances);
     MaybeAddRepeatedPass<FuzzerPassReplaceCopyObjectsWithStoresLoads>(
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 1250cba..3f0d10e 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -114,6 +114,8 @@
 const std::pair<uint32_t, uint32_t> kChanceOfPushingIdThroughVariable = {5, 50};
 const std::pair<uint32_t, uint32_t>
     kChanceOfReplacingAddSubMulWithCarryingExtended = {20, 70};
+const std::pair<uint32_t, uint32_t>
+    kChanceOfReplacingBranchFromDeadBlockWithExit = {10, 65};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingCopyMemoryWithLoadStore =
     {20, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingCopyObjectWithStoreLoad =
@@ -304,6 +306,8 @@
       ChooseBetweenMinAndMax(kChanceOfPushingIdThroughVariable);
   chance_of_replacing_add_sub_mul_with_carrying_extended_ =
       ChooseBetweenMinAndMax(kChanceOfReplacingAddSubMulWithCarryingExtended);
+  chance_of_replacing_branch_from_dead_block_with_exit_ =
+      ChooseBetweenMinAndMax(kChanceOfReplacingBranchFromDeadBlockWithExit);
   chance_of_replacing_copy_memory_with_load_store_ =
       ChooseBetweenMinAndMax(kChanceOfReplacingCopyMemoryWithLoadStore);
   chance_of_replacing_copyobject_with_store_load_ =
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index 01a92f7..147c66d 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -276,6 +276,9 @@
   uint32_t GetChanceOfReplacingAddSubMulWithCarryingExtended() {
     return chance_of_replacing_add_sub_mul_with_carrying_extended_;
   }
+  uint32_t GetChanceOfReplacingBranchFromDeadBlockWithExit() {
+    return chance_of_replacing_branch_from_dead_block_with_exit_;
+  }
   uint32_t GetChanceOfReplacingCopyMemoryWithLoadStore() {
     return chance_of_replacing_copy_memory_with_load_store_;
   }
@@ -470,6 +473,7 @@
   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_;
+  uint32_t chance_of_replacing_branch_from_dead_block_with_exit_;
   uint32_t chance_of_replacing_copy_memory_with_load_store_;
   uint32_t chance_of_replacing_copyobject_with_store_load_;
   uint32_t chance_of_replacing_id_with_synonym_;
diff --git a/source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.cpp b/source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.cpp
new file mode 100644
index 0000000..200aaad
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.cpp
@@ -0,0 +1,132 @@
+// Copyright (c) 2020 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_replace_branches_from_dead_blocks_with_exits.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassReplaceBranchesFromDeadBlocksWithExits::
+    FuzzerPassReplaceBranchesFromDeadBlocksWithExits(
+        opt::IRContext* ir_context,
+        TransformationContext* transformation_context,
+        FuzzerContext* fuzzer_context,
+        protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, transformation_context, fuzzer_context,
+                 transformations) {}
+
+FuzzerPassReplaceBranchesFromDeadBlocksWithExits::
+    ~FuzzerPassReplaceBranchesFromDeadBlocksWithExits() = default;
+
+void FuzzerPassReplaceBranchesFromDeadBlocksWithExits::Apply() {
+  // OpKill can only be used as a terminator in a function that is guaranteed
+  // to be executed with the Fragment execution model.  We conservatively only
+  // allow OpKill if every entry point in the module has the Fragment execution
+  // model.
+  auto fragment_execution_model_guaranteed =
+      std::all_of(GetIRContext()->module()->entry_points().begin(),
+                  GetIRContext()->module()->entry_points().end(),
+                  [](const opt::Instruction& entry_point) -> bool {
+                    return entry_point.GetSingleWordInOperand(0) ==
+                           SpvExecutionModelFragment;
+                  });
+
+  // Transformations of this type can disable one another.  To avoid ordering
+  // bias, we therefore build a set of candidate transformations to apply, and
+  // subsequently apply them in a random order, skipping any that cease to be
+  // applicable.
+  std::vector<TransformationReplaceBranchFromDeadBlockWithExit>
+      candidate_transformations;
+
+  // Consider every block in every function.
+  for (auto& function : *GetIRContext()->module()) {
+    for (auto& block : function) {
+      // Probabilistically decide whether to skip this block.
+      if (GetFuzzerContext()->ChoosePercentage(
+              GetFuzzerContext()
+                  ->GetChanceOfReplacingBranchFromDeadBlockWithExit())) {
+        continue;
+      }
+      // Check whether the block is suitable for having its terminator replaced.
+      if (!TransformationReplaceBranchFromDeadBlockWithExit::BlockIsSuitable(
+              GetIRContext(), *GetTransformationContext(), block)) {
+        continue;
+      }
+      // We can always use OpUnreachable to replace a block's terminator.
+      // Whether we can use OpKill depends on the execution model, and which of
+      // OpReturn and OpReturnValue we can use depends on the return type of the
+      // enclosing function.
+      std::vector<SpvOp> opcodes = {SpvOpUnreachable};
+      if (fragment_execution_model_guaranteed) {
+        opcodes.emplace_back(SpvOpKill);
+      }
+      auto function_return_type =
+          GetIRContext()->get_type_mgr()->GetType(function.type_id());
+      if (function_return_type->AsVoid()) {
+        opcodes.emplace_back(SpvOpReturn);
+      } else if (fuzzerutil::CanCreateConstant(*function_return_type)) {
+        // For simplicity we only allow OpReturnValue if the function return
+        // type is a type for which we can create a constant.  This allows us a
+        // zero of the given type as a default return value.
+        opcodes.emplace_back(SpvOpReturnValue);
+      }
+      // Choose one of the available terminator opcodes at random and create a
+      // candidate transformation.
+      auto opcode = opcodes[GetFuzzerContext()->RandomIndex(opcodes)];
+      candidate_transformations.emplace_back(
+          TransformationReplaceBranchFromDeadBlockWithExit(
+              block.id(), opcode,
+              opcode == SpvOpReturnValue
+                  ? FindOrCreateZeroConstant(function.type_id(), true)
+                  : 0));
+    }
+  }
+
+  // Process the candidate transformations in a random order.
+  while (!candidate_transformations.empty()) {
+    // Transformations of this type can disable one another.  For example,
+    // suppose we have dead blocks A, B, C, D arranged as follows:
+    //
+    //         A         |
+    //        / \        |
+    //       B   C       |
+    //        \ /        |
+    //         D         |
+    //
+    // Here we can replace the terminator of either B or C with an early exit,
+    // because D has two predecessors.  But if we replace the terminator of B,
+    // say, we get:
+    //
+    //         A         |
+    //        / \        |
+    //       B   C       |
+    //          /        |
+    //         D         |
+    //
+    // and now it is no longer OK to replace the terminator of C as D only has
+    // one predecessor and we do not want to make D unreachable in the control
+    // flow graph.
+    MaybeApplyTransformation(
+        GetFuzzerContext()->RemoveAtRandomIndex(&candidate_transformations));
+  }
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.h b/source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.h
new file mode 100644
index 0000000..62164b3
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.h
@@ -0,0 +1,41 @@
+// Copyright (c) 2020 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_REPLACE_BRANCHES_FROM_DEAD_BLOCKS_WITH_EXITS_H_
+#define SOURCE_FUZZ_FUZZER_PASS_REPLACE_BRANCHES_FROM_DEAD_BLOCKS_WITH_EXITS_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// Fuzzer pass that, under the right conditions, replaces branch instructions
+// from dead blocks with non-branching "exit" terminators, such as OpKill and
+// OpReturn.
+class FuzzerPassReplaceBranchesFromDeadBlocksWithExits : public FuzzerPass {
+ public:
+  FuzzerPassReplaceBranchesFromDeadBlocksWithExits(
+      opt::IRContext* ir_context, TransformationContext* transformation_context,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassReplaceBranchesFromDeadBlocksWithExits() override;
+
+  void Apply() override;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_REPLACE_BRANCHES_FROM_DEAD_BLOCKS_WITH_EXITS_H_
diff --git a/source/fuzz/pass_management/repeated_pass_instances.h b/source/fuzz/pass_management/repeated_pass_instances.h
index 8d2c0f8..6809d07 100644
--- a/source/fuzz/pass_management/repeated_pass_instances.h
+++ b/source/fuzz/pass_management/repeated_pass_instances.h
@@ -58,6 +58,7 @@
 #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"
+#include "source/fuzz/fuzzer_pass_replace_branches_from_dead_blocks_with_exits.h"
 #include "source/fuzz/fuzzer_pass_replace_copy_memories_with_loads_stores.h"
 #include "source/fuzz/fuzzer_pass_replace_copy_objects_with_stores_loads.h"
 #include "source/fuzz/fuzzer_pass_replace_irrelevant_ids.h"
@@ -150,6 +151,7 @@
   REPEATED_PASS_INSTANCE(PropagateInstructionsUp);
   REPEATED_PASS_INSTANCE(PushIdsThroughVariables);
   REPEATED_PASS_INSTANCE(ReplaceAddsSubsMulsWithCarryingExtended);
+  REPEATED_PASS_INSTANCE(ReplaceBranchesFromDeadBlocksWithExits);
   REPEATED_PASS_INSTANCE(ReplaceCopyMemoriesWithLoadsStores);
   REPEATED_PASS_INSTANCE(ReplaceCopyObjectsWithStoresLoads);
   REPEATED_PASS_INSTANCE(ReplaceLoadsStoresWithCopyMemories);
diff --git a/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp b/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
index a6f024b..060c743 100644
--- a/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
+++ b/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
@@ -63,10 +63,12 @@
     // - Dead blocks are great for adding function calls
     // - Dead blocks are also great for adding loads and stores
     // - The guard associated with a dead block can be obfuscated
-    return RandomOrderAndNonNull({pass_instances_->GetAddFunctionCalls(),
-                                  pass_instances_->GetAddLoads(),
-                                  pass_instances_->GetAddStores(),
-                                  pass_instances_->GetObfuscateConstants()});
+    // - Branches from dead blocks may be replaced with exits
+    return RandomOrderAndNonNull(
+        {pass_instances_->GetAddFunctionCalls(), pass_instances_->GetAddLoads(),
+         pass_instances_->GetAddStores(),
+         pass_instances_->GetObfuscateConstants(),
+         pass_instances_->GetReplaceBranchesFromDeadBlocksWithExits()});
   }
   if (&pass == pass_instances_->GetAddDeadBreaks()) {
     // - The guard of the dead break is a good candidate for obfuscation
@@ -189,9 +191,12 @@
     // - New functions in the module can be called
     // - Donated dead functions produce irrelevant ids, which can be replaced
     // - Donated functions are good candidates for having their returns merged
-    return RandomOrderAndNonNull({pass_instances_->GetAddFunctionCalls(),
-                                  pass_instances_->GetReplaceIrrelevantIds(),
-                                  pass_instances_->GetMergeFunctionReturns()});
+    // - Donated dead functions may allow branches to be replaced with exits
+    return RandomOrderAndNonNull(
+        {pass_instances_->GetAddFunctionCalls(),
+         pass_instances_->GetReplaceIrrelevantIds(),
+         pass_instances_->GetMergeFunctionReturns(),
+         pass_instances_->GetReplaceBranchesFromDeadBlocksWithExits()});
   }
   if (&pass == pass_instances_->GetDuplicateRegionsWithSelections()) {
     // - Parts of duplicated regions can be outlined
@@ -274,6 +279,11 @@
     // No obvious follow-on passes
     return {};
   }
+  if (&pass == pass_instances_->GetReplaceBranchesFromDeadBlocksWithExits()) {
+    // - Changing a branch to OpReturnValue introduces an irrelevant id, which
+    //   can be replaced
+    return RandomOrderAndNonNull({pass_instances_->GetReplaceIrrelevantIds()});
+  }
   if (&pass == pass_instances_->GetReplaceCopyMemoriesWithLoadsStores()) {
     // No obvious follow-on passes
     return {};
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index 20ab98e..e99d5f0 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -553,6 +553,7 @@
     TransformationWrapRegionInSelection wrap_region_in_selection = 79;
     TransformationAddEarlyTerminatorWrapper add_early_terminator_wrapper = 80;
     TransformationPropagateInstructionDown propagate_instruction_down = 81;
+    TransformationReplaceBranchFromDeadBlockWithExit replace_branch_from_dead_block_with_exit = 82;
     // Add additional option using the next available number.
   }
 }
@@ -1863,6 +1864,24 @@
 
 }
 
+message TransformationReplaceBranchFromDeadBlockWithExit {
+
+  // Given a dead block that ends with OpBranch, replaces OpBranch with an
+  // "exit" instruction; one of OpReturn/OpReturnValue, OpKill (in a fragment
+  // shader) or OpUnreachable.
+
+  // The dead block whose terminator is to be replaced.
+  uint32 block_id = 1;
+
+  // The opcode of the new terminator.
+  uint32 opcode = 2;
+
+  // Ignored unless opcode is OpReturnValue, in which case this field provides
+  // a suitable result id to be returned.
+  uint32 return_value_id = 3;
+
+}
+
 message TransformationReplaceParameterWithGlobal {
 
   // Removes parameter with result id |parameter_id| from its function
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index cc3d010..28903a2 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -76,6 +76,7 @@
 #include "source/fuzz/transformation_record_synonymous_constants.h"
 #include "source/fuzz/transformation_replace_add_sub_mul_with_carrying_extended.h"
 #include "source/fuzz/transformation_replace_boolean_constant_with_constant_binary.h"
+#include "source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h"
 #include "source/fuzz/transformation_replace_constant_with_uniform.h"
 #include "source/fuzz/transformation_replace_copy_memory_with_load_store.h"
 #include "source/fuzz/transformation_replace_copy_object_with_store_load.h"
@@ -283,6 +284,10 @@
       return MakeUnique<TransformationReplaceBooleanConstantWithConstantBinary>(
           message.replace_boolean_constant_with_constant_binary());
     case protobufs::Transformation::TransformationCase::
+        kReplaceBranchFromDeadBlockWithExit:
+      return MakeUnique<TransformationReplaceBranchFromDeadBlockWithExit>(
+          message.replace_branch_from_dead_block_with_exit());
+    case protobufs::Transformation::TransformationCase::
         kReplaceConstantWithUniform:
       return MakeUnique<TransformationReplaceConstantWithUniform>(
           message.replace_constant_with_uniform());
diff --git a/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp
new file mode 100644
index 0000000..561e8b6
--- /dev/null
+++ b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp
@@ -0,0 +1,169 @@
+// Copyright (c) 2020 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_replace_branch_from_dead_block_with_exit.h"
+
+#include "source/fuzz/fuzzer_util.h"
+
+namespace spvtools {
+namespace fuzz {
+
+TransformationReplaceBranchFromDeadBlockWithExit::
+    TransformationReplaceBranchFromDeadBlockWithExit(
+        const spvtools::fuzz::protobufs::
+            TransformationReplaceBranchFromDeadBlockWithExit& message)
+    : message_(message) {}
+
+TransformationReplaceBranchFromDeadBlockWithExit::
+    TransformationReplaceBranchFromDeadBlockWithExit(uint32_t block_id,
+                                                     SpvOp opcode,
+                                                     uint32_t return_value_id) {
+  message_.set_block_id(block_id);
+  message_.set_opcode(opcode);
+  message_.set_return_value_id(return_value_id);
+}
+
+bool TransformationReplaceBranchFromDeadBlockWithExit::IsApplicable(
+    opt::IRContext* ir_context,
+    const TransformationContext& transformation_context) const {
+  // The block whose terminator is to be changed must exist.
+  auto block = ir_context->get_instr_block(message_.block_id());
+  if (!block) {
+    return false;
+  }
+  if (!BlockIsSuitable(ir_context, transformation_context, *block)) {
+    return false;
+  }
+  auto function_return_type_id = block->GetParent()->type_id();
+  switch (message_.opcode()) {
+    case SpvOpKill:
+      for (auto& entry_point : ir_context->module()->entry_points()) {
+        if (entry_point.GetSingleWordInOperand(0) !=
+            SpvExecutionModelFragment) {
+          // OpKill is only allowed in a fragment shader.  This is a
+          // conservative check: if the module contains a non-fragment entry
+          // point then adding an OpKill might lead to OpKill being used in a
+          // non-fragment shader.
+          return false;
+        }
+      }
+      break;
+    case SpvOpReturn:
+      if (ir_context->get_def_use_mgr()
+              ->GetDef(function_return_type_id)
+              ->opcode() != SpvOpTypeVoid) {
+        // OpReturn is only allowed in a function with void return type.
+        return false;
+      }
+      break;
+    case SpvOpReturnValue: {
+      // If the terminator is to be changed to OpReturnValue, with
+      // |message_.return_value_id| being the value that will be returned, then
+      // |message_.return_value_id| must have a compatible type and be available
+      // at the block terminator.
+      auto return_value =
+          ir_context->get_def_use_mgr()->GetDef(message_.return_value_id());
+      if (!return_value || return_value->type_id() != function_return_type_id) {
+        return false;
+      }
+      if (!fuzzerutil::IdIsAvailableBeforeInstruction(
+              ir_context, block->terminator(), message_.return_value_id())) {
+        return false;
+      }
+      break;
+    }
+    default:
+      assert(message_.opcode() == SpvOpUnreachable &&
+             "Invalid early exit opcode.");
+      break;
+  }
+  return true;
+}
+
+void TransformationReplaceBranchFromDeadBlockWithExit::Apply(
+    opt::IRContext* ir_context, TransformationContext* /*unused*/) const {
+  // If the successor block has OpPhi instructions then arguments related to
+  // |message_.block_id| need to be removed from these instruction.
+  auto block = ir_context->get_instr_block(message_.block_id());
+  assert(block->terminator()->opcode() == SpvOpBranch &&
+         "Precondition: the block must end with OpBranch.");
+  auto successor = ir_context->get_instr_block(
+      block->terminator()->GetSingleWordInOperand(0));
+  successor->ForEachPhiInst([block](opt::Instruction* phi_inst) {
+    opt::Instruction::OperandList new_phi_in_operands;
+    for (uint32_t i = 0; i < phi_inst->NumInOperands(); i += 2) {
+      if (phi_inst->GetSingleWordInOperand(i + 1) == block->id()) {
+        continue;
+      }
+      new_phi_in_operands.emplace_back(phi_inst->GetInOperand(i));
+      new_phi_in_operands.emplace_back(phi_inst->GetInOperand(i + 1));
+    }
+    assert(new_phi_in_operands.size() == phi_inst->NumInOperands() - 2);
+    phi_inst->SetInOperands(std::move(new_phi_in_operands));
+  });
+
+  // Rewrite the terminator of |message_.block_id|.
+  opt::Instruction::OperandList new_terminator_in_operands;
+  if (message_.opcode() == SpvOpReturnValue) {
+    new_terminator_in_operands.push_back(
+        {SPV_OPERAND_TYPE_TYPE_ID, {message_.return_value_id()}});
+  }
+  auto terminator = block->terminator();
+  terminator->SetOpcode(static_cast<SpvOp>(message_.opcode()));
+  terminator->SetInOperands(std::move(new_terminator_in_operands));
+  ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+}
+
+std::unordered_set<uint32_t>
+TransformationReplaceBranchFromDeadBlockWithExit::GetFreshIds() const {
+  return std::unordered_set<uint32_t>();
+}
+
+protobufs::Transformation
+TransformationReplaceBranchFromDeadBlockWithExit::ToMessage() const {
+  protobufs::Transformation result;
+  *result.mutable_replace_branch_from_dead_block_with_exit() = message_;
+  return result;
+}
+
+bool TransformationReplaceBranchFromDeadBlockWithExit::BlockIsSuitable(
+    opt::IRContext* ir_context,
+    const TransformationContext& transformation_context,
+    const opt::BasicBlock& block) {
+  // The block must be dead.
+  if (!transformation_context.GetFactManager()->BlockIsDead(block.id())) {
+    return false;
+  }
+  // The block's terminator must be OpBranch.
+  if (block.terminator()->opcode() != SpvOpBranch) {
+    return false;
+  }
+  if (ir_context->GetStructuredCFGAnalysis()->IsInContinueConstruct(
+          block.id())) {
+    // Early exits from continue constructs are not allowed as they would break
+    // the SPIR-V structured control flow rules.
+    return false;
+  }
+  // We only allow changing OpBranch to an early terminator if the target of the
+  // OpBranch has at least one other predecessor.
+  auto successor = ir_context->get_instr_block(
+      block.terminator()->GetSingleWordInOperand(0));
+  if (ir_context->cfg()->preds(successor->id()).size() < 2) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h
new file mode 100644
index 0000000..e1418c9
--- /dev/null
+++ b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h
@@ -0,0 +1,84 @@
+// Copyright (c) 2020 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_REPLACE_BRANCH_FROM_DEAD_BLOCK_WITH_EXIT_H_
+#define SOURCE_FUZZ_TRANSFORMATION_REPLACE_BRANCH_FROM_DEAD_BLOCK_WITH_EXIT_H_
+
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/fuzz/transformation_context.h"
+#include "source/opt/basic_block.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationReplaceBranchFromDeadBlockWithExit : public Transformation {
+ public:
+  explicit TransformationReplaceBranchFromDeadBlockWithExit(
+      const protobufs::TransformationReplaceBranchFromDeadBlockWithExit&
+          message);
+
+  TransformationReplaceBranchFromDeadBlockWithExit(uint32_t block_id,
+                                                   SpvOp opcode,
+                                                   uint32_t return_value_id);
+
+  // - |message_.block_id| must be the id of a dead block that is not part of
+  //   a continue construct
+  // - |message_.block_id| must end with OpBranch
+  // - The successor of |message_.block_id| must have at least one other
+  //   predecessor
+  // - |message_.opcode()| must be one of OpKill, OpReturn, OpReturnValue and
+  //   OpUnreachable
+  // - |message_.opcode()| can only be OpKill the module's entry points all
+  //   have Fragment execution mode
+  // - |message_.opcode()| can only be OpReturn if the return type of the
+  //   function containing the block is void
+  // - If |message_.opcode()| is OpReturnValue then |message_.return_value_id|
+  //   must be an id that is available at the block terminator and that matches
+  //   the return type of the enclosing function
+  bool IsApplicable(
+      opt::IRContext* ir_context,
+      const TransformationContext& transformation_context) const override;
+
+  // Changes the terminator of |message_.block_id| to have opcode
+  // |message_.opcode|, additionally with input operand
+  // |message_.return_value_id| in the case that |message_.opcode| is
+  // OpReturnValue.
+  //
+  // If |message_.block_id|'s successor starts with OpPhi instructions these are
+  // updated so that they no longer refer to |message_.block_id|.
+  void Apply(opt::IRContext* ir_context,
+             TransformationContext* transformation_context) const override;
+
+  std::unordered_set<uint32_t> GetFreshIds() const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+  // Returns true if and only if |block| meets the criteria for having its
+  // terminator replaced with an early exit (see IsApplicable for details of the
+  // criteria.)
+  static bool BlockIsSuitable(
+      opt::IRContext* ir_context,
+      const TransformationContext& transformation_context,
+      const opt::BasicBlock& block);
+
+ private:
+  protobufs::TransformationReplaceBranchFromDeadBlockWithExit message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_REPLACE_BRANCH_FROM_DEAD_BLOCK_WITH_EXIT_H_
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index c42bd2d..d586425 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -92,6 +92,7 @@
           transformation_push_id_through_variable_test.cpp
           transformation_replace_add_sub_mul_with_carrying_extended_test.cpp
           transformation_replace_boolean_constant_with_constant_binary_test.cpp
+          transformation_replace_branch_from_dead_block_with_exit_test.cpp
           transformation_replace_copy_object_with_store_load_test.cpp
           transformation_replace_constant_with_uniform_test.cpp
           transformation_replace_copy_memory_with_load_store_test.cpp
diff --git a/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp b/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp
new file mode 100644
index 0000000..ee1252d
--- /dev/null
+++ b/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp
@@ -0,0 +1,560 @@
+// Copyright (c) 2020 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_replace_branch_from_dead_block_with_exit.h"
+
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, BasicTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeBool
+          %7 = OpConstantFalse %6
+         %12 = OpTypeInt 32 1
+         %13 = OpTypePointer Function %12
+         %15 = OpConstant %12 1
+         %17 = OpConstant %12 2
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %14 = OpVariable %13 Function
+               OpSelectionMerge %9 None
+               OpBranchConditional %7 %8 %21
+          %8 = OpLabel
+               OpSelectionMerge %11 None
+               OpBranchConditional %7 %10 %16
+         %10 = OpLabel
+               OpStore %14 %15
+               OpBranch %20
+         %20 = OpLabel
+               OpBranch %11
+         %16 = OpLabel
+               OpStore %14 %17
+               OpBranch %11
+         %11 = OpLabel
+               OpBranch %9
+         %21 = OpLabel
+               OpBranch %9
+          %9 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  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(8);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(10);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(11);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(16);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(20);
+
+  // Bad: 4 is not a block
+  ASSERT_FALSE(TransformationReplaceBranchFromDeadBlockWithExit(4, SpvOpKill, 0)
+                   .IsApplicable(context.get(), transformation_context));
+  // Bad: 200 does not exist
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(200, SpvOpKill, 0)
+          .IsApplicable(context.get(), transformation_context));
+  // Bad: 21 is not a dead block
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(21, SpvOpKill, 0)
+          .IsApplicable(context.get(), transformation_context));
+  // Bad: terminator of 8 is not OpBranch
+  ASSERT_FALSE(TransformationReplaceBranchFromDeadBlockWithExit(8, SpvOpKill, 0)
+                   .IsApplicable(context.get(), transformation_context));
+  // Bad: 10's successor only has 10 as a predecessor
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(10, SpvOpKill, 0)
+          .IsApplicable(context.get(), transformation_context));
+
+#ifndef NDEBUG
+  ASSERT_DEATH(
+      TransformationReplaceBranchFromDeadBlockWithExit(20, SpvOpSwitch, 0)
+          .IsApplicable(context.get(), transformation_context),
+      "Invalid early exit opcode.");
+#endif
+
+  auto transformation1 =
+      TransformationReplaceBranchFromDeadBlockWithExit(20, SpvOpKill, 0);
+  auto transformation2 =
+      TransformationReplaceBranchFromDeadBlockWithExit(16, SpvOpKill, 0);
+  ASSERT_TRUE(
+      transformation1.IsApplicable(context.get(), transformation_context));
+  ASSERT_TRUE(
+      transformation2.IsApplicable(context.get(), transformation_context));
+
+  ApplyAndCheckFreshIds(transformation1, context.get(),
+                        &transformation_context);
+  // Applying transformation 1 should disable transformation 2
+  ASSERT_FALSE(
+      transformation2.IsApplicable(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 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeBool
+          %7 = OpConstantFalse %6
+         %12 = OpTypeInt 32 1
+         %13 = OpTypePointer Function %12
+         %15 = OpConstant %12 1
+         %17 = OpConstant %12 2
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %14 = OpVariable %13 Function
+               OpSelectionMerge %9 None
+               OpBranchConditional %7 %8 %21
+          %8 = OpLabel
+               OpSelectionMerge %11 None
+               OpBranchConditional %7 %10 %16
+         %10 = OpLabel
+               OpStore %14 %15
+               OpBranch %20
+         %20 = OpLabel
+               OpKill
+         %16 = OpLabel
+               OpStore %14 %17
+               OpBranch %11
+         %11 = OpLabel
+               OpBranch %9
+         %21 = OpLabel
+               OpBranch %9
+          %9 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationReplaceBranchFromDeadBlockWithExitTest,
+     VertexShaderWithLoopInContinueConstruct) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Vertex %4 "main"
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeFunction %6
+         %12 = OpTypePointer Function %6
+         %14 = OpConstant %6 0
+         %22 = OpConstant %6 10
+         %23 = OpTypeBool
+         %26 = OpConstant %6 1
+         %40 = OpConstant %6 100
+         %48 = OpConstantFalse %23
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpSelectionMerge %50 None
+               OpBranchConditional %48 %49 %50
+         %49 = OpLabel
+         %51 = OpFunctionCall %6 %10
+               OpBranch %50
+         %50 = OpLabel
+               OpReturn
+               OpFunctionEnd
+         %10 = OpFunction %6 None %7
+         %11 = OpLabel
+         %13 = OpVariable %12 Function
+         %15 = OpVariable %12 Function
+         %33 = OpVariable %12 Function
+               OpStore %33 %14
+               OpBranch %34
+         %34 = OpLabel
+               OpLoopMerge %36 %37 None
+               OpBranch %38
+         %38 = OpLabel
+         %39 = OpLoad %6 %33
+         %41 = OpSLessThan %23 %39 %40
+               OpBranchConditional %41 %35 %36
+         %35 = OpLabel
+               OpSelectionMerge %202 None
+               OpBranchConditional %48 %200 %201
+        %200 = OpLabel
+               OpBranch %202
+        %201 = OpLabel
+        %400 = OpCopyObject %6 %14
+               OpBranch %202
+        %202 = OpLabel
+               OpBranch %37
+         %37 = OpLabel
+               OpStore %13 %14
+               OpStore %15 %14
+               OpBranch %16
+         %16 = OpLabel
+               OpLoopMerge %18 %19 None
+               OpBranch %20
+         %20 = OpLabel
+               OpSelectionMerge %102 None
+               OpBranchConditional %48 %100 %101
+        %100 = OpLabel
+               OpBranch %102
+        %101 = OpLabel
+               OpBranch %102
+        %102 = OpLabel
+         %21 = OpLoad %6 %15
+         %24 = OpSLessThan %23 %21 %22
+               OpBranchConditional %24 %17 %18
+         %17 = OpLabel
+               OpSelectionMerge %302 None
+               OpBranchConditional %48 %300 %301
+        %300 = OpLabel
+               OpBranch %302
+        %301 = OpLabel
+               OpBranch %302
+        %302 = OpLabel
+         %25 = OpLoad %6 %13
+         %27 = OpIAdd %6 %25 %26
+               OpStore %13 %27
+               OpBranch %19
+         %19 = OpLabel
+         %28 = OpLoad %6 %15
+         %29 = OpIAdd %6 %28 %26
+               OpStore %15 %29
+               OpBranch %16
+         %18 = OpLabel
+         %30 = OpLoad %6 %13
+         %42 = OpCopyObject %6 %30
+         %43 = OpLoad %6 %33
+         %44 = OpIAdd %6 %43 %42
+               OpStore %33 %44
+               OpBranch %34
+         %36 = OpLabel
+         %45 = OpLoad %6 %33
+               OpReturnValue %45
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  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);
+
+  for (auto block : {16, 17,  18,  19,  20,  34,  35,  36,  37,  38,
+                     49, 100, 101, 102, 200, 201, 202, 300, 301, 302}) {
+    transformation_context.GetFactManager()->AddFactBlockIsDead(block);
+  }
+
+  // Bad: OpKill not allowed in vertex shader
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(201, SpvOpKill, 0)
+          .IsApplicable(context.get(), transformation_context));
+
+  // Bad: OpReturn is not allowed in function that expects a returned value.
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(200, SpvOpReturn, 0)
+          .IsApplicable(context.get(), transformation_context));
+
+  // Bad: Return value id does not exist
+  ASSERT_FALSE(TransformationReplaceBranchFromDeadBlockWithExit(
+                   201, SpvOpReturnValue, 1000)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Bad: Return value id does not have a type
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(200, SpvOpReturnValue, 6)
+          .IsApplicable(context.get(), transformation_context));
+
+  // Bad: Return value id does not have the right type
+  ASSERT_FALSE(TransformationReplaceBranchFromDeadBlockWithExit(
+                   201, SpvOpReturnValue, 48)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Bad: Return value id is not available
+  ASSERT_FALSE(TransformationReplaceBranchFromDeadBlockWithExit(
+                   200, SpvOpReturnValue, 400)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Bad: Early exit now allowed in continue construct
+  ASSERT_FALSE(
+      TransformationReplaceBranchFromDeadBlockWithExit(101, SpvOpUnreachable, 0)
+          .IsApplicable(context.get(), transformation_context));
+
+  // Bad: Early exit now allowed in continue construct (again)
+  ASSERT_FALSE(TransformationReplaceBranchFromDeadBlockWithExit(
+                   300, SpvOpReturnValue, 14)
+                   .IsApplicable(context.get(), transformation_context));
+
+  auto transformation1 = TransformationReplaceBranchFromDeadBlockWithExit(
+      200, SpvOpUnreachable, 0);
+  auto transformation2 = TransformationReplaceBranchFromDeadBlockWithExit(
+      201, SpvOpReturnValue, 400);
+
+  ASSERT_TRUE(
+      transformation1.IsApplicable(context.get(), transformation_context));
+  ASSERT_TRUE(
+      transformation2.IsApplicable(context.get(), transformation_context));
+
+  ApplyAndCheckFreshIds(transformation2, context.get(),
+                        &transformation_context);
+  ASSERT_FALSE(
+      transformation1.IsApplicable(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 Vertex %4 "main"
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeFunction %6
+         %12 = OpTypePointer Function %6
+         %14 = OpConstant %6 0
+         %22 = OpConstant %6 10
+         %23 = OpTypeBool
+         %26 = OpConstant %6 1
+         %40 = OpConstant %6 100
+         %48 = OpConstantFalse %23
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpSelectionMerge %50 None
+               OpBranchConditional %48 %49 %50
+         %49 = OpLabel
+         %51 = OpFunctionCall %6 %10
+               OpBranch %50
+         %50 = OpLabel
+               OpReturn
+               OpFunctionEnd
+         %10 = OpFunction %6 None %7
+         %11 = OpLabel
+         %13 = OpVariable %12 Function
+         %15 = OpVariable %12 Function
+         %33 = OpVariable %12 Function
+               OpStore %33 %14
+               OpBranch %34
+         %34 = OpLabel
+               OpLoopMerge %36 %37 None
+               OpBranch %38
+         %38 = OpLabel
+         %39 = OpLoad %6 %33
+         %41 = OpSLessThan %23 %39 %40
+               OpBranchConditional %41 %35 %36
+         %35 = OpLabel
+               OpSelectionMerge %202 None
+               OpBranchConditional %48 %200 %201
+        %200 = OpLabel
+               OpBranch %202
+        %201 = OpLabel
+        %400 = OpCopyObject %6 %14
+               OpReturnValue %400
+        %202 = OpLabel
+               OpBranch %37
+         %37 = OpLabel
+               OpStore %13 %14
+               OpStore %15 %14
+               OpBranch %16
+         %16 = OpLabel
+               OpLoopMerge %18 %19 None
+               OpBranch %20
+         %20 = OpLabel
+               OpSelectionMerge %102 None
+               OpBranchConditional %48 %100 %101
+        %100 = OpLabel
+               OpBranch %102
+        %101 = OpLabel
+               OpBranch %102
+        %102 = OpLabel
+         %21 = OpLoad %6 %15
+         %24 = OpSLessThan %23 %21 %22
+               OpBranchConditional %24 %17 %18
+         %17 = OpLabel
+               OpSelectionMerge %302 None
+               OpBranchConditional %48 %300 %301
+        %300 = OpLabel
+               OpBranch %302
+        %301 = OpLabel
+               OpBranch %302
+        %302 = OpLabel
+         %25 = OpLoad %6 %13
+         %27 = OpIAdd %6 %25 %26
+               OpStore %13 %27
+               OpBranch %19
+         %19 = OpLabel
+         %28 = OpLoad %6 %15
+         %29 = OpIAdd %6 %28 %26
+               OpStore %15 %29
+               OpBranch %16
+         %18 = OpLabel
+         %30 = OpLoad %6 %13
+         %42 = OpCopyObject %6 %30
+         %43 = OpLoad %6 %33
+         %44 = OpIAdd %6 %43 %42
+               OpStore %33 %44
+               OpBranch %34
+         %36 = OpLabel
+         %45 = OpLoad %6 %33
+               OpReturnValue %45
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, OpPhi) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeBool
+          %7 = OpConstantFalse %6
+         %12 = OpTypeInt 32 1
+         %13 = OpTypePointer Function %12
+         %15 = OpConstant %12 1
+         %17 = OpConstant %12 2
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %14 = OpVariable %13 Function
+               OpSelectionMerge %9 None
+               OpBranchConditional %7 %8 %21
+          %8 = OpLabel
+               OpSelectionMerge %11 None
+               OpBranchConditional %7 %10 %16
+         %10 = OpLabel
+               OpStore %14 %15
+               OpBranch %20
+         %20 = OpLabel
+         %48 = OpCopyObject %12 %15
+               OpBranch %11
+         %16 = OpLabel
+               OpStore %14 %17
+         %49 = OpCopyObject %12 %17
+               OpBranch %11
+         %11 = OpLabel
+         %50 = OpPhi %12 %48 %20 %49 %16
+               OpBranch %9
+         %21 = OpLabel
+               OpBranch %9
+          %9 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_4;
+  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(8);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(10);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(11);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(16);
+  transformation_context.GetFactManager()->AddFactBlockIsDead(20);
+
+  auto transformation1 =
+      TransformationReplaceBranchFromDeadBlockWithExit(20, SpvOpKill, 0);
+  auto transformation2 =
+      TransformationReplaceBranchFromDeadBlockWithExit(16, SpvOpKill, 0);
+  ASSERT_TRUE(
+      transformation1.IsApplicable(context.get(), transformation_context));
+  ASSERT_TRUE(
+      transformation2.IsApplicable(context.get(), transformation_context));
+
+  ApplyAndCheckFreshIds(transformation1, context.get(),
+                        &transformation_context);
+  // Applying transformation 1 should disable transformation 2
+  ASSERT_FALSE(
+      transformation2.IsApplicable(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 320
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeBool
+          %7 = OpConstantFalse %6
+         %12 = OpTypeInt 32 1
+         %13 = OpTypePointer Function %12
+         %15 = OpConstant %12 1
+         %17 = OpConstant %12 2
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %14 = OpVariable %13 Function
+               OpSelectionMerge %9 None
+               OpBranchConditional %7 %8 %21
+          %8 = OpLabel
+               OpSelectionMerge %11 None
+               OpBranchConditional %7 %10 %16
+         %10 = OpLabel
+               OpStore %14 %15
+               OpBranch %20
+         %20 = OpLabel
+         %48 = OpCopyObject %12 %15
+               OpKill
+         %16 = OpLabel
+               OpStore %14 %17
+         %49 = OpCopyObject %12 %17
+               OpBranch %11
+         %11 = OpLabel
+         %50 = OpPhi %12 %49 %16
+               OpBranch %9
+         %21 = OpLabel
+               OpBranch %9
+          %9 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools