Transformation and fuzzer pass to add dead continues (#2758)

Similar to the existing 'add dead breaks' pass, this adds a pass to
add dead continues to blocks in loops where such a transformation is
viable.  Various functionality common to this new pass and 'add dead
breaks' has been factored into 'fuzzer_util', and some small
improvements to 'add dead breaks' that were identified while reviewing
that code again have been applied.

Fixes #2719.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 75e2125..fbabba1 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -31,6 +31,7 @@
         fuzzer_context.h
         fuzzer_pass.h
         fuzzer_pass_add_dead_breaks.h
+        fuzzer_pass_add_dead_continues.h
         fuzzer_pass_add_useful_constructs.h
         fuzzer_pass_obfuscate_constants.h
         fuzzer_pass_permute_blocks.h
@@ -46,6 +47,7 @@
         transformation_add_constant_boolean.h
         transformation_add_constant_scalar.h
         transformation_add_dead_break.h
+        transformation_add_dead_continue.h
         transformation_add_type_boolean.h
         transformation_add_type_float.h
         transformation_add_type_int.h
@@ -62,6 +64,7 @@
         fuzzer_context.cpp
         fuzzer_pass.cpp
         fuzzer_pass_add_dead_breaks.cpp
+        fuzzer_pass_add_dead_continues.cpp
         fuzzer_pass_add_useful_constructs.cpp
         fuzzer_pass_obfuscate_constants.cpp
         fuzzer_pass_permute_blocks.cpp
@@ -76,6 +79,7 @@
         transformation_add_constant_boolean.cpp
         transformation_add_constant_scalar.cpp
         transformation_add_dead_break.cpp
+        transformation_add_dead_continue.cpp
         transformation_add_type_boolean.cpp
         transformation_add_type_float.cpp
         transformation_add_type_int.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index 0fa645a..89250a0 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -20,6 +20,7 @@
 #include "source/fuzz/fact_manager.h"
 #include "source/fuzz/fuzzer_context.h"
 #include "source/fuzz/fuzzer_pass_add_dead_breaks.h"
+#include "source/fuzz/fuzzer_pass_add_dead_continues.h"
 #include "source/fuzz/fuzzer_pass_add_useful_constructs.h"
 #include "source/fuzz/fuzzer_pass_obfuscate_constants.h"
 #include "source/fuzz/fuzzer_pass_permute_blocks.h"
@@ -112,6 +113,9 @@
   FuzzerPassAddDeadBreaks(ir_context.get(), &fact_manager, &fuzzer_context,
                           transformation_sequence_out)
       .Apply();
+  FuzzerPassAddDeadContinues(ir_context.get(), &fact_manager, &fuzzer_context,
+                             transformation_sequence_out)
+      .Apply();
   FuzzerPassObfuscateConstants(ir_context.get(), &fact_manager, &fuzzer_context,
                                transformation_sequence_out)
       .Apply();
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 9252341..c8e7719 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -25,6 +25,7 @@
 // Keep them in alphabetical order.
 
 const uint32_t kDefaultChanceOfAddingDeadBreak = 20;
+const uint32_t kDefaultChanceOfAddingDeadContinue = 20;
 const uint32_t kDefaultChanceOfMovingBlockDown = 25;
 const uint32_t kDefaultChanceOfObfuscatingConstant = 20;
 const uint32_t kDefaultChanceOfSplittingBlock = 20;
@@ -46,6 +47,7 @@
     : random_generator_(random_generator),
       next_fresh_id_(min_fresh_id),
       chance_of_adding_dead_break_(kDefaultChanceOfAddingDeadBreak),
+      chance_of_adding_dead_continue_(kDefaultChanceOfAddingDeadContinue),
       chance_of_moving_block_down_(kDefaultChanceOfMovingBlockDown),
       chance_of_obfuscating_constant_(kDefaultChanceOfObfuscatingConstant),
       chance_of_splitting_block_(kDefaultChanceOfSplittingBlock),
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index 2815bf7..3eaefc7 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -44,6 +44,9 @@
   // Probabilities associated with applying various transformations.
   // Keep them in alphabetical order.
   uint32_t GetChanceOfAddingDeadBreak() { return chance_of_adding_dead_break_; }
+  uint32_t GetChanceOfAddingDeadContinue() {
+    return chance_of_adding_dead_continue_;
+  }
   uint32_t GetChanceOfMovingBlockDown() { return chance_of_moving_block_down_; }
   uint32_t GetChanceOfObfuscatingConstant() {
     return chance_of_obfuscating_constant_;
@@ -66,6 +69,7 @@
   // Probabilities associated with applying various transformations.
   // Keep them in alphabetical order.
   uint32_t chance_of_adding_dead_break_;
+  uint32_t chance_of_adding_dead_continue_;
   uint32_t chance_of_moving_block_down_;
   uint32_t chance_of_obfuscating_constant_;
   uint32_t chance_of_splitting_block_;
diff --git a/source/fuzz/fuzzer_pass_add_dead_breaks.cpp b/source/fuzz/fuzzer_pass_add_dead_breaks.cpp
index 62e2a8b..72cd17b 100644
--- a/source/fuzz/fuzzer_pass_add_dead_breaks.cpp
+++ b/source/fuzz/fuzzer_pass_add_dead_breaks.cpp
@@ -50,11 +50,9 @@
         // TODO(afd): right now we completely ignore OpPhi instructions at
         //  merge blocks.  This will lead to interesting opportunities being
         //  missed.
-        std::vector<uint32_t> phi_ids;
         auto candidate_transformation = TransformationAddDeadBreak(
             block.id(), merge_block_id,
-            GetFuzzerContext()->GetRandomGenerator()->RandomBool(),
-            std::move(phi_ids));
+            GetFuzzerContext()->GetRandomGenerator()->RandomBool(), {});
         if (candidate_transformation.IsApplicable(GetIRContext(),
                                                   *GetFactManager())) {
           // Only consider a transformation as a candidate if it is applicable.
@@ -84,14 +82,11 @@
     // Remove the transformation at the chosen index from the sequence.
     auto transformation = std::move(candidate_transformations[index]);
     candidate_transformations.erase(candidate_transformations.begin() + index);
-    // Probabilistically decide whether to try to apply it vs. ignore it.
-    if (GetFuzzerContext()->GetRandomGenerator()->RandomPercentage() >
-        GetFuzzerContext()->GetChanceOfAddingDeadBreak()) {
-      continue;
-    }
-    // If the transformation can be applied, apply it and add it to the
-    // sequence of transformations that have been applied.
-    if (transformation.IsApplicable(GetIRContext(), *GetFactManager())) {
+    // Probabilistically decide whether to try to apply it vs. ignore it, in the
+    // case that it is applicable.
+    if (transformation.IsApplicable(GetIRContext(), *GetFactManager()) &&
+        GetFuzzerContext()->GetRandomGenerator()->RandomPercentage() >
+            GetFuzzerContext()->GetChanceOfAddingDeadBreak()) {
       transformation.Apply(GetIRContext(), GetFactManager());
       *GetTransformations()->add_transformation() = transformation.ToMessage();
     }
diff --git a/source/fuzz/fuzzer_pass_add_dead_continues.cpp b/source/fuzz/fuzzer_pass_add_dead_continues.cpp
new file mode 100644
index 0000000..2156d36
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_add_dead_continues.cpp
@@ -0,0 +1,60 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/fuzzer_pass_add_dead_continues.h"
+
+#include "source/fuzz/transformation_add_dead_continue.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassAddDeadContinues::FuzzerPassAddDeadContinues(
+    opt::IRContext* ir_context, FactManager* fact_manager,
+    FuzzerContext* fuzzer_context,
+    protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, fact_manager, fuzzer_context, transformations) {}
+
+FuzzerPassAddDeadContinues::~FuzzerPassAddDeadContinues() = default;
+
+void FuzzerPassAddDeadContinues::Apply() {
+  // Consider every block in every function.
+  for (auto& function : *GetIRContext()->module()) {
+    for (auto& block : function) {
+      // Make a transformation to add a dead continue from this node; if the
+      // node turns out to be inappropriate (e.g. by not being in a loop) the
+      // precondition for the transformation will fail and it will be ignored.
+      //
+      // TODO(afd): right now we completely ignore OpPhi instructions at
+      //  merge blocks.  This will lead to interesting opportunities being
+      //  missed.
+      auto candidate_transformation = TransformationAddDeadContinue(
+          block.id(), GetFuzzerContext()->GetRandomGenerator()->RandomBool(),
+          {});
+      // Probabilistically decide whether to apply the transformation in the
+      // case that it is applicable.
+      if (candidate_transformation.IsApplicable(GetIRContext(),
+                                                *GetFactManager()) &&
+          GetFuzzerContext()->GetRandomGenerator()->RandomPercentage() >
+              GetFuzzerContext()->GetChanceOfAddingDeadContinue()) {
+        candidate_transformation.Apply(GetIRContext(), GetFactManager());
+        *GetTransformations()->add_transformation() =
+            candidate_transformation.ToMessage();
+      }
+    }
+  }
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_add_dead_continues.h b/source/fuzz/fuzzer_pass_add_dead_continues.h
new file mode 100644
index 0000000..6cadc97
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_add_dead_continues.h
@@ -0,0 +1,39 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_FUZZER_PASS_ADD_DEAD_CONTINUES_H_
+#define SOURCE_FUZZ_FUZZER_PASS_ADD_DEAD_CONTINUES_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// A fuzzer pass for adding dead continue edges to the module.
+class FuzzerPassAddDeadContinues : public FuzzerPass {
+ public:
+  FuzzerPassAddDeadContinues(
+      opt::IRContext* ir_context, FactManager* fact_manager,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassAddDeadContinues();
+
+  void Apply() override;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // #define SOURCE_FUZZ_FUZZER_PASS_ADD_DEAD_CONTINUES_H_
diff --git a/source/fuzz/fuzzer_util.cpp b/source/fuzz/fuzzer_util.cpp
index 645c121..9a05c74 100644
--- a/source/fuzz/fuzzer_util.cpp
+++ b/source/fuzz/fuzzer_util.cpp
@@ -30,6 +30,146 @@
       std::max(context->module()->id_bound(), id + 1));
 }
 
+opt::BasicBlock* MaybeFindBlock(opt::IRContext* context,
+                                uint32_t maybe_block_id) {
+  auto inst = context->get_def_use_mgr()->GetDef(maybe_block_id);
+  if (inst == nullptr) {
+    // No instruction defining this id was found.
+    return nullptr;
+  }
+  if (inst->opcode() != SpvOpLabel) {
+    // The instruction defining the id is not a label, so it cannot be a block
+    // id.
+    return nullptr;
+  }
+  return context->cfg()->block(maybe_block_id);
+}
+
+bool PhiIdsOkForNewEdge(
+    opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to,
+    const google::protobuf::RepeatedField<google::protobuf::uint32>& phi_ids) {
+  if (bb_from->IsSuccessor(bb_to)) {
+    // There is already an edge from |from_block| to |to_block|, so there is
+    // no need to extend OpPhi instructions.  Do not allow phi ids to be
+    // present. This might turn out to be too strict; perhaps it would be OK
+    // just to ignore the ids in this case.
+    return phi_ids.empty();
+  }
+  // The edge would add a previously non-existent edge from |from_block| to
+  // |to_block|, so we go through the given phi ids and check that they exactly
+  // match the OpPhi instructions in |to_block|.
+  uint32_t phi_index = 0;
+  // An explicit loop, rather than applying a lambda to each OpPhi in |bb_to|,
+  // makes sense here because we need to increment |phi_index| for each OpPhi
+  // instruction.
+  for (auto& inst : *bb_to) {
+    if (inst.opcode() != SpvOpPhi) {
+      // The OpPhi instructions all occur at the start of the block; if we find
+      // a non-OpPhi then we have seen them all.
+      break;
+    }
+    if (phi_index == static_cast<uint32_t>(phi_ids.size())) {
+      // Not enough phi ids have been provided to account for the OpPhi
+      // instructions.
+      return false;
+    }
+    // Look for an instruction defining the next phi id.
+    opt::Instruction* phi_extension =
+        context->get_def_use_mgr()->GetDef(phi_ids[phi_index]);
+    if (!phi_extension) {
+      // The id given to extend this OpPhi does not exist.
+      return false;
+    }
+    if (phi_extension->type_id() != inst.type_id()) {
+      // The instruction given to extend this OpPhi either does not have a type
+      // or its type does not match that of the OpPhi.
+      return false;
+    }
+
+    if (context->get_instr_block(phi_extension)) {
+      // The instruction defining the phi id has an associated block (i.e., it
+      // is not a global value).  Check whether its definition dominates the
+      // exit of |from_block|.
+      auto dominator_analysis =
+          context->GetDominatorAnalysis(bb_from->GetParent());
+      if (!dominator_analysis->Dominates(phi_extension,
+                                         bb_from->terminator())) {
+        // The given id is no good as its definition does not dominate the exit
+        // of |from_block|
+        return false;
+      }
+    }
+    phi_index++;
+  }
+  // Return false if not all of the ids for extending OpPhi instructions are
+  // needed. This might turn out to be stricter than necessary; perhaps it would
+  // be OK just to not use the ids in this case.
+  return phi_index == static_cast<uint32_t>(phi_ids.size());
+}
+
+void AddUnreachableEdgeAndUpdateOpPhis(
+    opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to,
+    bool condition_value,
+    const google::protobuf::RepeatedField<google::protobuf::uint32>& phi_ids) {
+  assert(PhiIdsOkForNewEdge(context, bb_from, bb_to, phi_ids) &&
+         "Precondition on phi_ids is not satisfied");
+  assert(bb_from->terminator()->opcode() == SpvOpBranch &&
+         "Precondition on terminator of bb_from is not satisfied");
+
+  // Get the id of the boolean constant to be used as the condition.
+  opt::analysis::Bool bool_type;
+  opt::analysis::BoolConstant bool_constant(
+      context->get_type_mgr()->GetRegisteredType(&bool_type)->AsBool(),
+      condition_value);
+  uint32_t bool_id = context->get_constant_mgr()->FindDeclaredConstant(
+      &bool_constant, context->get_type_mgr()->GetId(&bool_type));
+
+  const bool from_to_edge_already_exists = bb_from->IsSuccessor(bb_to);
+  auto successor = bb_from->terminator()->GetSingleWordInOperand(0);
+
+  // Add the dead branch, by turning OpBranch into OpBranchConditional, and
+  // ordering the targets depending on whether the given boolean corresponds to
+  // true or false.
+  bb_from->terminator()->SetOpcode(SpvOpBranchConditional);
+  bb_from->terminator()->SetInOperands(
+      {{SPV_OPERAND_TYPE_ID, {bool_id}},
+       {SPV_OPERAND_TYPE_ID, {condition_value ? successor : bb_to->id()}},
+       {SPV_OPERAND_TYPE_ID, {condition_value ? bb_to->id() : successor}}});
+
+  // Update OpPhi instructions in the target block if this branch adds a
+  // previously non-existent edge from source to target.
+  if (!from_to_edge_already_exists) {
+    uint32_t phi_index = 0;
+    for (auto& inst : *bb_to) {
+      if (inst.opcode() != SpvOpPhi) {
+        break;
+      }
+      assert(phi_index < static_cast<uint32_t>(phi_ids.size()) &&
+             "There should be exactly one phi id per OpPhi instruction.");
+      inst.AddOperand({SPV_OPERAND_TYPE_ID, {phi_ids[phi_index]}});
+      inst.AddOperand({SPV_OPERAND_TYPE_ID, {bb_from->id()}});
+      phi_index++;
+    }
+    assert(phi_index == static_cast<uint32_t>(phi_ids.size()) &&
+           "There should be exactly one phi id per OpPhi instruction.");
+  }
+}
+
+bool BlockIsInLoopContinueConstruct(opt::IRContext* context, uint32_t block_id,
+                                    uint32_t maybe_loop_header_id) {
+  // We deem a block to be part of a loop's continue construct if the loop's
+  // continue target dominates the block.
+  auto containing_construct_block = context->cfg()->block(maybe_loop_header_id);
+  if (containing_construct_block->IsLoopHeader()) {
+    auto continue_target = containing_construct_block->ContinueBlockId();
+    if (context->GetDominatorAnalysis(containing_construct_block->GetParent())
+            ->Dominates(continue_target, block_id)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 }  // namespace fuzzerutil
 
 }  // namespace fuzz
diff --git a/source/fuzz/fuzzer_util.h b/source/fuzz/fuzzer_util.h
index 30d870b..15228de 100644
--- a/source/fuzz/fuzzer_util.h
+++ b/source/fuzz/fuzzer_util.h
@@ -15,6 +15,9 @@
 #ifndef SOURCE_FUZZ_FUZZER_UTIL_H_
 #define SOURCE_FUZZ_FUZZER_UTIL_H_
 
+#include <vector>
+
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
 #include "source/opt/ir_context.h"
 
 namespace spvtools {
@@ -30,6 +33,35 @@
 // account for the given id.
 void UpdateModuleIdBound(opt::IRContext* context, uint32_t id);
 
+// Return the block with id |maybe_block_id| if it exists, and nullptr
+// otherwise.
+opt::BasicBlock* MaybeFindBlock(opt::IRContext* context,
+                                uint32_t maybe_block_id);
+
+// When adding an edge from |bb_from| to |bb_to| (which are assumed to be blocks
+// in the same function), it is important to supply |bb_to| with ids that can be
+// used to augment OpPhi instructions in the case that there is not already such
+// an edge.  This function returns true if and only if the ids provided in
+// |phi_ids| suffice for this purpose,
+bool PhiIdsOkForNewEdge(
+    opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to,
+    const google::protobuf::RepeatedField<google::protobuf::uint32>& phi_ids);
+
+// Requires that PhiIdsOkForNewEdge(context, bb_from, bb_to, phi_ids) holds,
+// and that bb_from ends with "OpBranch %some_block".  Turns OpBranch into
+// "OpBranchConditional |condition_value| ...", such that control will branch
+// to %some_block, with |bb_to| being the unreachable alternative.  Updates
+// OpPhi instructions in |bb_to| using |phi_ids| so that the new edge is valid.
+void AddUnreachableEdgeAndUpdateOpPhis(
+    opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to,
+    bool condition_value,
+    const google::protobuf::RepeatedField<google::protobuf::uint32>& phi_ids);
+
+// Returns true if and only if |maybe_loop_header_id| is a loop header and
+// |block_id| is in the continue construct of the associated loop.
+bool BlockIsInLoopContinueConstruct(opt::IRContext* context, uint32_t block_id,
+                                    uint32_t maybe_loop_header_id);
+
 }  // namespace fuzzerutil
 
 }  // namespace fuzz
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index aecd4c7..13d8a05 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -137,6 +137,7 @@
     TransformationReplaceBooleanConstantWithConstantBinary replace_boolean_constant_with_constant_binary = 9;
     TransformationAddTypePointer add_type_pointer = 10;
     TransformationReplaceConstantWithUniform replace_constant_with_uniform = 11;
+    TransformationAddDeadContinue add_dead_continue = 12;
     // Add additional option using the next available number.
   }
 }
@@ -190,6 +191,25 @@
 
 }
 
+message TransformationAddDeadContinue {
+
+  // A transformation that turns a basic block appearing in a loop and that
+  // unconditionally branches to its successor into a block that potentially
+  // branches to the continue target of the loop, but in such a manner that the
+  // continue branch cannot actually be taken.
+
+  // The block to continue from
+  uint32 from_block = 1;
+
+  // Determines whether the continue condition is true or false
+  bool continue_condition_value = 2;
+
+  // A sequence of ids suitable for extending OpPhi instructions as a result of
+  // the new break edge
+  repeated uint32 phi_id = 3;
+
+}
+
 message TransformationAddTypeBoolean {
 
   // Adds OpTypeBool to the module
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index 18a66d4..a252734 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -20,6 +20,7 @@
 #include "transformation_add_constant_boolean.h"
 #include "transformation_add_constant_scalar.h"
 #include "transformation_add_dead_break.h"
+#include "transformation_add_dead_continue.h"
 #include "transformation_add_type_boolean.h"
 #include "transformation_add_type_float.h"
 #include "transformation_add_type_int.h"
@@ -45,6 +46,9 @@
           message.add_constant_scalar());
     case protobufs::Transformation::TransformationCase::kAddDeadBreak:
       return MakeUnique<TransformationAddDeadBreak>(message.add_dead_break());
+    case protobufs::Transformation::TransformationCase::kAddDeadContinue:
+      return MakeUnique<TransformationAddDeadContinue>(
+          message.add_dead_continue());
     case protobufs::Transformation::TransformationCase::kAddTypeBoolean:
       return MakeUnique<TransformationAddTypeBoolean>(
           message.add_type_boolean());
diff --git a/source/fuzz/transformation_add_dead_break.cpp b/source/fuzz/transformation_add_dead_break.cpp
index 7b55f47..229dc90 100644
--- a/source/fuzz/transformation_add_dead_break.cpp
+++ b/source/fuzz/transformation_add_dead_break.cpp
@@ -15,6 +15,7 @@
 #include "source/fuzz/transformation_add_dead_break.h"
 
 #include "source/fuzz/fact_manager.h"
+#include "source/fuzz/fuzzer_util.h"
 #include "source/opt/basic_block.h"
 #include "source/opt/ir_context.h"
 #include "source/opt/struct_cfg_analysis.h"
@@ -37,98 +38,6 @@
   }
 }
 
-opt::BasicBlock* TransformationAddDeadBreak::MaybeFindBlock(
-    opt::IRContext* context, uint32_t maybe_block_id) const {
-  auto inst = context->get_def_use_mgr()->GetDef(maybe_block_id);
-  if (inst == nullptr) {
-    // No instruction defining this id was found.
-    return nullptr;
-  }
-  if (inst->opcode() != SpvOpLabel) {
-    // The instruction defining the id is not a label, so it cannot be a block
-    // id.
-    return nullptr;
-  }
-  return context->cfg()->block(maybe_block_id);
-}
-
-bool TransformationAddDeadBreak::PhiIdsOk(opt::IRContext* context,
-                                          opt::BasicBlock* bb_from,
-                                          opt::BasicBlock* bb_to) const {
-  if (bb_from->IsSuccessor(bb_to)) {
-    // There is already an edge from |from_block| to |to_block|, so there is
-    // no need to extend OpPhi instructions.  Do not allow phi ids to be
-    // present. This might turn out to be too strict; perhaps it would be OK
-    // just to ignore the ids in this case.
-    return message_.phi_id().empty();
-  }
-  // The break would add a previously non-existent edge from |from_block| to
-  // |to_block|, so we go through the given phi ids and check that they exactly
-  // match the OpPhi instructions in |to_block|.
-  uint32_t phi_index = 0;
-  // An explicit loop, rather than applying a lambda to each OpPhi in |bb_to|,
-  // makes sense here because we need to increment |phi_index| for each OpPhi
-  // instruction.
-  for (auto& inst : *bb_to) {
-    if (inst.opcode() != SpvOpPhi) {
-      // The OpPhi instructions all occur at the start of the block; if we find
-      // a non-OpPhi then we have seen them all.
-      break;
-    }
-    if (phi_index == static_cast<uint32_t>(message_.phi_id().size())) {
-      // Not enough phi ids have been provided to account for the OpPhi
-      // instructions.
-      return false;
-    }
-    // Look for an instruction defining the next phi id.
-    opt::Instruction* phi_extension =
-        context->get_def_use_mgr()->GetDef(message_.phi_id()[phi_index]);
-    if (!phi_extension) {
-      // The id given to extend this OpPhi does not exist.
-      return false;
-    }
-    if (phi_extension->type_id() != inst.type_id()) {
-      // The instruction given to extend this OpPhi either does not have a type
-      // or its type does not match that of the OpPhi.
-      return false;
-    }
-
-    if (context->get_instr_block(phi_extension)) {
-      // The instruction defining the phi id has an associated block (i.e., it
-      // is not a global value).  Check whether its definition dominates the
-      // exit of |from_block|.
-      auto dominator_analysis =
-          context->GetDominatorAnalysis(bb_from->GetParent());
-      if (!dominator_analysis->Dominates(phi_extension,
-                                         bb_from->terminator())) {
-        // The given id is no good as its definition does not dominate the exit
-        // of |from_block|
-        return false;
-      }
-    }
-    phi_index++;
-  }
-  // Reject the transformation if not all of the ids for extending OpPhi
-  // instructions are needed. This might turn out to be stricter than necessary;
-  // perhaps it would be OK just to not use the ids in this case.
-  return phi_index == static_cast<uint32_t>(message_.phi_id().size());
-}
-
-bool TransformationAddDeadBreak::FromBlockIsInLoopContinueConstruct(
-    opt::IRContext* context, uint32_t maybe_loop_header) const {
-  // We deem a block to be part of a loop's continue construct if the loop's
-  // continue target dominates the block.
-  auto containing_construct_block = context->cfg()->block(maybe_loop_header);
-  if (containing_construct_block->IsLoopHeader()) {
-    auto continue_target = containing_construct_block->ContinueBlockId();
-    if (context->GetDominatorAnalysis(containing_construct_block->GetParent())
-            ->Dominates(continue_target, message_.from_block())) {
-      return true;
-    }
-  }
-  return false;
-}
-
 bool TransformationAddDeadBreak::AddingBreakRespectsStructuredControlFlow(
     opt::IRContext* context, opt::BasicBlock* bb_from) const {
   // Look at the structured control flow associated with |from_block| and
@@ -180,7 +89,8 @@
     // TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/2577): We do not
     //  currently allow a dead break from a back edge block, but we could and
     //  ultimately should.
-    return !FromBlockIsInLoopContinueConstruct(context, containing_construct);
+    return !fuzzerutil::BlockIsInLoopContinueConstruct(
+        context, message_.from_block(), containing_construct);
   }
 
   // Case (3) holds if and only if |to_block| is the merge block for this
@@ -191,7 +101,8 @@
   if (containing_loop_header &&
       message_.to_block() ==
           context->cfg()->block(containing_loop_header)->MergeBlockId()) {
-    return !FromBlockIsInLoopContinueConstruct(context, containing_loop_header);
+    return !fuzzerutil::BlockIsInLoopContinueConstruct(
+        context, message_.from_block(), containing_loop_header);
   }
   return false;
 }
@@ -199,7 +110,7 @@
 bool TransformationAddDeadBreak::IsApplicable(
     opt::IRContext* context, const FactManager& /*unused*/) const {
   // First, we check that a constant with the same value as
-  // |break_condition_value| is present.
+  // |message_.break_condition_value| is present.
   opt::analysis::Bool bool_type;
   auto registered_bool_type =
       context->get_type_mgr()->GetRegisteredType(&bool_type);
@@ -214,17 +125,20 @@
     return false;
   }
 
-  // Check that |from_block| and |to_block| really are block ids
-  opt::BasicBlock* bb_from = MaybeFindBlock(context, message_.from_block());
+  // Check that |message_.from_block| and |message_.to_block| really are block
+  // ids
+  opt::BasicBlock* bb_from =
+      fuzzerutil::MaybeFindBlock(context, message_.from_block());
   if (bb_from == nullptr) {
     return false;
   }
-  opt::BasicBlock* bb_to = MaybeFindBlock(context, message_.to_block());
+  opt::BasicBlock* bb_to =
+      fuzzerutil::MaybeFindBlock(context, message_.to_block());
   if (bb_to == nullptr) {
     return false;
   }
 
-  // Check that |from_block| ends with an unconditional branch.
+  // Check that |message_.from_block| ends with an unconditional branch.
   if (bb_from->terminator()->opcode() != SpvOpBranch) {
     // The block associated with the id does not end with an unconditional
     // branch.
@@ -243,7 +157,8 @@
       "The id of the block we found should match the target id for the break.");
 
   // Check whether the data passed to extend OpPhi instructions is appropriate.
