diff --git a/source/fuzz/fuzzer_util.cpp b/source/fuzz/fuzzer_util.cpp index 8a233927..15c7246c 100644 --- a/source/fuzz/fuzzer_util.cpp +++ b/source/fuzz/fuzzer_util.cpp @@ -25,6 +25,25 @@ namespace fuzz { namespace fuzzerutil { namespace { +// A utility class that uses RAII to change and restore the terminator +// instruction of the |block|. +class ChangeTerminatorRAII { + public: + explicit ChangeTerminatorRAII(opt::BasicBlock* block, + opt::Instruction new_terminator) + : block_(block), old_terminator_(std::move(*block->terminator())) { + *block_->terminator() = std::move(new_terminator); + } + + ~ChangeTerminatorRAII() { + *block_->terminator() = std::move(old_terminator_); + } + + private: + opt::BasicBlock* block_; + opt::Instruction old_terminator_; +}; + uint32_t MaybeGetOpConstant(opt::IRContext* ir_context, const TransformationContext& transformation_context, const std::vector& words, @@ -163,35 +182,46 @@ bool PhiIdsOkForNewEdge( return true; } -void AddUnreachableEdgeAndUpdateOpPhis( - opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to, - uint32_t bool_id, - const google::protobuf::RepeatedField& phi_ids) { - assert(PhiIdsOkForNewEdge(context, bb_from, bb_to, phi_ids) && - "Precondition on phi_ids is not satisfied"); +opt::Instruction CreateUnreachableEdgeInstruction(opt::IRContext* ir_context, + uint32_t bb_from_id, + uint32_t bb_to_id, + uint32_t bool_id) { + const auto* bb_from = MaybeFindBlock(ir_context, bb_from_id); + assert(bb_from && "|bb_from_id| is invalid"); + assert(MaybeFindBlock(ir_context, bb_to_id) && "|bb_to_id| is invalid"); 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. - auto condition_inst = context->get_def_use_mgr()->GetDef(bool_id); + auto condition_inst = ir_context->get_def_use_mgr()->GetDef(bool_id); assert(condition_inst && (condition_inst->opcode() == SpvOpConstantTrue || condition_inst->opcode() == SpvOpConstantFalse) && "|bool_id| is invalid"); auto condition_value = condition_inst->opcode() == SpvOpConstantTrue; - - const bool from_to_edge_already_exists = bb_from->IsSuccessor(bb_to); - auto successor = bb_from->terminator()->GetSingleWordInOperand(0); + auto successor_id = 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( + return opt::Instruction( + ir_context, SpvOpBranchConditional, 0, 0, {{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}}}); + {SPV_OPERAND_TYPE_ID, {condition_value ? successor_id : bb_to_id}}, + {SPV_OPERAND_TYPE_ID, {condition_value ? bb_to_id : successor_id}}}); +} + +void AddUnreachableEdgeAndUpdateOpPhis( + opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to, + uint32_t bool_id, + const google::protobuf::RepeatedField& phi_ids) { + assert(PhiIdsOkForNewEdge(context, bb_from, bb_to, phi_ids) && + "Precondition on phi_ids is not satisfied"); + + const bool from_to_edge_already_exists = bb_from->IsSuccessor(bb_to); + *bb_from->terminator() = CreateUnreachableEdgeInstruction( + context, bb_from->id(), bb_to->id(), bool_id); // Update OpPhi instructions in the target block if this branch adds a // previously non-existent edge from source to target. @@ -1856,6 +1886,104 @@ std::set GetReachableReturnBlocks(opt::IRContext* ir_context, return result; } +bool NewTerminatorPreservesDominationRules(opt::IRContext* ir_context, + uint32_t block_id, + opt::Instruction new_terminator) { + auto* mutated_block = MaybeFindBlock(ir_context, block_id); + assert(mutated_block && "|block_id| is invalid"); + + ChangeTerminatorRAII change_terminator_raii(mutated_block, + std::move(new_terminator)); + opt::DominatorAnalysis dominator_analysis; + dominator_analysis.InitializeTree(*ir_context->cfg(), + mutated_block->GetParent()); + + // Check that each dominator appears before each dominated block. + std::unordered_map positions; + for (const auto& block : *mutated_block->GetParent()) { + positions[block.id()] = positions.size(); + } + + std::queue q({mutated_block->GetParent()->begin()->id()}); + std::unordered_set visited; + while (!q.empty()) { + auto block = q.front(); + q.pop(); + visited.insert(block); + + auto success = ir_context->cfg()->block(block)->WhileEachSuccessorLabel( + [&positions, &visited, &dominator_analysis, block, &q](uint32_t id) { + if (id == block) { + // Handle the case when loop header and continue target are the same + // block. + return true; + } + + if (dominator_analysis.Dominates(block, id) && + positions[block] > positions[id]) { + // |block| dominates |id| but appears after |id| - violates + // domination rules. + return false; + } + + if (!visited.count(id)) { + q.push(id); + } + + return true; + }); + + if (!success) { + return false; + } + } + + // For each instruction in the |block->GetParent()| function check whether + // all its dependencies satisfy domination rules (i.e. all id operands + // dominate that instruction). + for (const auto& block : *mutated_block->GetParent()) { + if (!dominator_analysis.IsReachable(&block)) { + // If some block is not reachable then we don't need to worry about the + // preservation of domination rules for its instructions. + continue; + } + + for (const auto& inst : block) { + for (uint32_t i = 0; i < inst.NumInOperands(); + i += inst.opcode() == SpvOpPhi ? 2 : 1) { + const auto& operand = inst.GetInOperand(i); + if (!spvIsInIdType(operand.type)) { + continue; + } + + if (MaybeFindBlock(ir_context, operand.words[0])) { + // Ignore operands that refer to OpLabel instructions. + continue; + } + + const auto* dependency_block = + ir_context->get_instr_block(operand.words[0]); + if (!dependency_block) { + // A global instruction always dominates all instructions in any + // function. + continue; + } + + auto domination_target_id = inst.opcode() == SpvOpPhi + ? inst.GetSingleWordInOperand(i + 1) + : block.id(); + + if (!dominator_analysis.Dominates(dependency_block->id(), + domination_target_id)) { + return false; + } + } + } + } + + return true; +} + } // namespace fuzzerutil } // namespace fuzz } // namespace spvtools diff --git a/source/fuzz/fuzzer_util.h b/source/fuzz/fuzzer_util.h index 3bb1aa62..cc212bcb 100644 --- a/source/fuzz/fuzzer_util.h +++ b/source/fuzz/fuzzer_util.h @@ -68,6 +68,16 @@ bool PhiIdsOkForNewEdge( opt::IRContext* context, opt::BasicBlock* bb_from, opt::BasicBlock* bb_to, const google::protobuf::RepeatedField& phi_ids); +// Returns an OpBranchConditional instruction that will create an unreachable +// branch from |bb_from_id| to |bb_to_id|. |bool_id| must be a result id of +// either OpConstantTrue or OpConstantFalse. Based on the opcode of |bool_id|, +// operands of the returned instruction will be positioned in a way that the +// branch from |bb_from_id| to |bb_to_id| is always unreachable. +opt::Instruction CreateUnreachableEdgeInstruction(opt::IRContext* ir_context, + uint32_t bb_from_id, + uint32_t bb_to_id, + uint32_t bool_id); + // Requires that |bool_id| is a valid result id of either OpConstantTrue or // OpConstantFalse, that PhiIdsOkForNewEdge(context, bb_from, bb_to, phi_ids) // holds, and that bb_from ends with "OpBranch %some_block". Turns OpBranch @@ -597,6 +607,14 @@ bool InstructionHasNoSideEffects(const opt::Instruction& instruction); std::set GetReachableReturnBlocks(opt::IRContext* ir_context, uint32_t function_id); +// Returns true if changing terminator instruction to |new_terminator| in the +// basic block with id |block_id| preserves domination rules and valid block +// order (i.e. dominator must always appear before dominated in the CFG). +// Returns false otherwise. +bool NewTerminatorPreservesDominationRules(opt::IRContext* ir_context, + uint32_t block_id, + opt::Instruction new_terminator); + } // namespace fuzzerutil } // namespace fuzz } // namespace spvtools diff --git a/source/fuzz/transformation_add_dead_break.cpp b/source/fuzz/transformation_add_dead_break.cpp index bc938d40..56cd92bb 100644 --- a/source/fuzz/transformation_add_dead_break.cpp +++ b/source/fuzz/transformation_add_dead_break.cpp @@ -112,9 +112,10 @@ bool TransformationAddDeadBreak::IsApplicable( const TransformationContext& transformation_context) const { // First, we check that a constant with the same value as // |message_.break_condition_value| is present. - if (!fuzzerutil::MaybeGetBoolConstant(ir_context, transformation_context, - message_.break_condition_value(), - false)) { + const auto bool_id = + fuzzerutil::MaybeGetBoolConstant(ir_context, transformation_context, + message_.break_condition_value(), false); + if (!bool_id) { // The required constant is not present, so the transformation cannot be // applied. return false; @@ -171,25 +172,23 @@ bool TransformationAddDeadBreak::IsApplicable( } // Adding the dead break is only valid if SPIR-V rules related to dominance - // hold. Rather than checking these rules explicitly, we defer to the - // validator. We make a clone of the module, apply the transformation to the - // clone, and check whether the transformed clone is valid. - // - // In principle some of the above checks could be removed, with more reliance - // being places on the validator. This should be revisited if we are sure - // the validator is complete with respect to checking structured control flow - // rules. - auto cloned_context = fuzzerutil::CloneIRContext(ir_context); - ApplyImpl(cloned_context.get(), transformation_context); - return fuzzerutil::IsValid(cloned_context.get(), - transformation_context.GetValidatorOptions(), - fuzzerutil::kSilentMessageConsumer); + // hold. + return fuzzerutil::NewTerminatorPreservesDominationRules( + ir_context, message_.from_block(), + fuzzerutil::CreateUnreachableEdgeInstruction( + ir_context, message_.from_block(), message_.to_block(), bool_id)); } void TransformationAddDeadBreak::Apply( opt::IRContext* ir_context, TransformationContext* transformation_context) const { - ApplyImpl(ir_context, *transformation_context); + fuzzerutil::AddUnreachableEdgeAndUpdateOpPhis( + ir_context, ir_context->cfg()->block(message_.from_block()), + ir_context->cfg()->block(message_.to_block()), + fuzzerutil::MaybeGetBoolConstant(ir_context, *transformation_context, + message_.break_condition_value(), false), + message_.phi_id()); + // Invalidate all analyses ir_context->InvalidateAnalysesExceptFor( opt::IRContext::Analysis::kAnalysisNone); @@ -201,17 +200,6 @@ protobufs::Transformation TransformationAddDeadBreak::ToMessage() const { return result; } -void TransformationAddDeadBreak::ApplyImpl( - spvtools::opt::IRContext* ir_context, - const TransformationContext& transformation_context) const { - fuzzerutil::AddUnreachableEdgeAndUpdateOpPhis( - ir_context, ir_context->cfg()->block(message_.from_block()), - ir_context->cfg()->block(message_.to_block()), - fuzzerutil::MaybeGetBoolConstant(ir_context, transformation_context, - message_.break_condition_value(), false), - message_.phi_id()); -} - std::unordered_set TransformationAddDeadBreak::GetFreshIds() const { return std::unordered_set(); } diff --git a/source/fuzz/transformation_add_dead_break.h b/source/fuzz/transformation_add_dead_break.h index afb8dc7b..b050260c 100644 --- a/source/fuzz/transformation_add_dead_break.h +++ b/source/fuzz/transformation_add_dead_break.h @@ -71,15 +71,6 @@ class TransformationAddDeadBreak : public Transformation { bool AddingBreakRespectsStructuredControlFlow(opt::IRContext* ir_context, opt::BasicBlock* bb_from) const; - // Used by 'Apply' to actually apply the transformation to the module of - // interest, and by 'IsApplicable' to do a dry-run of the transformation on a - // cloned module, in order to check that the transformation leads to a valid - // module. This is only invoked by 'IsApplicable' after certain basic - // applicability checks have been made, ensuring that the invocation of this - // method is legal. - void ApplyImpl(opt::IRContext* ir_context, - const TransformationContext& transformation_context) const; - protobufs::TransformationAddDeadBreak message_; }; diff --git a/source/fuzz/transformation_add_dead_continue.cpp b/source/fuzz/transformation_add_dead_continue.cpp index 18b3c39f..c3bdb4af 100644 --- a/source/fuzz/transformation_add_dead_continue.cpp +++ b/source/fuzz/transformation_add_dead_continue.cpp @@ -38,9 +38,10 @@ bool TransformationAddDeadContinue::IsApplicable( const TransformationContext& transformation_context) const { // First, we check that a constant with the same value as // |message_.continue_condition_value| is present. - if (!fuzzerutil::MaybeGetBoolConstant(ir_context, transformation_context, - message_.continue_condition_value(), - false)) { + const auto bool_id = fuzzerutil::MaybeGetBoolConstant( + ir_context, transformation_context, message_.continue_condition_value(), + false); + if (!bool_id) { // The required constant is not present, so the transformation cannot be // applied. return false; @@ -111,25 +112,30 @@ bool TransformationAddDeadContinue::IsApplicable( } // Adding the dead break is only valid if SPIR-V rules related to dominance - // hold. Rather than checking these rules explicitly, we defer to the - // validator. We make a clone of the module, apply the transformation to the - // clone, and check whether the transformed clone is valid. - // - // In principle some of the above checks could be removed, with more reliance - // being placed on the validator. This should be revisited if we are sure - // the validator is complete with respect to checking structured control flow - // rules. - auto cloned_context = fuzzerutil::CloneIRContext(ir_context); - ApplyImpl(cloned_context.get(), transformation_context); - return fuzzerutil::IsValid(cloned_context.get(), - transformation_context.GetValidatorOptions(), - fuzzerutil::kSilentMessageConsumer); + // hold. + return fuzzerutil::NewTerminatorPreservesDominationRules( + ir_context, message_.from_block(), + fuzzerutil::CreateUnreachableEdgeInstruction( + ir_context, message_.from_block(), continue_block, bool_id)); } void TransformationAddDeadContinue::Apply( opt::IRContext* ir_context, TransformationContext* transformation_context) const { - ApplyImpl(ir_context, *transformation_context); + auto bb_from = ir_context->cfg()->block(message_.from_block()); + auto continue_block = + bb_from->IsLoopHeader() + ? bb_from->ContinueBlockId() + : ir_context->GetStructuredCFGAnalysis()->LoopContinueBlock( + message_.from_block()); + assert(continue_block && "message_.from_block must be in a loop."); + fuzzerutil::AddUnreachableEdgeAndUpdateOpPhis( + ir_context, bb_from, ir_context->cfg()->block(continue_block), + fuzzerutil::MaybeGetBoolConstant(ir_context, *transformation_context, + message_.continue_condition_value(), + false), + message_.phi_id()); + // Invalidate all analyses ir_context->InvalidateAnalysesExceptFor( opt::IRContext::Analysis::kAnalysisNone); @@ -141,24 +147,6 @@ protobufs::Transformation TransformationAddDeadContinue::ToMessage() const { return result; } -void TransformationAddDeadContinue::ApplyImpl( - spvtools::opt::IRContext* ir_context, - const TransformationContext& transformation_context) const { - auto bb_from = ir_context->cfg()->block(message_.from_block()); - auto continue_block = - bb_from->IsLoopHeader() - ? bb_from->ContinueBlockId() - : ir_context->GetStructuredCFGAnalysis()->LoopContinueBlock( - message_.from_block()); - assert(continue_block && "message_.from_block must be in a loop."); - fuzzerutil::AddUnreachableEdgeAndUpdateOpPhis( - ir_context, bb_from, ir_context->cfg()->block(continue_block), - fuzzerutil::MaybeGetBoolConstant(ir_context, transformation_context, - message_.continue_condition_value(), - false), - message_.phi_id()); -} - std::unordered_set TransformationAddDeadContinue::GetFreshIds() const { return std::unordered_set(); diff --git a/source/fuzz/transformation_add_dead_continue.h b/source/fuzz/transformation_add_dead_continue.h index 27527e78..c78b9079 100644 --- a/source/fuzz/transformation_add_dead_continue.h +++ b/source/fuzz/transformation_add_dead_continue.h @@ -68,15 +68,6 @@ class TransformationAddDeadContinue : public Transformation { protobufs::Transformation ToMessage() const override; private: - // Used by 'Apply' to actually apply the transformation to the module of - // interest, and by 'IsApplicable' to do a dry-run of the transformation on a - // cloned module, in order to check that the transformation leads to a valid - // module. This is only invoked by 'IsApplicable' after certain basic - // applicability checks have been made, ensuring that the invocation of this - // method is legal. - void ApplyImpl(opt::IRContext* ir_context, - const TransformationContext& transformation_context) const; - protobufs::TransformationAddDeadContinue message_; }; diff --git a/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp index e8090121..78b54c46 100644 --- a/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp +++ b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.cpp @@ -162,7 +162,10 @@ bool TransformationReplaceBranchFromDeadBlockWithExit::BlockIsSuitable( if (ir_context->cfg()->preds(successor->id()).size() < 2) { return false; } - return true; + // Make sure that domination rules are satisfied when we remove the branch + // from the |block| to its |successor|. + return fuzzerutil::NewTerminatorPreservesDominationRules( + ir_context, block.id(), {ir_context, SpvOpUnreachable}); } } // namespace fuzz diff --git a/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h index e1418c9d..e0f596ef 100644 --- a/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h +++ b/source/fuzz/transformation_replace_branch_from_dead_block_with_exit.h @@ -41,13 +41,17 @@ class TransformationReplaceBranchFromDeadBlockWithExit : public Transformation { // predecessor // - |message_.opcode()| must be one of OpKill, OpReturn, OpReturnValue and // OpUnreachable - // - |message_.opcode()| can only be OpKill the module's entry points all + // - |message_.opcode()| can only be OpKill if the module's entry points all // have Fragment execution mode // - |message_.opcode()| can only be OpReturn if the return type of the // function containing the block is void // - If |message_.opcode()| is OpReturnValue then |message_.return_value_id| // must be an id that is available at the block terminator and that matches // the return type of the enclosing function + // - Domination rules should be preserved when we apply this transformation. + // In particular, if some block appears after the |block_id|'s successor in + // the CFG, then that block cannot dominate |block_id|'s successor when this + // transformation is applied. bool IsApplicable( opt::IRContext* ir_context, const TransformationContext& transformation_context) const override; diff --git a/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp b/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp index 45325038..6bba14f6 100644 --- a/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp +++ b/test/fuzz/transformation_replace_branch_from_dead_block_with_exit_test.cpp @@ -566,6 +566,302 @@ TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, OpPhi) { ASSERT_TRUE(IsEqual(env, after_transformation, context.get())); } +TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, + DominatorAfterDeadBlockSuccessor) { + std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantFalse %6 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpBranchConditional %7 %9 %10 + %9 = OpLabel + OpBranch %11 + %11 = OpLabel + %12 = OpCopyObject %6 %7 + OpBranch %8 + %10 = OpLabel + OpBranch %13 + %8 = OpLabel + OpReturn + %13 = OpLabel + OpBranch %8 + OpFunctionEnd + )"; + + const auto env = SPV_ENV_UNIVERSAL_1_4; + const auto consumer = nullptr; + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + spvtools::ValidatorOptions validator_options; + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + TransformationContext transformation_context( + MakeUnique(context.get()), validator_options); + + transformation_context.GetFactManager()->AddFactBlockIsDead(9); + transformation_context.GetFactManager()->AddFactBlockIsDead(11); + + ASSERT_FALSE( + TransformationReplaceBranchFromDeadBlockWithExit(11, SpvOpUnreachable, 0) + .IsApplicable(context.get(), transformation_context)); +} + +TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, + UnreachableSuccessor) { + std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantFalse %6 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpBranchConditional %7 %9 %10 + %9 = OpLabel + OpBranch %11 + %11 = OpLabel + %12 = OpCopyObject %6 %7 + OpBranch %8 + %10 = OpLabel + OpReturn + %8 = OpLabel + OpReturn + %13 = OpLabel + OpBranch %8 + OpFunctionEnd + )"; + + const auto env = SPV_ENV_UNIVERSAL_1_4; + const auto consumer = nullptr; + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + spvtools::ValidatorOptions validator_options; + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + TransformationContext transformation_context( + MakeUnique(context.get()), validator_options); + + transformation_context.GetFactManager()->AddFactBlockIsDead(9); + transformation_context.GetFactManager()->AddFactBlockIsDead(11); + + TransformationReplaceBranchFromDeadBlockWithExit transformation( + 11, SpvOpUnreachable, 0); + ASSERT_TRUE( + transformation.IsApplicable(context.get(), transformation_context)); + transformation.Apply(context.get(), &transformation_context); + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + + std::string after_transformation = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantFalse %6 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpBranchConditional %7 %9 %10 + %9 = OpLabel + OpBranch %11 + %11 = OpLabel + %12 = OpCopyObject %6 %7 + OpUnreachable + %10 = OpLabel + OpReturn + %8 = OpLabel + OpReturn + %13 = OpLabel + OpBranch %8 + OpFunctionEnd + )"; + + ASSERT_TRUE(IsEqual(env, after_transformation, context.get())); +} + +TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, + DeadBlockAfterItsSuccessor) { + std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantTrue %6 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpBranchConditional %7 %9 %10 + %9 = OpLabel + OpBranch %11 + %11 = OpLabel + %12 = OpCopyObject %6 %7 + OpBranch %8 + %10 = OpLabel + OpBranch %13 + %8 = OpLabel + OpReturn + %13 = OpLabel + OpBranch %8 + OpFunctionEnd + )"; + + const auto env = SPV_ENV_UNIVERSAL_1_4; + const auto consumer = nullptr; + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + spvtools::ValidatorOptions validator_options; + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + TransformationContext transformation_context( + MakeUnique(context.get()), validator_options); + + transformation_context.GetFactManager()->AddFactBlockIsDead(10); + transformation_context.GetFactManager()->AddFactBlockIsDead(13); + + TransformationReplaceBranchFromDeadBlockWithExit transformation( + 13, SpvOpUnreachable, 0); + ASSERT_TRUE( + transformation.IsApplicable(context.get(), transformation_context)); + transformation.Apply(context.get(), &transformation_context); + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + + std::string after_transformation = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantTrue %6 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpBranchConditional %7 %9 %10 + %9 = OpLabel + OpBranch %11 + %11 = OpLabel + %12 = OpCopyObject %6 %7 + OpBranch %8 + %10 = OpLabel + OpBranch %13 + %8 = OpLabel + OpReturn + %13 = OpLabel + OpUnreachable + OpFunctionEnd + )"; + + ASSERT_TRUE(IsEqual(env, after_transformation, context.get())); +} + +TEST(TransformationReplaceBranchFromDeadBlockWithExitTest, + BranchToOuterMergeBlock) { + std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantTrue %6 + %15 = OpTypeInt 32 0 + %14 = OpUndef %15 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpSwitch %14 %9 1 %8 + %9 = OpLabel + OpSelectionMerge %10 None + OpBranchConditional %7 %11 %10 + %8 = OpLabel + OpReturn + %11 = OpLabel + OpBranch %8 + %10 = OpLabel + OpBranch %8 + OpFunctionEnd + )"; + + const auto env = SPV_ENV_UNIVERSAL_1_4; + const auto consumer = nullptr; + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + spvtools::ValidatorOptions validator_options; + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + TransformationContext transformation_context( + MakeUnique(context.get()), validator_options); + + transformation_context.GetFactManager()->AddFactBlockIsDead(10); + + TransformationReplaceBranchFromDeadBlockWithExit transformation( + 10, SpvOpUnreachable, 0); + ASSERT_TRUE( + transformation.IsApplicable(context.get(), transformation_context)); + transformation.Apply(context.get(), &transformation_context); + ASSERT_TRUE(fuzzerutil::IsValidAndWellFormed(context.get(), validator_options, + kConsoleMessageConsumer)); + + std::string after_transformation = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %4 "main" + OpExecutionMode %4 OriginUpperLeft + OpSource ESSL 320 + %2 = OpTypeVoid + %3 = OpTypeFunction %2 + %6 = OpTypeBool + %7 = OpConstantTrue %6 + %15 = OpTypeInt 32 0 + %14 = OpUndef %15 + %4 = OpFunction %2 None %3 + %5 = OpLabel + OpSelectionMerge %8 None + OpSwitch %14 %9 1 %8 + %9 = OpLabel + OpSelectionMerge %10 None + OpBranchConditional %7 %11 %10 + %8 = OpLabel + OpReturn + %11 = OpLabel + OpBranch %8 + %10 = OpLabel + OpUnreachable + OpFunctionEnd + )"; + + ASSERT_TRUE(IsEqual(env, after_transformation, context.get())); +} + } // namespace } // namespace fuzz } // namespace spvtools