spirv-fuzz: TransformationWrapRegionInSelection (#3674)

Fixes #3675.
diff --git a/source/fuzz/CMakeLists.txt b/source/fuzz/CMakeLists.txt
index cea05cf..61c9fb3 100644
--- a/source/fuzz/CMakeLists.txt
+++ b/source/fuzz/CMakeLists.txt
@@ -114,6 +114,7 @@
         fuzzer_pass_swap_commutable_operands.h
         fuzzer_pass_swap_conditional_branch_operands.h
         fuzzer_pass_toggle_access_chain_instruction.h
+        fuzzer_pass_wrap_regions_in_selections.h
         fuzzer_util.h
         id_use_descriptor.h
         instruction_descriptor.h
@@ -211,6 +212,7 @@
         transformation_swap_conditional_branch_operands.h
         transformation_toggle_access_chain_instruction.h
         transformation_vector_shuffle.h
+        transformation_wrap_region_in_selection.h
         uniform_buffer_element_descriptor.h
         ${CMAKE_CURRENT_BINARY_DIR}/protobufs/spvtoolsfuzz.pb.h
 
@@ -290,6 +292,7 @@
         fuzzer_pass_swap_commutable_operands.cpp
         fuzzer_pass_swap_conditional_branch_operands.cpp
         fuzzer_pass_toggle_access_chain_instruction.cpp
+        fuzzer_pass_wrap_regions_in_selections.cpp
         fuzzer_util.cpp
         id_use_descriptor.cpp
         instruction_descriptor.cpp
@@ -385,6 +388,7 @@
         transformation_swap_conditional_branch_operands.cpp
         transformation_toggle_access_chain_instruction.cpp
         transformation_vector_shuffle.cpp
+        transformation_wrap_region_in_selection.cpp
         uniform_buffer_element_descriptor.cpp
         ${CMAKE_CURRENT_BINARY_DIR}/protobufs/spvtoolsfuzz.pb.cc
         )
diff --git a/source/fuzz/fuzzer.cpp b/source/fuzz/fuzzer.cpp
index 4c196b6..9680dda 100644
--- a/source/fuzz/fuzzer.cpp
+++ b/source/fuzz/fuzzer.cpp
@@ -83,6 +83,7 @@
 #include "source/fuzz/fuzzer_pass_swap_commutable_operands.h"
 #include "source/fuzz/fuzzer_pass_swap_conditional_branch_operands.h"
 #include "source/fuzz/fuzzer_pass_toggle_access_chain_instruction.h"
+#include "source/fuzz/fuzzer_pass_wrap_regions_in_selections.h"
 #include "source/fuzz/pass_management/repeated_pass_manager.h"
 #include "source/fuzz/pass_management/repeated_pass_manager_looped_with_recommendations.h"
 #include "source/fuzz/pass_management/repeated_pass_manager_random_with_recommendations.h"
@@ -294,6 +295,7 @@
     MaybeAddRepeatedPass<FuzzerPassSplitBlocks>(&pass_instances);
     MaybeAddRepeatedPass<FuzzerPassSwapBranchConditionalOperands>(
         &pass_instances);
+    MaybeAddRepeatedPass<FuzzerPassWrapRegionsInSelections>(&pass_instances);
     // There is a theoretical possibility that no pass instances were created
     // until now; loop again if so.
   } while (pass_instances.GetPasses().empty());
diff --git a/source/fuzz/fuzzer_context.cpp b/source/fuzz/fuzzer_context.cpp
index 9e9e78d..5151117 100644
--- a/source/fuzz/fuzzer_context.cpp
+++ b/source/fuzz/fuzzer_context.cpp
@@ -134,6 +134,8 @@
     {10, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfTogglingAccessChainInstruction = {
     20, 90};
+const std::pair<uint32_t, uint32_t> kChanceOfWrappingRegionInSelection = {70,
+                                                                          90};
 
 // Default limits for various quantities that are chosen during fuzzing.
 // Keep them in alphabetical order.
@@ -320,6 +322,8 @@
       ChooseBetweenMinAndMax(kChanceOfSwappingConditionalBranchOperands);
   chance_of_toggling_access_chain_instruction_ =
       ChooseBetweenMinAndMax(kChanceOfTogglingAccessChainInstruction);
+  chance_of_wrapping_region_in_selection_ =
+      ChooseBetweenMinAndMax(kChanceOfWrappingRegionInSelection);
 }
 
 FuzzerContext::~FuzzerContext() = default;
diff --git a/source/fuzz/fuzzer_context.h b/source/fuzz/fuzzer_context.h
index b6f466f..213d51b 100644
--- a/source/fuzz/fuzzer_context.h
+++ b/source/fuzz/fuzzer_context.h
@@ -307,6 +307,9 @@
   uint32_t GetChanceOfTogglingAccessChainInstruction() {
     return chance_of_toggling_access_chain_instruction_;
   }
+  uint32_t GetChanceOfWrappingRegionInSelection() {
+    return chance_of_wrapping_region_in_selection_;
+  }
 
   // Other functions to control transformations. Keep them in alphabetical
   // order.
@@ -472,6 +475,7 @@
   uint32_t chance_of_splitting_block_;
   uint32_t chance_of_swapping_conditional_branch_operands_;
   uint32_t chance_of_toggling_access_chain_instruction_;
+  uint32_t chance_of_wrapping_region_in_selection_;
 
   // Limits associated with various quantities for which random values are
   // chosen during fuzzing.
diff --git a/source/fuzz/fuzzer_pass_wrap_regions_in_selections.cpp b/source/fuzz/fuzzer_pass_wrap_regions_in_selections.cpp
new file mode 100644
index 0000000..e6cdca4
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_wrap_regions_in_selections.cpp
@@ -0,0 +1,140 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/fuzzer_pass_wrap_regions_in_selections.h"
+
+#include "source/fuzz/fuzzer_context.h"
+#include "source/fuzz/instruction_descriptor.h"
+#include "source/fuzz/transformation_split_block.h"
+#include "source/fuzz/transformation_wrap_region_in_selection.h"
+
+namespace spvtools {
+namespace fuzz {
+
+FuzzerPassWrapRegionsInSelections::FuzzerPassWrapRegionsInSelections(
+    opt::IRContext* ir_context, TransformationContext* transformation_context,
+    FuzzerContext* fuzzer_context,
+    protobufs::TransformationSequence* transformations)
+    : FuzzerPass(ir_context, transformation_context, fuzzer_context,
+                 transformations) {}
+
+FuzzerPassWrapRegionsInSelections::~FuzzerPassWrapRegionsInSelections() =
+    default;
+
+void FuzzerPassWrapRegionsInSelections::Apply() {
+  for (auto& function : *GetIRContext()->module()) {
+    if (!GetFuzzerContext()->ChoosePercentage(
+            GetFuzzerContext()->GetChanceOfWrappingRegionInSelection())) {
+      continue;
+    }
+
+    // It is easier to select an element at random from a vector than from an
+    // instruction list.
+    std::vector<opt::BasicBlock*> header_block_candidates;
+    for (auto& block : function) {
+      header_block_candidates.push_back(&block);
+    }
+
+    if (header_block_candidates.empty()) {
+      continue;
+    }
+
+    // Try to get a header block candidate that will increase the chances of the
+    // transformation being applicable.
+    auto* header_block_candidate = MaybeGetHeaderBlockCandidate(
+        header_block_candidates[GetFuzzerContext()->RandomIndex(
+            header_block_candidates)]);
+    if (!header_block_candidate) {
+      continue;
+    }
+
+    std::vector<opt::BasicBlock*> merge_block_candidates;
+    for (auto& block : function) {
+      if (GetIRContext()->GetDominatorAnalysis(&function)->StrictlyDominates(
+              header_block_candidate, &block) &&
+          GetIRContext()
+              ->GetPostDominatorAnalysis(&function)
+              ->StrictlyDominates(&block, header_block_candidate)) {
+        merge_block_candidates.push_back(&block);
+      }
+    }
+
+    if (merge_block_candidates.empty()) {
+      continue;
+    }
+
+    // Try to get a merge block candidate that will increase the chances of the
+    // transformation being applicable.
+    auto* merge_block_candidate = MaybeGetMergeBlockCandidate(
+        merge_block_candidates[GetFuzzerContext()->RandomIndex(
+            merge_block_candidates)]);
+    if (!merge_block_candidate) {
+      continue;
+    }
+
+    if (!TransformationWrapRegionInSelection::IsApplicableToBlockRange(
+            GetIRContext(), header_block_candidate->id(),
+            merge_block_candidate->id())) {
+      continue;
+    }
+
+    // This boolean constant will be used as a condition for the
+    // OpBranchConditional instruction. We mark it as irrelevant to be able to
+    // replace it with a more interesting value later.
+    auto branch_condition = GetFuzzerContext()->ChooseEven();
+    FindOrCreateBoolConstant(branch_condition, true);
+
+    ApplyTransformation(TransformationWrapRegionInSelection(
+        header_block_candidate->id(), merge_block_candidate->id(),
+        branch_condition));
+  }
+}
+
+opt::BasicBlock*
+FuzzerPassWrapRegionsInSelections::MaybeGetHeaderBlockCandidate(
+    opt::BasicBlock* header_block_candidate) {
+  // Try to create a preheader if |header_block_candidate| is a loop header.
+  if (header_block_candidate->IsLoopHeader()) {
+    // GetOrCreateSimpleLoopPreheader only supports reachable blocks.
+    return GetIRContext()->cfg()->preds(header_block_candidate->id()).size() ==
+                   1
+               ? nullptr
+               : GetOrCreateSimpleLoopPreheader(header_block_candidate->id());
+  }
+
+  // Try to split |header_block_candidate| if it's already a header block.
+  if (header_block_candidate->GetMergeInst()) {
+    SplitBlockAfterOpPhiOrOpVariable(header_block_candidate->id());
+  }
+
+  return header_block_candidate;
+}
+
+opt::BasicBlock* FuzzerPassWrapRegionsInSelections::MaybeGetMergeBlockCandidate(
+    opt::BasicBlock* merge_block_candidate) {
+  // If |merge_block_candidate| is a merge block of some construct, try to split
+  // it and return a newly created block.
+  if (GetIRContext()->GetStructuredCFGAnalysis()->IsMergeBlock(
+          merge_block_candidate->id())) {
+    // We can't split a merge block if it's also a loop header.
+    return merge_block_candidate->IsLoopHeader()
+               ? nullptr
+               : SplitBlockAfterOpPhiOrOpVariable(merge_block_candidate->id());
+  }
+
+  return merge_block_candidate;
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/fuzzer_pass_wrap_regions_in_selections.h b/source/fuzz/fuzzer_pass_wrap_regions_in_selections.h
new file mode 100644
index 0000000..eb28d20
--- /dev/null
+++ b/source/fuzz/fuzzer_pass_wrap_regions_in_selections.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_FUZZER_PASS_WRAP_REGIONS_IN_SELECTIONS_H_
+#define SOURCE_FUZZ_FUZZER_PASS_WRAP_REGIONS_IN_SELECTIONS_H_
+
+#include "source/fuzz/fuzzer_pass.h"
+
+namespace spvtools {
+namespace fuzz {
+
+// Randomly wraps a region of blocks in every function into a selection
+// construct.
+class FuzzerPassWrapRegionsInSelections : public FuzzerPass {
+ public:
+  FuzzerPassWrapRegionsInSelections(
+      opt::IRContext* ir_context, TransformationContext* transformation_context,
+      FuzzerContext* fuzzer_context,
+      protobufs::TransformationSequence* transformations);
+
+  ~FuzzerPassWrapRegionsInSelections() override;
+
+  void Apply() override;
+
+ private:
+  // Tries to adjust |header_block_candidate| such that
+  // TransformationWrapRegionInSelection has higher chances of being
+  // applied. In particular, tries to split |header_block_candidate| if it's
+  // already a header block of some other construct.
+  opt::BasicBlock* MaybeGetHeaderBlockCandidate(
+      opt::BasicBlock* header_block_candidate);
+
+  // Tries to adjust |merge_block_candidate| such that
+  // TransformationWrapRegionInSelection has higher chances of being
+  // applied. In particular, tries to split |merge_block_candidate| if it's
+  // already a merge block of some other construct.
+  opt::BasicBlock* MaybeGetMergeBlockCandidate(
+      opt::BasicBlock* merge_block_candidate);
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_FUZZER_PASS_WRAP_REGIONS_IN_SELECTIONS_H_
diff --git a/source/fuzz/pass_management/repeated_pass_instances.h b/source/fuzz/pass_management/repeated_pass_instances.h
index 57820a2..ed55741 100644
--- a/source/fuzz/pass_management/repeated_pass_instances.h
+++ b/source/fuzz/pass_management/repeated_pass_instances.h
@@ -67,6 +67,7 @@
 #include "source/fuzz/fuzzer_pass_replace_params_with_struct.h"
 #include "source/fuzz/fuzzer_pass_split_blocks.h"
 #include "source/fuzz/fuzzer_pass_swap_conditional_branch_operands.h"
+#include "source/fuzz/fuzzer_pass_wrap_regions_in_selections.h"
 
 namespace spvtools {
 namespace fuzz {
@@ -156,6 +157,7 @@
   REPEATED_PASS_INSTANCE(ReplaceParamsWithStruct);
   REPEATED_PASS_INSTANCE(SplitBlocks);
   REPEATED_PASS_INSTANCE(SwapBranchConditionalOperands);
+  REPEATED_PASS_INSTANCE(WrapRegionsInSelections);
 #undef REPEATED_PASS_INSTANCE
 
  public:
diff --git a/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp b/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
index 8c03807..8597de2 100644
--- a/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
+++ b/source/fuzz/pass_management/repeated_pass_recommender_standard.cpp
@@ -120,7 +120,8 @@
     //   outlining functions.
     return RandomOrderAndNonNull(
         {pass_instances_->GetDuplicateRegionsWithSelections(),
-         pass_instances_->GetOutlineFunctions()});
+         pass_instances_->GetOutlineFunctions(),
+         pass_instances_->GetWrapRegionsInSelections()});
   }
   if (&pass == pass_instances_->GetAddLoopsToCreateIntConstantSynonyms()) {
     // - New synonyms can be applied
@@ -308,6 +309,16 @@
     // No obvious follow-on passes
     return {};
   }
+  if (&pass == pass_instances_->GetWrapRegionsInSelections()) {
+    // - This pass uses an irrelevant boolean constant - we can replace it with
+    //   something more interesting.
+    // - We can obfuscate that very constant as well.
+    // - We can flatten created selection construct.
+    return RandomOrderAndNonNull(
+        {pass_instances_->GetObfuscateConstants(),
+         pass_instances_->GetReplaceIrrelevantIds(),
+         pass_instances_->GetFlattenConditionalBranches()});
+  }
   assert(false && "Unreachable: every fuzzer pass should be dealt with.");
   return {};
 }
diff --git a/source/fuzz/protobufs/spvtoolsfuzz.proto b/source/fuzz/protobufs/spvtoolsfuzz.proto
index 3c8ab9b..de2f843 100644
--- a/source/fuzz/protobufs/spvtoolsfuzz.proto
+++ b/source/fuzz/protobufs/spvtoolsfuzz.proto
@@ -498,6 +498,7 @@
     TransformationFlattenConditionalBranch flatten_conditional_branch = 76;
     TransformationAddBitInstructionSynonym add_bit_instruction_synonym = 77;
     TransformationAddLoopToCreateIntConstantSynonym add_loop_to_create_int_constant_synonym = 78;
+    TransformationWrapRegionInSelection wrap_region_in_selection = 79;
     // Add additional option using the next available number.
   }
 }
@@ -2072,3 +2073,33 @@
   repeated uint32 component = 5;
 
 }
+
+message TransformationWrapRegionInSelection {
+
+  // Transforms a single-entry-single-exit region R into
+  // if (|branch_condition|) { R } else { R }
+  // The entry block for R becomes a selection header and
+  // the exit block - a selection merge.
+  //
+  // Note that the region R is not duplicated. Thus, the effect of
+  // this transformation can be represented as follows:
+  //              entry
+  //  entry        / \
+  //    |          \ /
+  //    R   -->     R
+  //    |           |
+  //   exit        exit
+
+  // This behaviour is different from TransformationDuplicateRegionWithSelection
+  // that copies the blocks in R.
+
+  // The entry block for the region R.
+  uint32 region_entry_block_id = 1;
+
+  // The exit block for the region R.
+  uint32 region_exit_block_id = 2;
+
+  // Boolean value for the condition expression.
+  bool branch_condition = 3;
+
+}
diff --git a/source/fuzz/transformation.cpp b/source/fuzz/transformation.cpp
index 7301a89..ea7c97c 100644
--- a/source/fuzz/transformation.cpp
+++ b/source/fuzz/transformation.cpp
@@ -95,6 +95,7 @@
 #include "source/fuzz/transformation_swap_conditional_branch_operands.h"
 #include "source/fuzz/transformation_toggle_access_chain_instruction.h"
 #include "source/fuzz/transformation_vector_shuffle.h"
+#include "source/fuzz/transformation_wrap_region_in_selection.h"
 #include "source/util/make_unique.h"
 
 namespace spvtools {
@@ -342,6 +343,9 @@
           message.toggle_access_chain_instruction());
     case protobufs::Transformation::TransformationCase::kVectorShuffle:
       return MakeUnique<TransformationVectorShuffle>(message.vector_shuffle());
+    case protobufs::Transformation::TransformationCase::kWrapRegionInSelection:
+      return MakeUnique<TransformationWrapRegionInSelection>(
+          message.wrap_region_in_selection());
     case protobufs::Transformation::TRANSFORMATION_NOT_SET:
       assert(false && "An unset transformation was encountered.");
       return nullptr;
diff --git a/source/fuzz/transformation_wrap_region_in_selection.cpp b/source/fuzz/transformation_wrap_region_in_selection.cpp
new file mode 100644
index 0000000..9924e36
--- /dev/null
+++ b/source/fuzz/transformation_wrap_region_in_selection.cpp
@@ -0,0 +1,166 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/transformation_wrap_region_in_selection.h"
+
+#include "source/fuzz/fuzzer_util.h"
+
+namespace spvtools {
+namespace fuzz {
+
+TransformationWrapRegionInSelection::TransformationWrapRegionInSelection(
+    const protobufs::TransformationWrapRegionInSelection& message)
+    : message_(message) {}
+
+TransformationWrapRegionInSelection::TransformationWrapRegionInSelection(
+    uint32_t region_entry_block_id, uint32_t region_exit_block_id,
+    bool branch_condition) {
+  message_.set_region_entry_block_id(region_entry_block_id);
+  message_.set_region_exit_block_id(region_exit_block_id);
+  message_.set_branch_condition(branch_condition);
+}
+
+bool TransformationWrapRegionInSelection::IsApplicable(
+    opt::IRContext* ir_context,
+    const TransformationContext& transformation_context) const {
+  // Check that it is possible to outline a region of blocks without breaking
+  // domination and structured control flow rules.
+  if (!IsApplicableToBlockRange(ir_context, message_.region_entry_block_id(),
+                                message_.region_exit_block_id())) {
+    return false;
+  }
+
+  // There must exist an irrelevant boolean constant to be used as a condition
+  // in the OpBranchConditional instruction.
+  return fuzzerutil::MaybeGetBoolConstant(ir_context, transformation_context,
+                                          message_.branch_condition(),
+                                          true) != 0;
+}
+
+void TransformationWrapRegionInSelection::Apply(
+    opt::IRContext* ir_context,
+    TransformationContext* transformation_context) const {
+  auto* new_header_block =
+      ir_context->cfg()->block(message_.region_entry_block_id());
+  assert(new_header_block->terminator()->opcode() == SpvOpBranch &&
+         "This condition should have been checked in the IsApplicable");
+
+  const auto successor_id =
+      new_header_block->terminator()->GetSingleWordInOperand(0);
+
+  // Change |entry_block|'s terminator to |OpBranchConditional|.
+  new_header_block->terminator()->SetOpcode(SpvOpBranchConditional);
+  new_header_block->terminator()->SetInOperands(
+      {{SPV_OPERAND_TYPE_ID,
+        {fuzzerutil::MaybeGetBoolConstant(ir_context, *transformation_context,
+                                          message_.branch_condition(), true)}},
+       {SPV_OPERAND_TYPE_ID, {successor_id}},
+       {SPV_OPERAND_TYPE_ID, {successor_id}}});
+
+  // Insert OpSelectionMerge before the terminator.
+  new_header_block->terminator()->InsertBefore(MakeUnique<opt::Instruction>(
+      ir_context, SpvOpSelectionMerge, 0, 0,
+      opt::Instruction::OperandList{
+          {SPV_OPERAND_TYPE_ID, {message_.region_exit_block_id()}},
+          {SPV_OPERAND_TYPE_SELECTION_CONTROL,
+           {SpvSelectionControlMaskNone}}}));
+
+  // We've change the module so we must invalidate analyses.
+  ir_context->InvalidateAnalysesExceptFor(opt::IRContext::kAnalysisNone);
+}
+
+protobufs::Transformation TransformationWrapRegionInSelection::ToMessage()
+    const {
+  protobufs::Transformation result;
+  *result.mutable_wrap_region_in_selection() = message_;
+  return result;
+}
+
+bool TransformationWrapRegionInSelection::IsApplicableToBlockRange(
+    opt::IRContext* ir_context, uint32_t header_block_candidate_id,
+    uint32_t merge_block_candidate_id) {
+  // Check that |header_block_candidate_id| and |merge_block_candidate_id| are
+  // valid.
+  const auto* header_block_candidate =
+      fuzzerutil::MaybeFindBlock(ir_context, header_block_candidate_id);
+  if (!header_block_candidate) {
+    return false;
+  }
+
+  const auto* merge_block_candidate =
+      fuzzerutil::MaybeFindBlock(ir_context, merge_block_candidate_id);
+  if (!merge_block_candidate) {
+    return false;
+  }
+
+  // |header_block_candidate| and |merge_block_candidate| must be from the same
+  // function.
+  if (header_block_candidate->GetParent() !=
+      merge_block_candidate->GetParent()) {
+    return false;
+  }
+
+  const auto* dominator_analysis =
+      ir_context->GetDominatorAnalysis(header_block_candidate->GetParent());
+  const auto* postdominator_analysis =
+      ir_context->GetPostDominatorAnalysis(header_block_candidate->GetParent());
+
+  if (!dominator_analysis->StrictlyDominates(header_block_candidate,
+                                             merge_block_candidate) ||
+      !postdominator_analysis->StrictlyDominates(merge_block_candidate,
+                                                 header_block_candidate)) {
+    return false;
+  }
+
+  // |header_block_candidate| can't be a header since we are about to make it
+  // one.
+  if (header_block_candidate->GetMergeInst()) {
+    return false;
+  }
+
+  // |header_block_candidate| must have an OpBranch terminator.
+  if (header_block_candidate->terminator()->opcode() != SpvOpBranch) {
+    return false;
+  }
+
+  // Every header block must have a unique merge block. Thus,
+  // |merge_block_candidate| can't be a merge block of some other header.
+  auto* structured_cfg = ir_context->GetStructuredCFGAnalysis();
+  if (structured_cfg->IsMergeBlock(merge_block_candidate_id)) {
+    return false;
+  }
+
+  // |header_block_candidate|'s containing construct must also contain
+  // |merge_block_candidate|.
+  //
+  // ContainingConstruct will return the id of a loop header for a block in the
+  // loop's continue construct. Thus, we must also check the case when one of
+  // the candidates is in continue construct and the other one is not.
+  if (structured_cfg->ContainingConstruct(header_block_candidate_id) !=
+          structured_cfg->ContainingConstruct(merge_block_candidate_id) ||
+      structured_cfg->IsInContinueConstruct(header_block_candidate_id) !=
+          structured_cfg->IsInContinueConstruct(merge_block_candidate_id)) {
+    return false;
+  }
+
+  return true;
+}
+
+std::unordered_set<uint32_t> TransformationWrapRegionInSelection::GetFreshIds()
+    const {
+  return std::unordered_set<uint32_t>();
+}
+
+}  // namespace fuzz
+}  // namespace spvtools
diff --git a/source/fuzz/transformation_wrap_region_in_selection.h b/source/fuzz/transformation_wrap_region_in_selection.h
new file mode 100644
index 0000000..57f4f64
--- /dev/null
+++ b/source/fuzz/transformation_wrap_region_in_selection.h
@@ -0,0 +1,88 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SOURCE_FUZZ_TRANSFORMATION_WRAP_REGION_IN_SELECTION_H_
+#define SOURCE_FUZZ_TRANSFORMATION_WRAP_REGION_IN_SELECTION_H_
+
+#include "source/fuzz/protobufs/spirvfuzz_protobufs.h"
+#include "source/fuzz/transformation.h"
+#include "source/fuzz/transformation_context.h"
+#include "source/opt/ir_context.h"
+
+namespace spvtools {
+namespace fuzz {
+
+class TransformationWrapRegionInSelection : public Transformation {
+ public:
+  explicit TransformationWrapRegionInSelection(
+      const protobufs::TransformationWrapRegionInSelection& message);
+
+  TransformationWrapRegionInSelection(uint32_t region_entry_block_id,
+                                      uint32_t region_exit_block_id,
+                                      bool branch_condition);
+
+  // - It should be possible to apply this transformation to a
+  //   single-exit-single-entry region of blocks dominated by
+  //   |region_entry_block_id| and postdominated by |region_exit_block_id|
+  //   (see IsApplicableToBlockRange method for further details).
+  //
+  //   TODO(https://github.com/KhronosGroup/SPIRV-Tools/issues/3828):
+  //    Consider applying this transformation to non-single-entry-single-exit
+  //    regions of blocks.
+  // - There must exist an irrelevant boolean constant with value
+  //   |branch_condition|.
+  bool IsApplicable(
+      opt::IRContext* ir_context,
+      const TransformationContext& transformation_context) const override;
+
+  // - Transforms |region_entry_block_id| into a selection header with both
+  //   branches pointing to the block's successor.
+  // - |branch_condition| is used as a condition in the header's
+  //   OpBranchConditional instruction.
+  // - Transforms |region_exit_block_id| into a merge block of the selection's
+  //   header.
+  void Apply(opt::IRContext* ir_context,
+             TransformationContext* transformation_context) const override;
+
+  protobufs::Transformation ToMessage() const override;
+
+  // Returns true if it's possible to apply this transformation to the
+  // single-exit-single-entry region of blocks starting with
+  // |header_block_candidate_id| and ending with |merge_block_candidate_id|.
+  // Concretely:
+  // - Both |header_block_candidate_id| and |merge_block_candidate_id| must be
+  //   result ids of some blocks in the module.
+  // - Both blocks must belong to the same function.
+  // - |header_block_candidate_id| must strictly dominate
+  //   |merge_block_candidate_id| and |merge_block_candidate_id| must strictly
+  //   postdominate |header_block_candidate_id|.
+  // - |header_block_candidate_id| can't be a header block of any construct.
+  // - |header_block_candidate_id|'s terminator must be an OpBranch.
+  // - |merge_block_candidate_id| can't be a merge block of any other construct.
+  // - Both |header_block_candidate_id| and |merge_block_candidate_id| must be
+  //   inside the same construct if any.
+  static bool IsApplicableToBlockRange(opt::IRContext* ir_context,
+                                       uint32_t header_block_candidate_id,
+                                       uint32_t merge_block_candidate_id);
+
+  std::unordered_set<uint32_t> GetFreshIds() const override;
+
+ private:
+  protobufs::TransformationWrapRegionInSelection message_;
+};
+
+}  // namespace fuzz
+}  // namespace spvtools
+
+#endif  // SOURCE_FUZZ_TRANSFORMATION_WRAP_REGION_IN_SELECTION_H_
diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt
index a918b24..924addb 100644
--- a/test/fuzz/CMakeLists.txt
+++ b/test/fuzz/CMakeLists.txt
@@ -107,6 +107,7 @@
           transformation_toggle_access_chain_instruction_test.cpp
           transformation_record_synonymous_constants_test.cpp
           transformation_vector_shuffle_test.cpp
+          transformation_wrap_region_in_selection_test.cpp
           uniform_buffer_element_descriptor_test.cpp)
 
   if (${SPIRV_ENABLE_LONG_FUZZER_TESTS})
diff --git a/test/fuzz/transformation_wrap_region_in_selection_test.cpp b/test/fuzz/transformation_wrap_region_in_selection_test.cpp
new file mode 100644
index 0000000..5881b3b
--- /dev/null
+++ b/test/fuzz/transformation_wrap_region_in_selection_test.cpp
@@ -0,0 +1,262 @@
+// Copyright (c) 2020 Vasyl Teliman
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "source/fuzz/transformation_wrap_region_in_selection.h"
+
+#include "source/fuzz/fuzzer_util.h"
+#include "source/fuzz/instruction_descriptor.h"
+#include "test/fuzz/fuzz_test_util.h"
+
+namespace spvtools {
+namespace fuzz {
+namespace {
+
+TEST(TransformationWrapRegionInSelectionTest, BasicTest) {
+  std::string shader = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %7 = OpTypeBool
+          %8 = OpConstantTrue %7
+
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpBranch %6
+
+          %6 = OpLabel
+               OpSelectionMerge %12 None
+               OpBranchConditional %8 %11 %12
+         %11 = OpLabel
+               OpReturn
+
+         %12 = OpLabel
+               OpSelectionMerge %15 None
+               OpBranchConditional %8 %13 %14
+         %13 = OpLabel
+               OpBranch %15
+         %14 = OpLabel
+               OpBranch %15
+         %15 = OpLabel
+               OpBranch %16
+
+         %16 = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+          %9 = OpFunction %2 None %3
+         %10 = OpLabel
+               OpBranch %20
+
+         %20 = OpLabel
+               OpLoopMerge %23 %22 None
+               OpBranch %21
+         %21 = OpLabel
+               OpBranchConditional %8 %24 %23
+         %24 = OpLabel
+               OpBranch %22
+
+         ; continue target
+         %22 = OpLabel
+               OpLoopMerge %25 %28 None
+               OpBranchConditional %8 %27 %25
+         %27 = OpLabel
+               OpBranch %28
+         %28 = OpLabel
+               OpBranch %22
+         %25 = OpLabel
+               OpBranch %20
+
+         ; merge block
+         %23 = OpLabel
+               OpBranch %26
+
+         %26 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  const auto env = SPV_ENV_UNIVERSAL_1_3;
+  const auto consumer = nullptr;
+  const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption);
+  ASSERT_TRUE(IsValid(env, context.get()));
+
+  spvtools::ValidatorOptions validator_options;
+  TransformationContext transformation_context(
+      MakeUnique<FactManager>(context.get()), validator_options);
+
+  // Boolean constant does not exist.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 6, false)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Irrelevant constant does not exist.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 6, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  transformation_context.GetFactManager()->AddFactIdIsIrrelevant(8);
+
+  // Block ids are invalid.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(100, 6, true)
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 100, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Blocks are from different functions.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 10, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Header block candidate does not dominate merge block candidate.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(13, 16, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Header block candidate does not *strictly* dominate merge block candidate.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 5, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Merge block candidate does not postdominate header block candidate.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 16, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Header block candidate is already a header block of some other construct.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(12, 16, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Header block's terminator is not an OpBranch.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(21, 24, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Merge block candidate is already a merge block of some other construct.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(5, 15, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  // Header block candidate and merge block candidate are in different
+  // constructs.
+  ASSERT_FALSE(TransformationWrapRegionInSelection(10, 21, true)
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationWrapRegionInSelection(24, 25, true)
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationWrapRegionInSelection(24, 22, true)
+                   .IsApplicable(context.get(), transformation_context));
+  ASSERT_FALSE(TransformationWrapRegionInSelection(24, 27, true)
+                   .IsApplicable(context.get(), transformation_context));
+
+  {
+    // Header block candidate can be a merge block of some existing construct.
+    TransformationWrapRegionInSelection transformation(15, 16, true);
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    // Merge block candidate can be a header block of some existing construct.
+    TransformationWrapRegionInSelection transformation(5, 6, true);
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+  {
+    // Wrap a loop construct.
+    TransformationWrapRegionInSelection transformation(10, 26, true);
+    ASSERT_TRUE(
+        transformation.IsApplicable(context.get(), transformation_context));
+    ApplyAndCheckFreshIds(transformation, context.get(),
+                          &transformation_context);
+    ASSERT_TRUE(IsValid(env, context.get()));
+  }
+
+  std::string after_transformation = R"(
+               OpCapability Shader
+          %1 = OpExtInstImport "GLSL.std.450"
+               OpMemoryModel Logical GLSL450
+               OpEntryPoint Fragment %4 "main"
+               OpExecutionMode %4 OriginUpperLeft
+               OpSource ESSL 310
+          %2 = OpTypeVoid
+          %3 = OpTypeFunction %2
+          %7 = OpTypeBool
+          %8 = OpConstantTrue %7
+
+          %4 = OpFunction %2 None %3
+          %5 = OpLabel
+               OpSelectionMerge %6 None
+               OpBranchConditional %8 %6 %6
+
+          %6 = OpLabel
+               OpSelectionMerge %12 None
+               OpBranchConditional %8 %11 %12
+         %11 = OpLabel
+               OpReturn
+
+         %12 = OpLabel
+               OpSelectionMerge %15 None
+               OpBranchConditional %8 %13 %14
+         %13 = OpLabel
+               OpBranch %15
+         %14 = OpLabel
+               OpBranch %15
+
+         %15 = OpLabel
+               OpSelectionMerge %16 None
+               OpBranchConditional %8 %16 %16
+         %16 = OpLabel
+               OpReturn
+               OpFunctionEnd
+
+          %9 = OpFunction %2 None %3
+         %10 = OpLabel
+               OpSelectionMerge %26 None
+               OpBranchConditional %8 %20 %20
+
+         %20 = OpLabel
+               OpLoopMerge %23 %22 None
+               OpBranch %21
+         %21 = OpLabel
+               OpBranchConditional %8 %24 %23
+         %24 = OpLabel
+               OpBranch %22
+
+         ; continue target
+         %22 = OpLabel
+               OpLoopMerge %25 %28 None
+               OpBranchConditional %8 %27 %25
+         %27 = OpLabel
+               OpBranch %28
+         %28 = OpLabel
+               OpBranch %22
+         %25 = OpLabel
+               OpBranch %20
+
+         ; merge block
+         %23 = OpLabel
+               OpBranch %26
+
+         %26 = OpLabel
+               OpReturn
+               OpFunctionEnd
+  )";
+
+  ASSERT_TRUE(IsEqual(env, after_transformation, context.get()));
+}
+
+}  // namespace
+}  // namespace fuzz
+}  // namespace spvtools