-  if (!PhiIdsOk(context, bb_from, bb_to)) {
+  if (!fuzzerutil::PhiIdsOkForNewEdge(context, bb_from, bb_to,
+                                      message_.phi_id())) {
     return false;
   }
 
@@ -254,50 +169,10 @@
 
 void TransformationAddDeadBreak::Apply(opt::IRContext* context,
                                        FactManager* /*unused*/) const {
-  // Get the id of the boolean constant to be used as the break condition.
-  opt::analysis::Bool bool_type;
-  opt::analysis::BoolConstant bool_constant(
-      context->get_type_mgr()->GetRegisteredType(&bool_type)->AsBool(),
-      message_.break_condition_value());
-  uint32_t bool_id = context->get_constant_mgr()->FindDeclaredConstant(
-      &bool_constant, context->get_type_mgr()->GetId(&bool_type));
-
-  auto bb_from = context->cfg()->block(message_.from_block());
-  auto bb_to = context->cfg()->block(message_.to_block());
-  const bool from_to_edge_already_exists = bb_from->IsSuccessor(bb_to);
-  auto successor = bb_from->terminator()->GetSingleWordInOperand(0);
-  assert(bb_from->terminator()->opcode() == SpvOpBranch &&
-         "Precondition for the transformation requires that the source block "
-         "ends with OpBranch");
-
-  // Add the dead break, by turning OpBranch into OpBranchConditional, and
-  // ordering the targets depending on whether the given boolean corresponds to
-  // true or false.
-  bb_from->terminator()->SetOpcode(SpvOpBranchConditional);
-  bb_from->terminator()->SetInOperands(
-      {{SPV_OPERAND_TYPE_ID, {bool_id}},
-       {SPV_OPERAND_TYPE_ID,
-        {message_.break_condition_value() ? successor : message_.to_block()}},
-       {SPV_OPERAND_TYPE_ID,
-        {message_.break_condition_value() ? message_.to_block() : successor}}});
-
-  // Update OpPhi instructions in the target block if this break adds a
-  // previously non-existent edge from source to target.
-  if (!from_to_edge_already_exists) {
-    uint32_t phi_index = 0;
-    for (auto& inst : *bb_to) {
-      if (inst.opcode() != SpvOpPhi) {
-        break;
-      }
-      assert(phi_index < static_cast<uint32_t>(message_.phi_id().size()) &&
-             "There should be exactly one phi id per OpPhi instruction.");
-      inst.AddOperand({SPV_OPERAND_TYPE_ID, {message_.phi_id()[phi_index]}});
-      inst.AddOperand({SPV_OPERAND_TYPE_ID, {message_.from_block()}});
-      phi_index++;
-    }
-    assert(phi_index == static_cast<uint32_t>(message_.phi_id().size()) &&
-           "There should be exactly one phi id per OpPhi instruction.");
-  }
+  fuzzerutil::AddUnreachableEdgeAndUpdateOpPhis(
+      context, context->cfg()->block(message_.from_block()),
+      context->cfg()->block(message_.to_block()),
+      message_.break_condition_value(), message_.phi_id());
 
   // Invalidate all analyses
   context->InvalidateAnalysesExceptFor(opt::IRContext::Analysis::kAnalysisNone);
diff --git a/source/fuzz/transformation_add_dead_break.h b/source/fuzz/transformation_add_dead_break.h
index 23392ba..aeb4dbb 100644
--- a/source/fuzz/transformation_add_dead_break.h
+++ b/source/fuzz/transformation_add_dead_break.h
@@ -34,25 +34,25 @@
                              bool break_condition_value,
                              std::vector<uint32_t> phi_id);
 
-  // - |message.from_block| must be the id of a block a in the given module.
-  // - |message.to_block| must be the id of a block b in the given module.
-  // - if |message.break_condition_value| holds (does not hold) then
+  // - |message_.from_block| must be the id of a block a in the given module.
+  // - |message_.to_block| must be the id of a block b in the given module.
+  // - if |message_.break_condition_value| holds (does not hold) then
   //   OpConstantTrue (OpConstantFalse) must be present in the module
-  // - |message.phi_ids| must be a list of ids that are all available at
-  //   |message.from_block|
+  // - |message_.phi_ids| must be a list of ids that are all available at
+  //   |message_.from_block|
   // - a and b must be in the same function.
   // - b must be a merge block.
   // - a must end with an unconditional branch to some block c.
   // - replacing this branch with a conditional branch to b or c, with
-  //   the boolean constant associated with |message.break_condition_value| as
-  //   the condition, and the ids in |message.phi_ids| used to extend
+  //   the boolean constant associated with |message_.break_condition_value| as
+  //   the condition, and the ids in |message_.phi_ids| used to extend
   //   any OpPhi instructions at b as a result of the edge from a, must
   //   maintain validity of the module.
   bool IsApplicable(opt::IRContext* context,
                     const FactManager& fact_manager) const override;
 
   // Replaces the terminator of a with a conditional branch to b or c.
-  // The boolean constant associated with |message.break_condition_value| is
+  // The boolean constant associated with |message_.break_condition_value| is
   // used as the condition, and the order of b and c is arranged such that
   // control is guaranteed to jump to c.
   void Apply(opt::IRContext* context, FactManager* fact_manager) const override;
@@ -60,21 +60,6 @@
   protobufs::Transformation ToMessage() const override;
 
  private:
-  // Return the block with id |maybe_block_id| if it exists, and nullptr
-  // otherwise.
-  opt::BasicBlock* MaybeFindBlock(opt::IRContext* context,
-                                  uint32_t maybe_block_id) const;
-
-  // Returns true if and only if the phi ids associated with |message_| are
-  // sufficient to allow an edge from |bb_from| to |bb_to| to be added.
-  bool PhiIdsOk(opt::IRContext* context, opt::BasicBlock* bb_from,
-                opt::BasicBlock* bb_to) const;
-
-  // Returns true if and only if |message_.from_block| is in the continue
-  // construct of a loop headed at |maybe_loop_header|.
-  bool FromBlockIsInLoopContinueConstruct(opt::IRContext* context,
-                                          uint32_t maybe_loop_header) const;
-
   // Returns true if and only if adding an edge from |bb_from| to
   // |message_.to_block| respects structured control flow.
   bool AddingBreakRespectsStructuredControlFlow(opt::IRContext* context,
diff --git a/source/fuzz/transformation_add_dead_continue.cpp b/source/fuzz/transformation_add_dead_continue.cpp
new file mode 100644
index 0000000..e3b3da2
--- /dev/null
+++ b/source/fuzz/transformation_add_dead_continue.cpp
@@ -0,0 +1,133 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/transformation_add_dead_continue.h"
+
+#include "source/fuzz/fuzzer_util.h"
+
+namespace spvtools {
+namespace fuzz {
+
+TransformationAddDeadContinue::TransformationAddDeadContinue(
+    const spvtools::fuzz::protobufs::TransformationAddDeadContinue& message)
+    : message_(message) {}
+
+TransformationAddDeadContinue::TransformationAddDeadContinue(
+    uint32_t from_block, bool continue_condition_value,
+    std::vector<uint32_t> phi_id) {
+  message_.set_from_block(from_block);
+  message_.set_continue_condition_value(continue_condition_value);
+  for (auto id : phi_id) {
+    message_.add_phi_id(id);
+  }
+}
+
+bool TransformationAddDeadContinue::IsApplicable(
+    opt::IRContext* context, const FactManager& /*unused*/) const {
+  // First, we check that a constant with the same value as
+  // |message_.continue_condition_value| is present.
+  opt::analysis::Bool bool_type;
+  auto registered_bool_type =
+      context->get_type_mgr()->GetRegisteredType(&bool_type);
+  if (!registered_bool_type) {
+    return false;
+  }
+  opt::analysis::BoolConstant bool_constant(
+      registered_bool_type->AsBool(), message_.continue_condition_value());
+  if (!context->get_constant_mgr()->FindConstant(&bool_constant)) {
+    // The required constant is not present, so the transformation cannot be
+    // applied.
+    return false;
+  }
+
+  // Check that |message_.from_block| really is a block id.
+  opt::BasicBlock* bb_from =
+      fuzzerutil::MaybeFindBlock(context, message_.from_block());
+  if (bb_from == nullptr) {
+    return false;
+  }
+
+  // Check that |message_.from_block| ends with an unconditional branch.
+  if (bb_from->terminator()->opcode() != SpvOpBranch) {
+    // The block associated with the id does not end with an unconditional
+    // branch.
+    return false;
+  }
+
+  assert(bb_from != nullptr &&
+         "We should have found a block if this line of code is reached.");
+  assert(
+      bb_from->id() == message_.from_block() &&
+      "The id of the block we found should match the source id for the break.");
+
+  // Get the header for the innermost loop containing |message_.from_block|.
+  // Because the structured CFG analysis does not regard a loop header as part
+  // of the loop it heads, we check first whether bb_from is a loop header
+  // before using the structured CFG analysis.
+  auto loop_header = bb_from->IsLoopHeader()
+                         ? message_.from_block()
+                         : context->GetStructuredCFGAnalysis()->ContainingLoop(
+                               message_.from_block());
+  if (!loop_header) {
+    return false;
+  }
+
+  if (fuzzerutil::BlockIsInLoopContinueConstruct(context, message_.from_block(),
+                                                 loop_header)) {
+    // We cannot jump to the continue target from the continue construct.
+    return false;
+  }
+
+  auto continue_block = context->cfg()->block(loop_header)->ContinueBlockId();
+
+  if (context->GetStructuredCFGAnalysis()->IsMergeBlock(continue_block)) {
+    // A branch straight to the continue target that is also a merge block might
+    // break the property that a construct header must dominate its merge block
+    // (if the merge block is reachable).
+    return false;
+  }
+
+  // The transformation is good if and only if the given phi ids are sufficient
+  // to extend relevant OpPhi instructions in the continue block.
+  return fuzzerutil::PhiIdsOkForNewEdge(context, bb_from,
+                                        context->cfg()->block(continue_block),
+                                        message_.phi_id());
+}
+
+void TransformationAddDeadContinue::Apply(opt::IRContext* context,
+                                          FactManager* /*unused*/) const {
+  auto bb_from = context->cfg()->block(message_.from_block());
+  auto continue_block =
+      bb_from->IsLoopHeader()
+          ? bb_from->ContinueBlockId()
+          : context->GetStructuredCFGAnalysis()->LoopContinueBlock(
+                message_.from_block());
+  assert(continue_block &&
+         "Precondition for this transformation requires that "
+         "message_.from_block must be in a loop.");
+  fuzzerutil::AddUnreachableEdgeAndUpdateOpPhis(
+      context, bb_from, context->cfg()->block(continue_block),
+      message_.continue_condition_value(), message_.phi_id());
+  // Invalidate all analyses
+  context->InvalidateAnalysesExceptFor(opt::IRContext::Analysis::kAnalysisNone);
+}
+
+protobufs::Transformation TransformationAddDeadContinue::ToMessage() const {
+  protobufs::Transformation result;
+  *result.mutable_add_dead_continue() = message_;
+  return result;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_add_dead_continue.h b/source/fuzz/transformation_add_dead_continue.h
new file mode 100644
index 0000000..e49e2a8
--- /dev/null
+++ b/source/fuzz/transformation_add_dead_continue.h
@@ -0,0 +1,68 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_TRANSFORMATION_ADD_DEAD_CONTINUE_H_
+#define SOURCE_FUZZ_TRANSFORMATION_ADD_DEAD_CONTINUE_H_
+
+#include <vector>
+
+#include "source/fuzz/fact_manager.h"
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationAddDeadContinue : public Transformation {
+ public:
+  explicit TransformationAddDeadContinue(
+      const protobufs::TransformationAddDeadContinue& message);
+
+  TransformationAddDeadContinue(uint32_t from_block,
+                                bool continue_condition_value,
+                                std::vector<uint32_t> phi_id);
+
+  // - |message_.from_block| must be the id of a block a in the given module.
+  // - a must be contained in a loop with continue target b
+  // - b must not be the merge block of a selection construct
+  // - if |message_.continue_condition_value| holds (does not hold) then
+  //   OpConstantTrue (OpConstantFalse) must be present in the module
+  // - |message_.phi_ids| must be a list of ids that are all available at
+  //   |message_.from_block|
+  // - a must end with an unconditional branch to some block c.
+  // - replacing this branch with a conditional branch to b or c, with
+  //   the boolean constant associated with |message_.continue_condition_value|
+  //   as the condition, and the ids in |message_.phi_ids| used to extend any
+  //   OpPhi instructions at b as a result of the edge from a, must maintain
+  //   validity of the module.
+  bool IsApplicable(opt::IRContext* context,
+                    const FactManager& fact_manager) const override;
+
+  // Replaces the terminator of a with a conditional branch to b or c.
+  // The boolean constant associated with |message_.continue_condition_value| is
+  // used as the condition, and the order of b and c is arranged such that
+  // control is guaranteed to jump to c.
+  void Apply(opt::IRContext* context, FactManager* fact_manager) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+ private:
+  protobufs::TransformationAddDeadContinue message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_ADD_DEAD_CONTINUE_H_
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index 9e4066b..c0e2925 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -25,6 +25,7 @@
           transformation_add_constant_boolean_test.cpp
           transformation_add_constant_scalar_test.cpp
           transformation_add_dead_break_test.cpp
+          transformation_add_dead_continue_test.cpp
           transformation_add_type_boolean_test.cpp
           transformation_add_type_float_test.cpp
           transformation_add_type_int_test.cpp
diff --git a/test/fuzz/fuzz_test_util.cpp b/test/fuzz/fuzz_test_util.cpp
index 545512f..e2b9518 100644
--- a/test/fuzz/fuzz_test_util.cpp
+++ b/test/fuzz/fuzz_test_util.cpp
@@ -79,6 +79,10 @@
 std::string ToString(spv_target_env env, const opt::IRContext* ir) {
   std::vector<uint32_t> binary;
   ir->module()->ToBinary(&binary, false);
+  return ToString(env, binary);
+}
+
+std::string ToString(spv_target_env env, const std::vector<uint32_t>& binary) {
   SpirvTools t(env);
   std::string result;
   t.Disassemble(binary, &result, kFuzzDisassembleOption);
diff --git a/test/fuzz/fuzz_test_util.h b/test/fuzz/fuzz_test_util.h
index 76b7fc2..88a0b20 100644
--- a/test/fuzz/fuzz_test_util.h
+++ b/test/fuzz/fuzz_test_util.h
@@ -17,6 +17,8 @@
 
 #include "gtest/gtest.h"
 
+#include <vector>
+
 #include "source/opt/build_module.h"
 #include "source/opt/ir_context.h"
 #include "spirv-tools/libspirv.h"
@@ -51,6 +53,10 @@
 // Useful for debugging.
 std::string ToString(spv_target_env env, const opt::IRContext* ir);
 
+// Returns the disassembly of the given binary as a string.
+// Useful for debugging.
+std::string ToString(spv_target_env env, const std::vector<uint32_t>& binary);
+
 // Assembly options for writing fuzzer tests.  It simplifies matters if
 // numeric ids do not change.
 const uint32_t kFuzzAssembleOption =
@@ -64,6 +70,29 @@
     [](spv_message_level_t, const char*, const spv_position_t&,
        const char*) -> void {};
 
+const spvtools::MessageConsumer kConsoleMessageConsumer =
+    [](spv_message_level_t level, const char*, const spv_position_t& position,
+       const char* message) -> void {
+  switch (level) {
+    case SPV_MSG_FATAL:
+    case SPV_MSG_INTERNAL_ERROR:
+    case SPV_MSG_ERROR:
+      std::cerr << "error: line " << position.index << ": " << message
+                << std::endl;
+      break;
+    case SPV_MSG_WARNING:
+      std::cout << "warning: line " << position.index << ": " << message
+                << std::endl;
+      break;
+    case SPV_MSG_INFO:
+      std::cout << "info: line " << position.index << ": " << message
+                << std::endl;
+      break;
+    default:
+      break;
+  }
+};
+
 }  // namespace fuzz
 }  // namespace spvtools
 
diff --git a/test/fuzz/fuzzer_replayer_test.cpp b/test/fuzz/fuzzer_replayer_test.cpp
index 2cb595a..166e6ea 100644
--- a/test/fuzz/fuzzer_replayer_test.cpp
+++ b/test/fuzz/fuzzer_replayer_test.cpp
@@ -33,6 +33,7 @@
 
   std::vector<uint32_t> binary_in;
   SpirvTools t(env);
+  t.SetMessageConsumer(kConsoleMessageConsumer);
   ASSERT_TRUE(t.Assemble(shader, &binary_in, kFuzzAssembleOption));
   ASSERT_TRUE(t.Validate(binary_in));
 
diff --git a/test/fuzz/transformation_add_dead_continue_test.cpp b/test/fuzz/transformation_add_dead_continue_test.cpp
new file mode 100644
index 0000000..573db19
--- /dev/null
+++ b/test/fuzz/transformation_add_dead_continue_test.cpp
@@ -0,0 +1,1034 @@
+// Copyright (c) 2019 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/transformation_add_dead_continue.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationAddDeadContinueTest, SimpleExample) {
+  // For a simple loop, checks that some dead continue scenarios are possible,
+  // sanity-checks that some illegal scenarios are indeed not allowed, and then
+  // applies a transformation.
+
+  // The SPIR-V for this test is adapted from the following GLSL, by separating
+  // some assignments into their own basic blocks, and adding constants for true
+  // and false:
+  //
+  // void main() {
+  //   int x = 0;
+  //   for (int i = 0; i < 10; i++) {
+  //     x = x + i;
+  //     x = x + i;
+  //   }
+  // }
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %8 "x"
+               OpName %10 "i"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 0
+         %17 = OpConstant %6 10
+         %18 = OpTypeBool
+         %41 = OpConstantTrue %18
+         %42 = OpConstantFalse %18
+         %27 = OpConstant %6 1
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %10 = OpVariable %7 Function
+               OpStore %8 %9
+               OpStore %10 %9
+               OpBranch %11
+         %11 = OpLabel
+               OpLoopMerge %13 %14 None
+               OpBranch %15
+         %15 = OpLabel
+         %16 = OpLoad %6 %10
+         %19 = OpSLessThan %18 %16 %17
+               OpBranchConditional %19 %12 %13
+         %12 = OpLabel
+         %20 = OpLoad %6 %8
+         %21 = OpLoad %6 %10
+         %22 = OpIAdd %6 %20 %21
+               OpStore %8 %22
+               OpBranch %40
+         %40 = OpLabel
+         %23 = OpLoad %6 %8
+         %24 = OpLoad %6 %10
+         %25 = OpIAdd %6 %23 %24
+               OpStore %8 %25
+               OpBranch %14
+         %14 = OpLabel
+         %26 = OpLoad %6 %10
+         %28 = OpIAdd %6 %26 %27
+               OpStore %10 %28
+               OpBranch %11
+         %13 = 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()));
+  FactManager fact_manager;
+
+  // These are all possibilities.
+  ASSERT_TRUE(TransformationAddDeadContinue(11, true, {})
+                  .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(TransformationAddDeadContinue(11, false, {})
+                  .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(TransformationAddDeadContinue(12, true, {})
+                  .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(TransformationAddDeadContinue(12, false, {})
+                  .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(TransformationAddDeadContinue(40, true, {})
+                  .IsApplicable(context.get(), fact_manager));
+  ASSERT_TRUE(TransformationAddDeadContinue(40, false, {})
+                  .IsApplicable(context.get(), fact_manager));
+
+  // Inapplicable: 100 is not a block id.
+  ASSERT_FALSE(TransformationAddDeadContinue(100, true, {})
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Inapplicable: 10 is not in a loop.
+  ASSERT_FALSE(TransformationAddDeadContinue(10, true, {})
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Inapplicable: 15 does not branch unconditionally to a single successor.
+  ASSERT_FALSE(TransformationAddDeadContinue(15, true, {})
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Inapplicable: 13 is not in a loop and has no successor.
+  ASSERT_FALSE(TransformationAddDeadContinue(13, true, {})
+                   .IsApplicable(context.get(), fact_manager));
+
+  // Inapplicable: 14 is the loop continue target, so it's not OK to jump to
+  // the loop continue from there.
+  ASSERT_FALSE(TransformationAddDeadContinue(14, false, {})
+                   .IsApplicable(context.get(), fact_manager));
+
+  // These are the transformations we will apply.
+  auto transformation1 = TransformationAddDeadContinue(11, true, {});
+  auto transformation2 = TransformationAddDeadContinue(12, false, {});
+  auto transformation3 = TransformationAddDeadContinue(40, true, {});
+
+  ASSERT_TRUE(transformation1.IsApplicable(context.get(), fact_manager));
+  transformation1.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  ASSERT_TRUE(transformation2.IsApplicable(context.get(), fact_manager));
+  transformation2.Apply(context.get(), &fact_manager);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  ASSERT_TRUE(transformation3.IsApplicable(context.get(), fact_manager));
+  transformation3.Apply(context.get(), &fact_manager);
+  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
+               OpName %4 "main"
+               OpName %8 "x"
+               OpName %10 "i"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 0
+         %17 = OpConstant %6 10
+         %18 = OpTypeBool
+         %41 = OpConstantTrue %18
+         %42 = OpConstantFalse %18
+         %27 = OpConstant %6 1
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %10 = OpVariable %7 Function
+               OpStore %8 %9
+               OpStore %10 %9
+               OpBranch %11
+         %11 = OpLabel
+               OpLoopMerge %13 %14 None
+               OpBranchConditional %41 %15 %14
+         %15 = OpLabel
+         %16 = OpLoad %6 %10
+         %19 = OpSLessThan %18 %16 %17
+               OpBranchConditional %19 %12 %13
+         %12 = OpLabel
+         %20 = OpLoad %6 %8
+         %21 = OpLoad %6 %10
+         %22 = OpIAdd %6 %20 %21
+               OpStore %8 %22
+               OpBranchConditional %42 %14 %40
+         %40 = OpLabel
+         %23 = OpLoad %6 %8
+         %24 = OpLoad %6 %10
+         %25 = OpIAdd %6 %23 %24
+               OpStore %8 %25
+               OpBranchConditional %41 %14 %14
+         %14 = OpLabel
+         %26 = OpLoad %6 %10
+         %28 = OpIAdd %6 %26 %27
+               OpStore %10 %28
+               OpBranch %11
+         %13 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationAddDeadContinueTest,
+     DoNotAllowContinueToMergeBlockOfAnotherLoop) {
+  // A loop header must dominate its merge block if that merge block is
+  // reachable. We are thus not allowed to add a dead continue that would result
+  // in violation of this property. This test checks for such a scenario.
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %16 %139
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypePointer Function %6
+          %8 = OpTypeBool
+         %14 = OpTypeVector %6 4
+         %15 = OpTypePointer Input %14
+         %16 = OpVariable %15 Input
+        %138 = OpTypePointer Output %14
+        %139 = OpVariable %138 Output
+        %400 = OpConstantTrue %8
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %500
+        %500 = OpLabel
+               OpLoopMerge %501 %502 None
+               OpBranch %503 ; We are not allowed to change this to OpBranchConditional %400 %503 %502
+        %503 = OpLabel
+               OpLoopMerge %502 %504 None
+               OpBranchConditional %400 %505 %504
+        %505 = OpLabel
+               OpBranch %502
+        %504 = OpLabel
+               OpBranch %503
+        %502 = OpLabel
+               OpBranchConditional %400 %501 %500
+        %501 = 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()));
+  FactManager fact_manager;
+
+  ASSERT_FALSE(TransformationAddDeadContinue(500, true, {})
+                   .IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(TransformationAddDeadContinue(500, false, {})
+                   .IsApplicable(context.get(), fact_manager));
+}
+
+TEST(TransformationAddDeadContinueTest, DoNotAllowContinueToSelectionMerge) {
+  // A selection header must dominate its merge block if that merge block is
+  // reachable. We are thus not allowed to add a dead continue that would result
+  // in violation of this property. This test checks for such a scenario.
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main" %16 %139
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeFloat 32
+          %7 = OpTypePointer Function %6
+          %8 = OpTypeBool
+         %14 = OpTypeVector %6 4
+         %15 = OpTypePointer Input %14
+         %16 = OpVariable %15 Input
+        %138 = OpTypePointer Output %14
+        %139 = OpVariable %138 Output
+        %400 = OpConstantTrue %8
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %500
+        %500 = OpLabel
+               OpLoopMerge %501 %502 None
+               OpBranch %503 ; We are not allowed to change this to OpBranchConditional %400 %503 %502
+        %503 = OpLabel
+               OpSelectionMerge %502 None
+               OpBranchConditional %400 %505 %504
+        %505 = OpLabel
+               OpBranch %502
+        %504 = OpLabel
+               OpBranch %502
+        %502 = OpLabel
+               OpBranchConditional %400 %501 %500
+        %501 = 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()));
+  FactManager fact_manager;
+
+  ASSERT_FALSE(TransformationAddDeadContinue(500, true, {})
+                   .IsApplicable(context.get(), fact_manager));
+  ASSERT_FALSE(TransformationAddDeadContinue(500, false, {})
+                   .IsApplicable(context.get(), fact_manager));
+}
+
+TEST(TransformationAddDeadContinueTest, LoopNest) {
+  // Checks some allowed and disallowed scenarios for a nest of loops, including
+  // continuing a loop from an if or switch.
+
+  // The SPIR-V for this test is adapted from the following GLSL:
+  //
+  // void main() {
+  //   int x, y;
+  //   do {
+  //     x++;
+  //     for (int j = 0; j < 100; j++) {
+  //       y++;
+  //       if (x == y) {
+  //         x++;
+  //         if (x == 2) {
+  //           y++;
+  //         }
+  //         switch (x) {
+  //           case 0:
+  //             x = 2;
+  //           default:
+  //             break;
+  //         }
+  //       }
+  //     }
+  //   } while (x > y);
+  //
+  //   for (int i = 0; i < 100; i++) {
+  //     x++;
+  //   }
+  // }
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %12 "x"
+               OpName %16 "j"
+               OpName %27 "y"
+               OpName %55 "i"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+         %10 = OpTypeInt 32 1
+         %11 = OpTypePointer Function %10
+         %14 = OpConstant %10 1
+         %17 = OpConstant %10 0
+         %24 = OpConstant %10 100
+         %25 = OpTypeBool
+         %38 = OpConstant %10 2
+         %67 = OpConstantTrue %25
+         %68 = OpConstantFalse %25
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %12 = OpVariable %11 Function
+         %16 = OpVariable %11 Function
+         %27 = OpVariable %11 Function
+         %55 = OpVariable %11 Function
+               OpBranch %6
+          %6 = OpLabel
+               OpLoopMerge %8 %9 None
+               OpBranch %7
+          %7 = OpLabel
+         %13 = OpLoad %10 %12
+         %15 = OpIAdd %10 %13 %14
+               OpStore %12 %15
+               OpStore %16 %17
+               OpBranch %18
+         %18 = OpLabel
+               OpLoopMerge %20 %21 None
+               OpBranch %22
+         %22 = OpLabel
+         %23 = OpLoad %10 %16
+         %26 = OpSLessThan %25 %23 %24
+               OpBranchConditional %26 %19 %20
+         %19 = OpLabel
+         %28 = OpLoad %10 %27
+         %29 = OpIAdd %10 %28 %14
+               OpStore %27 %29
+         %30 = OpLoad %10 %12
+         %31 = OpLoad %10 %27
+         %32 = OpIEqual %25 %30 %31
+               OpSelectionMerge %34 None
+               OpBranchConditional %32 %33 %34
+         %33 = OpLabel
+         %35 = OpLoad %10 %12
+         %36 = OpIAdd %10 %35 %14
+               OpStore %12 %36
+         %37 = OpLoad %10 %12
+         %39 = OpIEqual %25 %37 %38
+               OpSelectionMerge %41 None
+               OpBranchConditional %39 %40 %41
+         %40 = OpLabel
+         %42 = OpLoad %10 %27
+         %43 = OpIAdd %10 %42 %14
+               OpStore %27 %43
+               OpBranch %41
+         %41 = OpLabel
+         %44 = OpLoad %10 %12
+               OpSelectionMerge %47 None
+               OpSwitch %44 %46 0 %45
+         %46 = OpLabel
+               OpBranch %47
+         %45 = OpLabel
+               OpStore %12 %38
+               OpBranch %46
+         %47 = OpLabel
+               OpBranch %34
+         %34 = OpLabel
+               OpBranch %21
+         %21 = OpLabel
+         %50 = OpLoad %10 %16
+         %51 = OpIAdd %10 %50 %14
+               OpStore %16 %51
+               OpBranch %18
+         %20 = OpLabel
+               OpBranch %9
+          %9 = OpLabel
+         %52 = OpLoad %10 %12
+         %53 = OpLoad %10 %27
+         %54 = OpSGreaterThan %25 %52 %53
+               OpBranchConditional %54 %6 %8
+          %8 = OpLabel
+               OpStore %55 %17
+               OpBranch %56
+         %56 = OpLabel
+               OpLoopMerge %58 %59 None
+               OpBranch %60
+         %60 = OpLabel
+         %61 = OpLoad %10 %55
+         %62 = OpSLessThan %25 %61 %24
+               OpBranchConditional %62 %57 %58
+         %57 = OpLabel
+         %63 = OpLoad %10 %12
+         %64 = OpIAdd %10 %63 %14
+               OpStore %12 %64
+               OpBranch %59
+         %59 = OpLabel
+         %65 = OpLoad %10 %55
+         %66 = OpIAdd %10 %65 %14
+               OpStore %55 %66
+               OpBranch %56
+         %58 = 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()));
+
+  FactManager fact_manager;
+
+  std::vector<uint32_t> good = {6, 7, 18, 20, 34, 40, 45, 46, 47, 56, 57};
+  std::vector<uint32_t> bad = {5, 8, 9, 19, 21, 22, 33, 41, 58, 59, 60};
+
+  for (uint32_t from_block : bad) {
+    ASSERT_FALSE(TransformationAddDeadContinue(from_block, true, {})
+                     .IsApplicable(context.get(), fact_manager));
+  }
+  for (uint32_t from_block : good) {
+    const TransformationAddDeadContinue transformation(from_block, true, {});
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_FALSE(transformation.IsApplicable(context.get(), fact_manager));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %12 "x"
+               OpName %16 "j"
+               OpName %27 "y"
+               OpName %55 "i"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+         %10 = OpTypeInt 32 1
+         %11 = OpTypePointer Function %10
+         %14 = OpConstant %10 1
+         %17 = OpConstant %10 0
+         %24 = OpConstant %10 100
+         %25 = OpTypeBool
+         %38 = OpConstant %10 2
+         %67 = OpConstantTrue %25
+         %68 = OpConstantFalse %25
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %12 = OpVariable %11 Function
+         %16 = OpVariable %11 Function
+         %27 = OpVariable %11 Function
+         %55 = OpVariable %11 Function
+               OpBranch %6
+          %6 = OpLabel
+               OpLoopMerge %8 %9 None
+               OpBranchConditional %67 %7 %9
+          %7 = OpLabel
+         %13 = OpLoad %10 %12
+         %15 = OpIAdd %10 %13 %14
+               OpStore %12 %15
+               OpStore %16 %17
+               OpBranchConditional %67 %18 %9
+         %18 = OpLabel
+               OpLoopMerge %20 %21 None
+               OpBranchConditional %67 %22 %21
+         %22 = OpLabel
+         %23 = OpLoad %10 %16
+         %26 = OpSLessThan %25 %23 %24
+               OpBranchConditional %26 %19 %20
+         %19 = OpLabel
+         %28 = OpLoad %10 %27
+         %29 = OpIAdd %10 %28 %14
+               OpStore %27 %29
+         %30 = OpLoad %10 %12
+         %31 = OpLoad %10 %27
+         %32 = OpIEqual %25 %30 %31
+               OpSelectionMerge %34 None
+               OpBranchConditional %32 %33 %34
+         %33 = OpLabel
+         %35 = OpLoad %10 %12
+         %36 = OpIAdd %10 %35 %14
+               OpStore %12 %36
+         %37 = OpLoad %10 %12
+         %39 = OpIEqual %25 %37 %38
+               OpSelectionMerge %41 None
+               OpBranchConditional %39 %40 %41
+         %40 = OpLabel
+         %42 = OpLoad %10 %27
+         %43 = OpIAdd %10 %42 %14
+               OpStore %27 %43
+               OpBranchConditional %67 %41 %21
+         %41 = OpLabel
+         %44 = OpLoad %10 %12
+               OpSelectionMerge %47 None
+               OpSwitch %44 %46 0 %45
+         %46 = OpLabel
+               OpBranchConditional %67 %47 %21
+         %45 = OpLabel
+               OpStore %12 %38
+               OpBranchConditional %67 %46 %21
+         %47 = OpLabel
+               OpBranchConditional %67 %34 %21
+         %34 = OpLabel
+               OpBranchConditional %67 %21 %21
+         %21 = OpLabel
+         %50 = OpLoad %10 %16
+         %51 = OpIAdd %10 %50 %14
+               OpStore %16 %51
+               OpBranch %18
+         %20 = OpLabel
+               OpBranchConditional %67 %9 %9
+          %9 = OpLabel
+         %52 = OpLoad %10 %12
+         %53 = OpLoad %10 %27
+         %54 = OpSGreaterThan %25 %52 %53
+               OpBranchConditional %54 %6 %8
+          %8 = OpLabel
+               OpStore %55 %17
+               OpBranch %56
+         %56 = OpLabel
+               OpLoopMerge %58 %59 None
+               OpBranchConditional %67 %60 %59
+         %60 = OpLabel
+         %61 = OpLoad %10 %55
+         %62 = OpSLessThan %25 %61 %24
+               OpBranchConditional %62 %57 %58
+         %57 = OpLabel
+         %63 = OpLoad %10 %12
+         %64 = OpIAdd %10 %63 %14
+               OpStore %12 %64
+               OpBranchConditional %67 %59 %59
+         %59 = OpLabel
+         %65 = OpLoad %10 %55
+         %66 = OpIAdd %10 %65 %14
+               OpStore %55 %66
+               OpBranch %56
+         %58 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationAddDeadConditionalTest, LoopInContinueConstruct) {
+  // Considers some scenarios where there is a loop in a loop's continue
+  // construct.
+
+  // The SPIR-V for this test is adapted from the following GLSL, with inlining
+  // applied so that the loop from foo is in the main loop's continue construct:
+  //
+  // int foo() {
+  //   int result = 0;
+  //   for (int j = 0; j < 10; j++) {
+  //     result++;
+  //   }
+  //   return result;
+  // }
+  //
+  // void main() {
+  //   for (int i = 0; i < 100; i += foo()) {
+  //   }
+  // }
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %31 "i"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeFunction %6
+         %10 = OpTypePointer Function %6
+         %12 = OpConstant %6 0
+         %20 = OpConstant %6 10
+         %21 = OpTypeBool
+        %100 = OpConstantFalse %21
+         %24 = OpConstant %6 1
+         %38 = OpConstant %6 100
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %43 = OpVariable %10 Function
+         %44 = OpVariable %10 Function
+         %45 = OpVariable %10 Function
+         %31 = OpVariable %10 Function
+               OpStore %31 %12
+               OpBranch %32
+         %32 = OpLabel
+               OpLoopMerge %34 %35 None
+               OpBranch %36
+         %36 = OpLabel
+         %37 = OpLoad %6 %31
+         %39 = OpSLessThan %21 %37 %38
+               OpBranchConditional %39 %33 %34
+         %33 = OpLabel
+               OpBranch %35
+         %35 = OpLabel
+               OpStore %43 %12
+               OpStore %44 %12
+               OpBranch %46
+         %46 = OpLabel
+               OpLoopMerge %47 %48 None
+               OpBranch %49
+         %49 = OpLabel
+         %50 = OpLoad %6 %44
+         %51 = OpSLessThan %21 %50 %20
+               OpBranchConditional %51 %52 %47
+         %52 = OpLabel
+         %53 = OpLoad %6 %43
+               OpBranch %101
+        %101 = OpLabel
+         %54 = OpIAdd %6 %53 %24
+               OpStore %43 %54
+               OpBranch %48
+         %48 = OpLabel
+         %55 = OpLoad %6 %44
+         %56 = OpIAdd %6 %55 %24
+               OpStore %44 %56
+               OpBranch %46
+         %47 = OpLabel
+         %57 = OpLoad %6 %43
+               OpStore %45 %57
+         %40 = OpLoad %6 %45
+         %41 = OpLoad %6 %31
+         %42 = OpIAdd %6 %41 %40
+               OpStore %31 %42
+               OpBranch %32
+         %34 = 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()));
+
+  FactManager fact_manager;
+
+  std::vector<uint32_t> good = {32, 33, 46, 52, 101};
+  std::vector<uint32_t> bad = {5, 34, 36, 35, 47, 49, 48};
+
+  for (uint32_t from_block : bad) {
+    ASSERT_FALSE(TransformationAddDeadContinue(from_block, false, {})
+                     .IsApplicable(context.get(), fact_manager));
+  }
+  for (uint32_t from_block : good) {
+    const TransformationAddDeadContinue transformation(from_block, false, {});
+    ASSERT_TRUE(transformation.IsApplicable(context.get(), fact_manager));
+    transformation.Apply(context.get(), &fact_manager);
+    ASSERT_FALSE(transformation.IsApplicable(context.get(), fact_manager));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %31 "i"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypeFunction %6
+         %10 = OpTypePointer Function %6
+         %12 = OpConstant %6 0
+         %20 = OpConstant %6 10
+         %21 = OpTypeBool
+        %100 = OpConstantFalse %21
+         %24 = OpConstant %6 1
+         %38 = OpConstant %6 100
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+         %43 = OpVariable %10 Function
+         %44 = OpVariable %10 Function
+         %45 = OpVariable %10 Function
+         %31 = OpVariable %10 Function
+               OpStore %31 %12
+               OpBranch %32
+         %32 = OpLabel
+               OpLoopMerge %34 %35 None
+               OpBranchConditional %100 %35 %36
+         %36 = OpLabel
+         %37 = OpLoad %6 %31
+         %39 = OpSLessThan %21 %37 %38
+               OpBranchConditional %39 %33 %34
+         %33 = OpLabel
+               OpBranchConditional %100 %35 %35
+         %35 = OpLabel
+               OpStore %43 %12
+               OpStore %44 %12
+               OpBranch %46
+         %46 = OpLabel
+               OpLoopMerge %47 %48 None
+               OpBranchConditional %100 %48 %49
+         %49 = OpLabel
+         %50 = OpLoad %6 %44
+         %51 = OpSLessThan %21 %50 %20
+               OpBranchConditional %51 %52 %47
+         %52 = OpLabel
+         %53 = OpLoad %6 %43
+               OpBranchConditional %100 %48 %101
+        %101 = OpLabel
+         %54 = OpIAdd %6 %53 %24
+               OpStore %43 %54
+               OpBranchConditional %100 %48 %48
+         %48 = OpLabel
+         %55 = OpLoad %6 %44
+         %56 = OpIAdd %6 %55 %24
+               OpStore %44 %56
+               OpBranch %46
+         %47 = OpLabel
+         %57 = OpLoad %6 %43
+               OpStore %45 %57
+         %40 = OpLoad %6 %45
+         %41 = OpLoad %6 %31
+         %42 = OpIAdd %6 %41 %40
+               OpStore %31 %42
+               OpBranch %32
+         %34 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+TEST(TransformationAddDeadContinueTest, PhiInstructions) {
+  // Checks that the transformation works in the presence of phi instructions.
+
+  // The SPIR-V for this test is adapted from the following GLSL, with a bit of
+  // extra and artificial work to get some interesting uses of OpPhi:
+  //
+  // void main() {
+  //   int x; int y;
+  //   float f;
+  //   x = 2;
+  //   f = 3.0;
+  //   if (x > y) {
+  //     x = 3;
+  //     f = 4.0;
+  //   } else {
+  //     x = x + 2;
+  //     f = f + 10.0;
+  //   }
+  //   while (x < y) {
+  //     x = x + 1;
+  //     f = f + 1.0;
+  //   }
+  //   y = x;
+  //   f = f + 3.0;
+  // }
+
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %8 "x"
+               OpName %12 "f"
+               OpName %15 "y"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 2
+         %10 = OpTypeFloat 32
+         %11 = OpTypePointer Function %10
+         %13 = OpConstant %10 3
+         %17 = OpTypeBool
+         %80 = OpConstantTrue %17
+         %21 = OpConstant %6 3
+         %22 = OpConstant %10 4
+         %27 = OpConstant %10 10
+         %38 = OpConstant %6 1
+         %41 = OpConstant %10 1
+         %46 = OpUndef %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %12 = OpVariable %11 Function
+         %15 = OpVariable %7 Function
+               OpStore %8 %9
+               OpStore %12 %13
+         %18 = OpSGreaterThan %17 %9 %46
+               OpSelectionMerge %20 None
+               OpBranchConditional %18 %19 %23
+         %19 = OpLabel
+               OpStore %8 %21
+               OpStore %12 %22
+               OpBranch %20
+         %23 = OpLabel
+         %25 = OpIAdd %6 %9 %9
+               OpStore %8 %25
+               OpBranch %70
+         %70 = OpLabel
+         %28 = OpFAdd %10 %13 %27
+               OpStore %12 %28
+               OpBranch %20
+         %20 = OpLabel
+         %52 = OpPhi %10 %22 %19 %28 %70
+         %48 = OpPhi %6 %21 %19 %25 %70
+               OpBranch %29
+         %29 = OpLabel
+         %51 = OpPhi %10 %52 %20 %100 %32
+         %47 = OpPhi %6 %48 %20 %101 %32
+               OpLoopMerge %31 %32 None
+               OpBranch %33
+         %33 = OpLabel
+         %36 = OpSLessThan %17 %47 %46
+               OpBranchConditional %36 %30 %31
+         %30 = OpLabel
+         %39 = OpIAdd %6 %47 %38
+               OpStore %8 %39
+               OpBranch %75
+         %75 = OpLabel
+         %42 = OpFAdd %10 %51 %41
+               OpStore %12 %42
+               OpBranch %32
+         %32 = OpLabel
+        %100 = OpPhi %10 %42 %75
+        %101 = OpPhi %6 %39 %75
+               OpBranch %29
+         %31 = OpLabel
+         %71 = OpPhi %6 %47 %33
+         %72 = OpPhi %10 %51 %33
+               OpStore %15 %71
+         %45 = OpFAdd %10 %72 %13
+               OpStore %12 %45
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+
+  std::vector<uint32_t> bad = {5, 19, 20, 23, 31, 32, 33, 70};
+
+  std::vector<uint32_t> good = {29, 30, 75};
+
+  for (uint32_t from_block : bad) {
+    ASSERT_FALSE(TransformationAddDeadContinue(from_block, true, {})
+                     .IsApplicable(context.get(), fact_manager));
+  }
+  auto transformation1 = TransformationAddDeadContinue(29, true, {13, 21});
+  ASSERT_TRUE(transformation1.IsApplicable(context.get(), fact_manager));
+  transformation1.Apply(context.get(), &fact_manager);
+
+  auto transformation2 = TransformationAddDeadContinue(30, true, {22, 46});
+  ASSERT_TRUE(transformation2.IsApplicable(context.get(), fact_manager));
+  transformation2.Apply(context.get(), &fact_manager);
+
+  // 75 already has the continue block as a successor, so we should not provide
+  // phi ids.
+  auto transformationBad = TransformationAddDeadContinue(75, true, {27, 46});
+  ASSERT_FALSE(transformationBad.IsApplicable(context.get(), fact_manager));
+
+  auto transformation3 = TransformationAddDeadContinue(75, true, {});
+  ASSERT_TRUE(transformation3.IsApplicable(context.get(), fact_manager));
+  transformation3.Apply(context.get(), &fact_manager);
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %4 "main"
+               OpName %8 "x"
+               OpName %12 "f"
+               OpName %15 "y"
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %6 = OpTypeInt 32 1
+          %7 = OpTypePointer Function %6
+          %9 = OpConstant %6 2
+         %10 = OpTypeFloat 32
+         %11 = OpTypePointer Function %10
+         %13 = OpConstant %10 3
+         %17 = OpTypeBool
+         %80 = OpConstantTrue %17
+         %21 = OpConstant %6 3
+         %22 = OpConstant %10 4
+         %27 = OpConstant %10 10
+         %38 = OpConstant %6 1
+         %41 = OpConstant %10 1
+         %46 = OpUndef %6
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+          %8 = OpVariable %7 Function
+         %12 = OpVariable %11 Function
+         %15 = OpVariable %7 Function
+               OpStore %8 %9
+               OpStore %12 %13
+         %18 = OpSGreaterThan %17 %9 %46
+               OpSelectionMerge %20 None
+               OpBranchConditional %18 %19 %23
+         %19 = OpLabel
+               OpStore %8 %21
+               OpStore %12 %22
+               OpBranch %20
+         %23 = OpLabel
+         %25 = OpIAdd %6 %9 %9
+               OpStore %8 %25
+               OpBranch %70
+         %70 = OpLabel
+         %28 = OpFAdd %10 %13 %27
+               OpStore %12 %28
+               OpBranch %20
+         %20 = OpLabel
+         %52 = OpPhi %10 %22 %19 %28 %70
+         %48 = OpPhi %6 %21 %19 %25 %70
+               OpBranch %29
+         %29 = OpLabel
+         %51 = OpPhi %10 %52 %20 %100 %32
+         %47 = OpPhi %6 %48 %20 %101 %32
+               OpLoopMerge %31 %32 None
+               OpBranchConditional %80 %33 %32
+         %33 = OpLabel
+         %36 = OpSLessThan %17 %47 %46
+               OpBranchConditional %36 %30 %31
+         %30 = OpLabel
+         %39 = OpIAdd %6 %47 %38
+               OpStore %8 %39
+               OpBranchConditional %80 %75 %32
+         %75 = OpLabel
+         %42 = OpFAdd %10 %51 %41
+               OpStore %12 %42
+               OpBranchConditional %80 %32 %32
+         %32 = OpLabel
+        %100 = OpPhi %10 %42 %75 %13 %29 %22 %30
+        %101 = OpPhi %6 %39 %75 %21 %29 %46 %30
+               OpBranch %29
+         %31 = OpLabel
+         %71 = OpPhi %6 %47 %33
+         %72 = OpPhi %10 %51 %33
+               OpStore %15 %71
+         %45 = OpFAdd %10 %72 %13
+               OpStore %12 %45
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools