spirv-fuzz: Transformation to convert OpSelect to conditional branch (#3681)

This transformation takes an OpSelect instruction and replaces it with
a conditional branch, selecting the correct value using an OpPhi
instruction.

Fixes part of the issue #3544.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index 288ea25..523f578 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -95,6 +95,7 @@
         fuzzer_pass_replace_linear_algebra_instructions.h
         fuzzer_pass_replace_loads_stores_with_copy_memories.h
         fuzzer_pass_replace_opphi_ids_from_dead_predecessors.h
+        fuzzer_pass_replace_opselects_with_conditional_branches.h
         fuzzer_pass_replace_parameter_with_global.h
         fuzzer_pass_replace_params_with_struct.h
         fuzzer_pass_split_blocks.h
@@ -174,6 +175,7 @@
         transformation_replace_linear_algebra_instruction.h
         transformation_replace_load_store_with_copy_memory.h
         transformation_replace_opphi_id_from_dead_predecessor.h
+        transformation_replace_opselect_with_conditional_branch.h
         transformation_replace_parameter_with_global.h
         transformation_replace_params_with_struct.h
         transformation_set_function_control.h
@@ -254,6 +256,7 @@
         fuzzer_pass_replace_linear_algebra_instructions.cpp
         fuzzer_pass_replace_loads_stores_with_copy_memories.cpp
         fuzzer_pass_replace_opphi_ids_from_dead_predecessors.cpp
+        fuzzer_pass_replace_opselects_with_conditional_branches.cpp
         fuzzer_pass_replace_parameter_with_global.cpp
         fuzzer_pass_replace_params_with_struct.cpp
         fuzzer_pass_split_blocks.cpp
@@ -306,6 +309,7 @@
         transformation_composite_insert.cpp
         transformation_compute_data_synonym_fact_closure.cpp
         transformation_context.cpp
+        transformation_replace_opselect_with_conditional_branch.cpp
         transformation_equation_instruction.cpp
         transformation_function_call.cpp
         transformation_inline_function.cpp
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index 3656e92..ee638b6 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -71,6 +71,7 @@
 #include "source/fuzz/fuzzer_pass_replace_linear_algebra_instructions.h"
 #include "source/fuzz/fuzzer_pass_replace_loads_stores_with_copy_memories.h"
 #include "source/fuzz/fuzzer_pass_replace_opphi_ids_from_dead_predecessors.h"
+#include "source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h"
 #include "source/fuzz/fuzzer_pass_replace_parameter_with_global.h"
 #include "source/fuzz/fuzzer_pass_replace_params_with_struct.h"
 #include "source/fuzz/fuzzer_pass_split_blocks.h"
@@ -325,6 +326,9 @@
     MaybeAddPass<FuzzerPassReplaceLinearAlgebraInstructions>(
         &passes, ir_context.get(), &transformation_context, &fuzzer_context,
         transformation_sequence_out);
+    MaybeAddPass<FuzzerPassReplaceOpSelectsWithConditionalBranches>(
+        &passes, ir_context.get(), &transformation_context, &fuzzer_context,
+        transformation_sequence_out);
     MaybeAddPass<FuzzerPassReplaceParamsWithStruct>(
         &passes, ir_context.get(), &transformation_context, &fuzzer_context,
         transformation_sequence_out);
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 153b9ef..3443a14 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -27,6 +27,8 @@
 const std::pair<uint32_t, uint32_t> kChanceOfAddingAnotherStructField = {20,
                                                                          90};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingArrayOrStructType = {20, 90};
+const std::pair<uint32_t, uint32_t>
+    kChanceOfAddingBothBranchesWhenReplacingOpSelect = {40, 60};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingCompositeInsert = {20, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingCopyMemory = {20, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingDeadBlock = {20, 90};
@@ -48,6 +50,8 @@
 const std::pair<uint32_t, uint32_t> kChanceOfAddingRelaxedDecoration = {20, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingStore = {5, 50};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingSynonyms = {20, 50};
+const std::pair<uint32_t, uint32_t>
+    kChanceOfAddingTrueBranchWhenReplacingOpSelect = {40, 60};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingVectorType = {20, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfAddingVectorShuffle = {20, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfAdjustingBranchWeights = {20, 90};
@@ -105,6 +109,8 @@
     {20, 90};
 const std::pair<uint32_t, uint32_t>
     kChanceOfReplacingOpPhiIdFromDeadPredecessor = {20, 90};
+const std::pair<uint32_t, uint32_t>
+    kChanceOfReplacingOpSelectWithConditionalBranch = {20, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingParametersWithGlobals = {
     30, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingParametersWithStruct = {
@@ -162,6 +168,8 @@
       ChooseBetweenMinAndMax(kChanceOfAddingAnotherStructField);
   chance_of_adding_array_or_struct_type_ =
       ChooseBetweenMinAndMax(kChanceOfAddingArrayOrStructType);
+  chance_of_adding_both_branches_when_replacing_opselect_ =
+      ChooseBetweenMinAndMax(kChanceOfAddingBothBranchesWhenReplacingOpSelect);
   chance_of_adding_composite_insert_ =
       ChooseBetweenMinAndMax(kChanceOfAddingCompositeInsert);
   chance_of_adding_copy_memory_ =
@@ -194,6 +202,8 @@
   chance_of_adding_relaxed_decoration_ =
       ChooseBetweenMinAndMax(kChanceOfAddingRelaxedDecoration);
   chance_of_adding_store_ = ChooseBetweenMinAndMax(kChanceOfAddingStore);
+  chance_of_adding_true_branch_when_replacing_opselect_ =
+      ChooseBetweenMinAndMax(kChanceOfAddingTrueBranchWhenReplacingOpSelect);
   chance_of_adding_vector_shuffle_ =
       ChooseBetweenMinAndMax(kChanceOfAddingVectorShuffle);
   chance_of_adding_vector_type_ =
@@ -271,6 +281,8 @@
       ChooseBetweenMinAndMax(kChanceOfReplacingLoadStoreWithCopyMemory);
   chance_of_replacing_opphi_id_from_dead_predecessor_ =
       ChooseBetweenMinAndMax(kChanceOfReplacingOpPhiIdFromDeadPredecessor);
+  chance_of_replacing_opselect_with_conditional_branch_ =
+      ChooseBetweenMinAndMax(kChanceOfReplacingOpSelectWithConditionalBranch);
   chance_of_replacing_parameters_with_globals_ =
       ChooseBetweenMinAndMax(kChanceOfReplacingParametersWithGlobals);
   chance_of_replacing_parameters_with_struct_ =
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index 594cb90..7427a41 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -115,6 +115,9 @@
   uint32_t GetChanceOfAddingArrayOrStructType() {
     return chance_of_adding_array_or_struct_type_;
   }
+  uint32_t GetChanceOfAddingBothBranchesWhenReplacingOpSelect() {
+    return chance_of_adding_both_branches_when_replacing_opselect_;
+  }
   uint32_t GetChanceOfAddingCompositeInsert() {
     return chance_of_adding_composite_insert_;
   }
@@ -157,6 +160,9 @@
   }
   uint32_t GetChanceOfAddingStore() { return chance_of_adding_store_; }
   uint32_t GetChanceOfAddingSynonyms() { return chance_of_adding_synonyms_; }
+  uint32_t GetChanceOfAddingTrueBranchWhenReplacingOpSelect() {
+    return chance_of_adding_true_branch_when_replacing_opselect_;
+  }
   uint32_t GetChanceOfAddingVectorShuffle() {
     return chance_of_adding_vector_shuffle_;
   }
@@ -264,6 +270,9 @@
   uint32_t GetChanceOfReplacingOpPhiIdFromDeadPredecessor() {
     return chance_of_replacing_opphi_id_from_dead_predecessor_;
   }
+  uint32_t GetChanceOfReplacingOpselectWithConditionalBranch() {
+    return chance_of_replacing_opselect_with_conditional_branch_;
+  }
   uint32_t GetChanceOfReplacingParametersWithGlobals() {
     return chance_of_replacing_parameters_with_globals_;
   }
@@ -364,6 +373,7 @@
   uint32_t chance_of_adding_access_chain_;
   uint32_t chance_of_adding_another_struct_field_;
   uint32_t chance_of_adding_array_or_struct_type_;
+  uint32_t chance_of_adding_both_branches_when_replacing_opselect_;
   uint32_t chance_of_adding_composite_insert_;
   uint32_t chance_of_adding_copy_memory_;
   uint32_t chance_of_adding_dead_block_;
@@ -382,6 +392,7 @@
   uint32_t chance_of_adding_relaxed_decoration_;
   uint32_t chance_of_adding_store_;
   uint32_t chance_of_adding_synonyms_;
+  uint32_t chance_of_adding_true_branch_when_replacing_opselect_;
   uint32_t chance_of_adding_vector_shuffle_;
   uint32_t chance_of_adding_vector_type_;
   uint32_t chance_of_adjusting_branch_weights_;
@@ -421,6 +432,7 @@
   uint32_t chance_of_replacing_linear_algebra_instructions_;
   uint32_t chance_of_replacing_load_store_with_copy_memory_;
   uint32_t chance_of_replacing_opphi_id_from_dead_predecessor_;
+  uint32_t chance_of_replacing_opselect_with_conditional_branch_;
   uint32_t chance_of_replacing_parameters_with_globals_;
   uint32_t chance_of_replacing_parameters_with_struct_;
   uint32_t chance_of_splitting_block_;
diff --git a/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp
new file mode 100644
index 0000000..0496268
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.cpp
@@ -0,0 +1,162 @@
+// 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_opselects_with_conditional_branches.h"
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/instruction_descriptor.h"
+#include "source/fuzz/transformation_replace_opselect_with_conditional_branch.h"
+#include "source/fuzz/transformation_split_block.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassReplaceOpSelectsWithConditionalBranches::
+    FuzzerPassReplaceOpSelectsWithConditionalBranches(
+        opt::IRContext* ir_context,
+        TransformationContext* transformation_context,
+        FuzzerContext* fuzzer_context,
+        protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, transformation_context, fuzzer_context,
+                 transformations) {}
+
+FuzzerPassReplaceOpSelectsWithConditionalBranches::
+    ~FuzzerPassReplaceOpSelectsWithConditionalBranches() = default;
+
+void FuzzerPassReplaceOpSelectsWithConditionalBranches::Apply() {
+  // Keep track of the instructions that we want to replace. We need to collect
+  // them in a vector, since it's not safe to modify the module while iterating
+  // over it.
+  std::vector<uint32_t> replaceable_opselect_instruction_ids;
+
+  // Loop over all the instructions in the module.
+  for (auto& function : *GetIRContext()->module()) {
+    for (auto& block : function) {
+      // We cannot split loop headers, so we don't need to consider instructions
+      // in loop headers that are also merge blocks (since they would need to be
+      // split).
+      if (block.IsLoopHeader() &&
+          GetIRContext()->GetStructuredCFGAnalysis()->IsMergeBlock(
+              block.id())) {
+        continue;
+      }
+
+      for (auto& instruction : block) {
+        // We only care about OpSelect instructions.
+        if (instruction.opcode() != SpvOpSelect) {
+          continue;
+        }
+
+        // Randomly choose whether to consider this instruction for replacement.
+        if (!GetFuzzerContext()->ChoosePercentage(
+                GetFuzzerContext()
+                    ->GetChanceOfReplacingOpselectWithConditionalBranch())) {
+          continue;
+        }
+
+        // If the block is a loop header and we need to split it, the
+        // transformation cannot be applied because loop headers cannot be
+        // split. We can break out of this loop because the transformation can
+        // only be applied to at most the first instruction in a loop header.
+        if (block.IsLoopHeader() && InstructionNeedsSplitBefore(&instruction)) {
+          break;
+        }
+
+        // If the instruction separates an OpSampledImage from its use, the
+        // block cannot be split around it and the instruction cannot be
+        // replaced.
+        if (fuzzerutil::
+                SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse(
+                    &block, &instruction)) {
+          continue;
+        }
+
+        // We can apply the transformation to this instruction.
+        replaceable_opselect_instruction_ids.push_back(instruction.result_id());
+      }
+    }
+  }
+
+  // Apply the transformations, splitting the blocks containing the
+  // instructions, if necessary.
+  for (uint32_t instruction_id : replaceable_opselect_instruction_ids) {
+    auto instruction =
+        GetIRContext()->get_def_use_mgr()->GetDef(instruction_id);
+
+    // If the instruction requires the block containing it to be split before
+    // it, split the block.
+    if (InstructionNeedsSplitBefore(instruction)) {
+      ApplyTransformation(TransformationSplitBlock(
+          MakeInstructionDescriptor(GetIRContext(), instruction),
+          GetFuzzerContext()->GetFreshId()));
+    }
+
+    // Decide whether to have two branches or just one.
+    bool two_branches = GetFuzzerContext()->ChoosePercentage(
+        GetFuzzerContext()
+            ->GetChanceOfAddingBothBranchesWhenReplacingOpSelect());
+
+    // If there will be only one branch, decide whether it will be the true
+    // branch or the false branch.
+    bool true_branch_id_zero =
+        !two_branches &&
+        GetFuzzerContext()->ChoosePercentage(
+            GetFuzzerContext()
+                ->GetChanceOfAddingTrueBranchWhenReplacingOpSelect());
+    bool false_branch_id_zero = !two_branches && !true_branch_id_zero;
+
+    uint32_t true_branch_id =
+        true_branch_id_zero ? 0 : GetFuzzerContext()->GetFreshId();
+    uint32_t false_branch_id =
+        false_branch_id_zero ? 0 : GetFuzzerContext()->GetFreshId();
+
+    ApplyTransformation(TransformationReplaceOpSelectWithConditionalBranch(
+        instruction_id, true_branch_id, false_branch_id));
+  }
+}
+
+bool FuzzerPassReplaceOpSelectsWithConditionalBranches::
+    InstructionNeedsSplitBefore(opt::Instruction* instruction) {
+  assert(instruction && instruction->opcode() == SpvOpSelect &&
+         "The instruction must be OpSelect.");
+
+  auto block = GetIRContext()->get_instr_block(instruction);
+  assert(block && "The instruction must be contained in a block.");
+
+  // We need to split the block if the instruction is not the first in its
+  // block.
+  if (instruction->unique_id() != block->begin()->unique_id()) {
+    return true;
+  }
+
+  // We need to split the block if it is a merge block.
+  if (GetIRContext()->GetStructuredCFGAnalysis()->IsMergeBlock(block->id())) {
+    return true;
+  }
+
+  // We need to split the block if it has more than one predecessor.
+  if (GetIRContext()->cfg()->preds(block->id()).size() != 1) {
+    return true;
+  }
+
+  // We need to split the block if its predecessor is a header or it does not
+  // branch unconditionally to the block.
+  auto predecessor = GetIRContext()->get_instr_block(
+      GetIRContext()->cfg()->preds(block->id())[0]);
+  return predecessor->MergeBlockIdIfAny() ||
+         predecessor->terminator()->opcode() != SpvOpBranch;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h
new file mode 100644
index 0000000..ef3ec57
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h
@@ -0,0 +1,53 @@
+// 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_OPSELECTS_WITH_CONDITIONAL_BRANCHES_
+#define SOURCE_FUZZ_FUZZER_PASS_REPLACE_OPSELECTS_WITH_CONDITIONAL_BRANCHES_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// A fuzzer pass to replace OpSelect instructions (where the condition is a
+// scalar boolean) with conditional branches and OpPhi instructions.
+class FuzzerPassReplaceOpSelectsWithConditionalBranches : public FuzzerPass {
+ public:
+  FuzzerPassReplaceOpSelectsWithConditionalBranches(
+      opt::IRContext* ir_context, TransformationContext* transformation_context,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassReplaceOpSelectsWithConditionalBranches() override;
+
+  void Apply() override;
+
+ private:
+  // Returns true if any of the following holds:
+  // - the instruction is not the first in its block
+  // - the block containing it is a merge block
+  // - the block does not have a unique predecessor
+  // - the predecessor of the block is the header of a construct
+  // - the predecessor does not branch unconditionally to the block
+  // If this function returns true, the block must be split before the
+  // instruction for TransformationReplaceOpSelectWithConditionalBranch to be
+  // applicable.
+  // Assumes that the instruction is OpSelect.
+  bool InstructionNeedsSplitBefore(opt::Instruction* instruction);
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_REPLACE_OPSELECTS_WITH_CONDITIONAL_BRANCHES_
diff --git a/source/fuzz/fuzzer_util.cpp b/source/fuzz/fuzzer_util.cpp
index aa45d66..c6d18c0 100644
--- a/source/fuzz/fuzzer_util.cpp
+++ b/source/fuzz/fuzzer_util.cpp
@@ -1486,6 +1486,39 @@
   return builtin_count != 0;
 }
 
+bool SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse(
+    opt::BasicBlock* block_to_split, opt::Instruction* split_before) {
+  std::set<uint32_t> sampled_image_result_ids;
+  bool before_split = true;
+
+  // Check all the instructions in the block to split.
+  for (auto& instruction : *block_to_split) {
+    if (&instruction == &*split_before) {
+      before_split = false;
+    }
+    if (before_split) {
+      // If the instruction comes before the split and its opcode is
+      // OpSampledImage, record its result id.
+      if (instruction.opcode() == SpvOpSampledImage) {
+        sampled_image_result_ids.insert(instruction.result_id());
+      }
+    } else {
+      // If the instruction comes after the split, check if ids
+      // corresponding to OpSampledImage instructions defined before the split
+      // are used, and return true if they are.
+      if (!instruction.WhileEachInId(
+              [&sampled_image_result_ids](uint32_t* id) -> bool {
+                return !sampled_image_result_ids.count(*id);
+              })) {
+        return true;
+      }
+    }
+  }
+
+  // No usage that would be separated from the definition has been found.
+  return false;
+}
+
 }  // namespace fuzzerutil
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/fuzzer_util.h b/source/fuzz/fuzzer_util.h
index 865c1a0..2496c11 100644
--- a/source/fuzz/fuzzer_util.h
+++ b/source/fuzz/fuzzer_util.h
@@ -524,6 +524,11 @@
 bool MembersHaveBuiltInDecoration(opt::IRContext* ir_context,
                                   uint32_t struct_type_id);
 
+// Returns true iff splitting block |block_to_split| just before the instruction
+// |split_before| would separate an OpSampledImage instruction from its usage.
+bool SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse(
+    opt::BasicBlock* block_to_split, opt::Instruction* split_before);
+
 }  // namespace fuzzerutil
 }  // namespace fuzz
 }  // namespace spvtools
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index 0cc7a78..5d27761 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -418,6 +418,7 @@
     TransformationMutatePointer mutate_pointer = 71;
     TransformationReplaceIrrelevantId replace_irrelevant_id = 72;
     TransformationReplaceOpPhiIdFromDeadPredecessor replace_opphi_id_from_dead_predecessor = 73;
+    TransformationReplaceOpSelectWithConditionalBranch replace_opselect_with_conditional_branch = 74;
     // Add additional option using the next available number.
   }
 }
@@ -1564,6 +1565,39 @@
 
 }
 
+message TransformationReplaceOpSelectWithConditionalBranch {
+
+  // A transformation that takes an OpSelect instruction with a
+  // scalar boolean condition and replaces it with a conditional
+  // branch and an OpPhi instruction.
+  // The OpSelect instruction must be the first instruction in its
+  // block, which must have a unique predecessor. The block will
+  // become the merge block of a new construct, while its predecessor
+  // will become the header.
+  // Given the original OpSelect instruction:
+  //   %id = OpSelect %type %cond %then %else
+  // The branching instruction of the header will be:
+  //         OpBranchConditional %cond %true_block_id %false_block_id
+  // and the OpSelect instruction will be turned into:
+  //   %id = OpPhi %type %then %true_block_id %else %false_block_id
+  // At most one of |true_block_id| and |false_block_id| can be zero. In
+  // that case, there will be no such block and all references to it
+  // will be replaced by %merge_block (where %merge_block is the
+  // block containing the OpSelect instruction).
+
+  // The result id of the OpSelect instruction.
+  uint32 select_id = 1;
+
+  // A fresh id for the new block that the predecessor of the block
+  // containing |select_id| will branch to if the condition holds.
+  uint32 true_block_id = 2;
+
+  // A fresh id for the new block that the predecessor of the block
+  // containing |select_id| will branch to if the condition does not
+  // hold.
+  uint32 false_block_id = 3;
+}
+
 message TransformationReplaceParamsWithStruct {
 
   // Replaces parameters of the function with a struct containing
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index 2360901..7156f6e 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -78,6 +78,7 @@
 #include "source/fuzz/transformation_replace_linear_algebra_instruction.h"
 #include "source/fuzz/transformation_replace_load_store_with_copy_memory.h"
 #include "source/fuzz/transformation_replace_opphi_id_from_dead_predecessor.h"
+#include "source/fuzz/transformation_replace_opselect_with_conditional_branch.h"
 #include "source/fuzz/transformation_replace_parameter_with_global.h"
 #include "source/fuzz/transformation_replace_params_with_struct.h"
 #include "source/fuzz/transformation_set_function_control.h"
@@ -277,6 +278,10 @@
       return MakeUnique<TransformationReplaceLoadStoreWithCopyMemory>(
           message.replace_load_store_with_copy_memory());
     case protobufs::Transformation::TransformationCase::
+        kReplaceOpselectWithConditionalBranch:
+      return MakeUnique<TransformationReplaceOpSelectWithConditionalBranch>(
+          message.replace_opselect_with_conditional_branch());
+    case protobufs::Transformation::TransformationCase::
         kReplaceParameterWithGlobal:
       return MakeUnique<TransformationReplaceParameterWithGlobal>(
           message.replace_parameter_with_global());
diff --git a/source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp b/source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp
new file mode 100644
index 0000000..5ae56fd
--- /dev/null
+++ b/source/fuzz/transformation_replace_opselect_with_conditional_branch.cpp
@@ -0,0 +1,204 @@
+// 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_opselect_with_conditional_branch.h"
+
+#include "source/fuzz/fuzzer_util.h"
+
+namespace spvtools {
+namespace fuzz {
+TransformationReplaceOpSelectWithConditionalBranch::
+    TransformationReplaceOpSelectWithConditionalBranch(
+        const spvtools::fuzz::protobufs::
+            TransformationReplaceOpSelectWithConditionalBranch& message)
+    : message_(message) {}
+
+TransformationReplaceOpSelectWithConditionalBranch::
+    TransformationReplaceOpSelectWithConditionalBranch(
+        uint32_t select_id, uint32_t true_block_id, uint32_t false_block_id) {
+  message_.set_select_id(select_id);
+  message_.set_true_block_id(true_block_id);
+  message_.set_false_block_id(false_block_id);
+}
+
+bool TransformationReplaceOpSelectWithConditionalBranch::IsApplicable(
+    opt::IRContext* ir_context,
+    const TransformationContext& /* unused */) const {
+  assert((message_.true_block_id() || message_.false_block_id()) &&
+         "At least one of the ids must be non-zero.");
+
+  // Check that the non-zero ids are fresh.
+  std::set<uint32_t> used_ids;
+  for (uint32_t id : {message_.true_block_id(), message_.false_block_id()}) {
+    if (id && !CheckIdIsFreshAndNotUsedByThisTransformation(id, ir_context,
+                                                            &used_ids)) {
+      return false;
+    }
+  }
+
+  auto instruction =
+      ir_context->get_def_use_mgr()->GetDef(message_.select_id());
+
+  // The instruction must exist and it must be an OpSelect instruction.
+  if (!instruction || instruction->opcode() != SpvOpSelect) {
+    return false;
+  }
+
+  // Check that the condition is a scalar boolean.
+  auto condition = ir_context->get_def_use_mgr()->GetDef(
+      instruction->GetSingleWordInOperand(0));
+  assert(condition && "The condition should always exist in a valid module.");
+
+  auto condition_type =
+      ir_context->get_type_mgr()->GetType(condition->type_id());
+  if (!condition_type->AsBool()) {
+    return false;
+  }
+
+  auto block = ir_context->get_instr_block(instruction);
+  assert(block && "The block containing the instruction must be found");
+
+  // The instruction must be the first in its block.
+  if (instruction->unique_id() != block->begin()->unique_id()) {
+    return false;
+  }
+
+  // The block must not be a merge block.
+  if (ir_context->GetStructuredCFGAnalysis()->IsMergeBlock(block->id())) {
+    return false;
+  }
+
+  // The block must have exactly one predecessor.
+  auto predecessors = ir_context->cfg()->preds(block->id());
+  if (predecessors.size() != 1) {
+    return false;
+  }
+
+  uint32_t pred_id = predecessors[0];
+  auto predecessor = ir_context->get_instr_block(pred_id);
+
+  // The predecessor must not be the header of a construct and it must end with
+  // OpBranch.
+  if (predecessor->GetMergeInst() != nullptr ||
+      predecessor->terminator()->opcode() != SpvOpBranch) {
+    return false;
+  }
+
+  return true;
+}
+
+void TransformationReplaceOpSelectWithConditionalBranch::Apply(
+    opt::IRContext* ir_context, TransformationContext* /* unused */) const {
+  auto instruction =
+      ir_context->get_def_use_mgr()->GetDef(message_.select_id());
+
+  auto block = ir_context->get_instr_block(instruction);
+
+  auto predecessor =
+      ir_context->get_instr_block(ir_context->cfg()->preds(block->id())[0]);
+
+  // Create a new block for each non-zero id in {|message_.true_branch_id|,
+  // |message_.false_branch_id|}. Make each newly-created block branch
+  // unconditionally to the instruction block.
+  for (uint32_t id : {message_.true_block_id(), message_.false_block_id()}) {
+    if (id) {
+      fuzzerutil::UpdateModuleIdBound(ir_context, id);
+
+      // Create the new block.
+      auto new_block = MakeUnique<opt::BasicBlock>(MakeUnique<opt::Instruction>(
+          ir_context, SpvOpLabel, 0, id, opt::Instruction::OperandList{}));
+
+      // Add an unconditional branch from the new block to the instruction
+      // block.
+      new_block->AddInstruction(MakeUnique<opt::Instruction>(
+          ir_context, SpvOpBranch, 0, 0,
+          opt::Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {block->id()}}}));
+
+      // Insert the new block right after the predecessor of the instruction
+      // block.
+      block->GetParent()->InsertBasicBlockBefore(std::move(new_block), block);
+    }
+  }
+
+  // Delete the OpBranch instruction from the predecessor.
+  ir_context->KillInst(predecessor->terminator());
+
+  // Add an OpSelectionMerge instruction to the predecessor block, where the
+  // merge block is the instruction block.
+  predecessor->AddInstruction(MakeUnique<opt::Instruction>(
+      ir_context, SpvOpSelectionMerge, 0, 0,
+      opt::Instruction::OperandList{{SPV_OPERAND_TYPE_ID, {block->id()}},
+                                    {SPV_OPERAND_TYPE_SELECTION_CONTROL,
+                                     {SpvSelectionControlMaskNone}}}));
+
+  // |if_block| will be the true block, if it has been created, the instruction
+  // block otherwise.
+  uint32_t if_block =
+      message_.true_block_id() ? message_.true_block_id() : block->id();
+
+  // |else_block| will be the false block, if it has been created, the
+  // instruction block otherwise.
+  uint32_t else_block =
+      message_.false_block_id() ? message_.false_block_id() : block->id();
+
+  assert(if_block != else_block &&
+         "|if_block| and |else_block| should always be different, if the "
+         "transformation is applicable.");
+
+  // Add a conditional branching instruction to the predecessor, branching to
+  // |if_block| if the condition is true and to |if_false| otherwise.
+  predecessor->AddInstruction(MakeUnique<opt::Instruction>(
+      ir_context, SpvOpBranchConditional, 0, 0,
+      opt::Instruction::OperandList{
+          {SPV_OPERAND_TYPE_ID, {instruction->GetSingleWordInOperand(0)}},
+          {SPV_OPERAND_TYPE_ID, {if_block}},
+          {SPV_OPERAND_TYPE_ID, {else_block}}}));
+
+  // |if_pred| will be the true block, if it has been created, the existing
+  // predecessor otherwise.
+  uint32_t if_pred =
+      message_.true_block_id() ? message_.true_block_id() : predecessor->id();
+
+  // |else_pred| will be the false block, if it has been created, the existing
+  // predecessor otherwise.
+  uint32_t else_pred =
+      message_.false_block_id() ? message_.false_block_id() : predecessor->id();
+
+  // Replace the OpSelect instruction in the merge block with an OpPhi.
+  // This:          OpSelect %type %cond %if %else
+  // will become:   OpPhi %type %if %if_pred %else %else_pred
+  instruction->SetOpcode(SpvOpPhi);
+  std::vector<opt::Operand> operands;
+
+  operands.emplace_back(instruction->GetInOperand(1));
+  operands.emplace_back(opt::Operand{SPV_OPERAND_TYPE_ID, {if_pred}});
+
+  operands.emplace_back(instruction->GetInOperand(2));
+  operands.emplace_back(opt::Operand{SPV_OPERAND_TYPE_ID, {else_pred}});
+
+  instruction->SetInOperands(std::move(operands));
+
+  // Invalidate all analyses, since the structure of the module was changed.
+  ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+}
+
+protobufs::Transformation
+TransformationReplaceOpSelectWithConditionalBranch::ToMessage() const {
+  protobufs::Transformation result;
+  *result.mutable_replace_opselect_with_conditional_branch() = message_;
+  return result;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_replace_opselect_with_conditional_branch.h b/source/fuzz/transformation_replace_opselect_with_conditional_branch.h
new file mode 100644
index 0000000..612c646
--- /dev/null
+++ b/source/fuzz/transformation_replace_opselect_with_conditional_branch.h
@@ -0,0 +1,61 @@
+// 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_OPSELECT_WITH_CONDITIONAL_BRANCH_H
+#define SOURCE_FUZZ_TRANSFORMATION_REPLACE_OPSELECT_WITH_CONDITIONAL_BRANCH_H
+
+#include "source/fuzz/transformation.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationReplaceOpSelectWithConditionalBranch
+    : public Transformation {
+ public:
+  explicit TransformationReplaceOpSelectWithConditionalBranch(
+      const protobufs::TransformationReplaceOpSelectWithConditionalBranch&
+          message);
+
+  TransformationReplaceOpSelectWithConditionalBranch(uint32_t select_id,
+                                                     uint32_t true_block_id,
+                                                     uint32_t false_block_id);
+
+  // - |message_.select_id| is the result id of an OpSelect instruction.
+  // - The condition of the OpSelect must be a scalar boolean.
+  // - The OpSelect instruction is the first instruction in its block.
+  // - The block containing the instruction is not a merge block, and it has a
+  //   single predecessor, which is not a header and whose last instruction is
+  //   OpBranch.
+  // - Each of |message_.true_block_id| and |message_.false_block_id| is either
+  //   0 or a valid fresh id, and at most one of them is 0. They must be
+  //   distinct.
+  bool IsApplicable(
+      opt::IRContext* ir_context,
+      const TransformationContext& transformation_context) const override;
+
+  // Replaces the OpSelect instruction with id |message_.select_id| with a
+  // conditional branch and an OpPhi instruction.
+  void Apply(opt::IRContext* ir_context,
+             TransformationContext* transformation_context) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+ private:
+  protobufs::TransformationReplaceOpSelectWithConditionalBranch message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_REPLACE_OPSELECT_WITH_CONDITIONAL_BRANCH_H
diff --git a/source/fuzz/transformation_split_block.cpp b/source/fuzz/transformation_split_block.cpp
index 3c437e4..5e2baba 100644
--- a/source/fuzz/transformation_split_block.cpp
+++ b/source/fuzz/transformation_split_block.cpp
@@ -83,27 +83,9 @@
 
   // Splitting the block must not separate the definition of an OpSampledImage
   // from its use: the SPIR-V data rules require them to be in the same block.
-  std::set<uint32_t> sampled_image_result_ids;
-  bool before_split = true;
-  for (auto& instruction : *block_to_split) {
-    if (&instruction == &*split_before) {
-      before_split = false;
-    }
-    if (before_split) {
-      if (instruction.opcode() == SpvOpSampledImage) {
-        sampled_image_result_ids.insert(instruction.result_id());
-      }
-    } else {
-      if (!instruction.WhileEachInId(
-              [&sampled_image_result_ids](uint32_t* id) -> bool {
-                return !sampled_image_result_ids.count(*id);
-              })) {
-        return false;
-      }
-    }
-  }
-
-  return true;
+  return !fuzzerutil::
+      SplittingBeforeInstructionSeparatesOpSampledImageDefinitionFromUse(
+          block_to_split, instruction_to_split_before);
 }
 
 void TransformationSplitBlock::Apply(
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index 34317d0..6199e69 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -87,6 +87,7 @@
           transformation_replace_linear_algebra_instruction_test.cpp
           transformation_replace_load_store_with_copy_memory_test.cpp
           transformation_replace_opphi_id_from_dead_predecessor_test.cpp
+          transformation_replace_opselect_with_conditional_branch_test.cpp
           transformation_replace_parameter_with_global_test.cpp
           transformation_replace_params_with_struct_test.cpp
           transformation_set_function_control_test.cpp
diff --git a/test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp b/test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp
new file mode 100644
index 0000000..b6a5138
--- /dev/null
+++ b/test/fuzz/transformation_replace_opselect_with_conditional_branch_test.cpp
@@ -0,0 +1,278 @@
+// 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_opselect_with_conditional_branch.h"
+
+#include "test/fuzz/fuzz_test_util.h"
+
+#include "source/fuzz/fuzzer_pass_replace_opselects_with_conditional_branches.h"
+#include "source/fuzz/pseudo_random_generator.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationReplaceOpSelectWithConditionalBranchTest, Inapplicable) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %2 "main"
+               OpExecutionMode %2 OriginUpperLeft
+               OpSource ESSL 310
+          %3 = OpTypeVoid
+          %4 = OpTypeFunction %3
+          %5 = OpTypeInt 32 1
+          %6 = OpConstant %5 1
+          %7 = OpConstant %5 2
+          %8 = OpTypeVector %5 4
+          %9 = OpConstantNull %8
+         %10 = OpConstantComposite %8 %6 %6 %7 %7
+         %11 = OpTypeBool
+         %12 = OpTypeVector %11 4
+         %13 = OpConstantTrue %11
+         %14 = OpConstantFalse %11
+         %15 = OpConstantComposite %12 %13 %14 %14 %13
+          %2 = OpFunction %3 None %4
+         %16 = OpLabel
+         %17 = OpCopyObject %5 %6
+         %18 = OpCopyObject %5 %7
+               OpBranch %19
+         %19 = OpLabel
+         %20 = OpCopyObject %5 %17
+         %21 = OpSelect %5 %13 %17 %18
+               OpBranch %22
+         %22 = OpLabel
+         %23 = OpSelect %8 %15 %9 %10
+               OpBranch %24
+         %24 = OpLabel
+               OpSelectionMerge %25 None
+               OpBranchConditional %13 %26 %27
+         %26 = OpLabel
+         %28 = OpSelect %5 %13 %17 %18
+               OpBranch %27
+         %27 = OpLabel
+         %29 = OpSelect %5 %13 %17 %18
+               OpBranch %25
+         %25 = OpLabel
+         %30 = OpSelect %5 %13 %17 %18
+               OpBranch %31
+         %31 = OpLabel
+               OpLoopMerge %32 %33 None
+               OpBranch %33
+         %33 = OpLabel
+         %34 = OpSelect %5 %13 %17 %18
+               OpBranchConditional %13 %31 %32
+         %32 = OpLabel
+         %35 = OpSelect %5 %13 %17 %18
+               OpBranch %36
+         %36 = OpLabel
+         %37 = OpSelect %5 %13 %17 %18
+               OpReturn
+               OpFunctionEnd
+)";
+  const auto env = SPV_ENV_UNIVERSAL_1_5;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(&fact_manager,
+                                               validator_options);
+
+  // %20 is not an OpSelect instruction.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(20, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // %21 is not the first instruction in its block.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(21, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The condition for %23 is not a scalar, but a vector of booleans.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(23, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The predecessor (%24) of the block containing %28 is the header of a
+  // selection construct and does not branch unconditionally.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(24, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The block containing %29 has two predecessors (%24 and %26).
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(29, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The block containing %30 is the merge block for a selection construct.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(30, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The predecessor (%31) of the block containing %34 is a loop header.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(31, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // The block containing %35 is the merge block for a loop construct.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(35, 100, 101)
+                   .IsApplicable(context.get(), transformation_context));
+
+#ifndef NDEBUG
+  // |true_block_id| and |false_block_id| are both 0.
+  ASSERT_DEATH(
+      TransformationReplaceOpSelectWithConditionalBranch(37, 0, 0).IsApplicable(
+          context.get(), transformation_context),
+      "At least one of the ids must be non-zero.");
+#endif
+
+  // The fresh ids are not distinct.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(37, 100, 100)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // One of the ids is not fresh.
+  ASSERT_FALSE(TransformationReplaceOpSelectWithConditionalBranch(37, 100, 10)
+                   .IsApplicable(context.get(), transformation_context));
+}
+
+TEST(TransformationReplaceOpSelectWithConditionalBranchTest, Simple) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %2 "main"
+               OpExecutionMode %2 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %2 "main"
+          %3 = OpTypeVoid
+          %4 = OpTypeFunction %3
+          %5 = OpTypeInt 32 1
+          %6 = OpConstant %5 1
+          %7 = OpConstant %5 2
+          %8 = OpTypeVector %5 4
+          %9 = OpConstantNull %8
+         %10 = OpConstantComposite %8 %6 %6 %7 %7
+         %11 = OpTypeBool
+         %12 = OpTypeVector %11 4
+         %13 = OpConstantTrue %11
+         %14 = OpConstantFalse %11
+         %15 = OpConstantComposite %12 %13 %14 %14 %13
+          %2 = OpFunction %3 None %4
+         %16 = OpLabel
+         %17 = OpCopyObject %5 %6
+         %18 = OpCopyObject %5 %7
+               OpBranch %19
+         %19 = OpLabel
+         %20 = OpSelect %5 %13 %17 %18
+               OpSelectionMerge %21 None
+               OpBranchConditional %13 %22 %21
+         %22 = OpLabel
+               OpBranch %23
+         %23 = OpLabel
+         %24 = OpSelect %8 %13 %9 %10
+               OpBranch %21
+         %21 = OpLabel
+               OpBranch %25
+         %25 = OpLabel
+         %26 = OpSelect %5 %13 %17 %18
+               OpReturn
+               OpFunctionEnd
+)";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_5;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  FactManager fact_manager;
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(&fact_manager,
+                                               validator_options);
+
+  auto transformation =
+      TransformationReplaceOpSelectWithConditionalBranch(20, 100, 101);
+  ASSERT_TRUE(
+      transformation.IsApplicable(context.get(), transformation_context));
+  transformation.Apply(context.get(), &transformation_context);
+
+  auto transformation2 =
+      TransformationReplaceOpSelectWithConditionalBranch(24, 0, 102);
+  ASSERT_TRUE(
+      transformation2.IsApplicable(context.get(), transformation_context));
+  transformation2.Apply(context.get(), &transformation_context);
+
+  auto transformation3 =
+      TransformationReplaceOpSelectWithConditionalBranch(26, 103, 0);
+  ASSERT_TRUE(
+      transformation3.IsApplicable(context.get(), transformation_context));
+  transformation3.Apply(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 %2 "main"
+               OpExecutionMode %2 OriginUpperLeft
+               OpSource ESSL 310
+               OpName %2 "main"
+          %3 = OpTypeVoid
+          %4 = OpTypeFunction %3
+          %5 = OpTypeInt 32 1
+          %6 = OpConstant %5 1
+          %7 = OpConstant %5 2
+          %8 = OpTypeVector %5 4
+          %9 = OpConstantNull %8
+         %10 = OpConstantComposite %8 %6 %6 %7 %7
+         %11 = OpTypeBool
+         %12 = OpTypeVector %11 4
+         %13 = OpConstantTrue %11
+         %14 = OpConstantFalse %11
+         %15 = OpConstantComposite %12 %13 %14 %14 %13
+          %2 = OpFunction %3 None %4
+         %16 = OpLabel
+         %17 = OpCopyObject %5 %6
+         %18 = OpCopyObject %5 %7
+               OpSelectionMerge %19 None
+               OpBranchConditional %13 %100 %101
+        %100 = OpLabel
+               OpBranch %19
+        %101 = OpLabel
+               OpBranch %19
+         %19 = OpLabel
+         %20 = OpPhi %5 %17 %100 %18 %101
+               OpSelectionMerge %21 None
+               OpBranchConditional %13 %22 %21
+         %22 = OpLabel
+               OpSelectionMerge %23 None
+               OpBranchConditional %13 %23 %102
+        %102 = OpLabel
+               OpBranch %23
+         %23 = OpLabel
+         %24 = OpPhi %8 %9 %22 %10 %102
+               OpBranch %21
+         %21 = OpLabel
+               OpSelectionMerge %25 None
+               OpBranchConditional %13 %103 %25
+        %103 = OpLabel
+               OpBranch %25
+         %25 = OpLabel
+         %26 = OpPhi %5 %17 %103 %18 %21
+               OpReturn
+               OpFunctionEnd
+)";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools