diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc index f728e10239..7d239230f7 100644 --- a/src/reader/spirv/function.cc +++ b/src/reader/spirv/function.cc @@ -443,6 +443,11 @@ bool FunctionEmitter::EmitBody() { if (!FindSwitchCaseHeaders()) { return false; } + if (!ClassifyCFGEdges()) { + return false; + } + // TODO(dneto): FindIfSelectionHeaders + // TODO(dneto): FindInvalidNestingBetweenSelections if (!EmitFunctionVariables()) { return false; @@ -915,6 +920,237 @@ bool FunctionEmitter::FindSwitchCaseHeaders() { return success(); } +BlockInfo* FunctionEmitter::HeaderForLoopOrContinue(const Construct* c) { + if (c->kind == Construct::kLoop) { + return GetBlockInfo(c->begin_id); + } + if (c->kind == Construct::kContinue) { + auto* continue_block = GetBlockInfo(c->begin_id); + return GetBlockInfo(continue_block->header_for_continue); + } + return nullptr; +} + +bool FunctionEmitter::ClassifyCFGEdges() { + if (failed()) { + return false; + } + + // Checks validity of CFG edges leaving each basic block. This implicitly + // checks dominance rules for headers and continue constructs. + // + // For each branch encountered, classify each edge (S,T) as: + // - a back-edge + // - a structured exit (specific ways of branching to enclosing construct) + // - a normal (forward) edge + // + // If more than one block is targeted by a normal edge, then S must be a + // structured header. + // + // Term: NEC(B) is the nearest enclosing construct for B. + // + // If edge (S,T) is a normal edge, and NEC(S) != NEC(T), then + // T is the header block of its NEC(T), and + // NEC(S) is the parent of NEC(T). + + for (const auto src : block_order_) { + assert(src > 0); + auto* src_info = GetBlockInfo(src); + assert(src_info); + const auto src_pos = src_info->pos; + const auto& src_construct = *(src_info->construct); + + // Compute the ordered list of unique successors. + std::vector successors; + { + std::unordered_set visited; + src_info->basic_block->ForEachSuccessorLabel( + [&successors, &visited](const uint32_t succ) { + if (visited.count(succ) == 0) { + successors.push_back(succ); + visited.insert(succ); + } + }); + } + + // There should only be one backedge per backedge block. + uint32_t num_backedges = 0; + + // Track destinations for normal forward edges. + std::vector normal_forward_edges; + + if (successors.empty() && src_construct.enclosing_continue) { + // Kill and return are not allowed in a continue construct. + return Fail() << "Invalid function exit at block " << src + << " from continue construct starting at " + << src_construct.enclosing_continue->begin_id; + } + + for (const auto dest : successors) { + const auto* dest_info = GetBlockInfo(dest); + // We've already checked terminators are sane. + assert(dest_info); + const auto dest_pos = dest_info->pos; + + // Insert the edge kind entry and keep a handle to update + // its classification. + EdgeKind& edge_kind = src_info->succ_edge[dest]; + + if (src_pos >= dest_pos) { + // This is a backedge. + edge_kind = EdgeKind::kBack; + num_backedges++; + const auto* continue_construct = src_construct.enclosing_continue; + if (!continue_construct) { + return Fail() << "Invalid backedge (" << src << "->" << dest + << "): " << src << " is not in a continue construct"; + } + if (src_pos != continue_construct->end_pos - 1) { + return Fail() << "Invalid exit (" << src << "->" << dest + << ") from continue construct: " << src + << " is not the last block in the continue construct " + "starting at " + << src_construct.begin_id + << " (violates post-dominance rule)"; + } + const auto* ct_info = GetBlockInfo(continue_construct->begin_id); + assert(ct_info); + if (ct_info->header_for_continue != dest) { + return Fail() + << "Invalid backedge (" << src << "->" << dest + << "): does not branch to the corresponding loop header, " + "expected " + << ct_info->header_for_continue; + } + } else { + // This is a forward edge. + // For now, classify it that way, but we might update it. + edge_kind = EdgeKind::kForward; + + // Exit from a continue construct can only be from the last block. + const auto* continue_construct = src_construct.enclosing_continue; + if (continue_construct != nullptr) { + if (continue_construct->ContainsPos(src_pos) && + !continue_construct->ContainsPos(dest_pos) && + (src_pos != continue_construct->end_pos - 1)) { + return Fail() << "Invalid exit (" << src << "->" << dest + << ") from continue construct: " << src + << " is not the last block in the continue construct " + "starting at " + << continue_construct->begin_id + << " (violates post-dominance rule)"; + } + } + + // Check valid structured exit cases. + + const auto& header_info = *GetBlockInfo(src_construct.begin_id); + if (dest == header_info.merge_for_header) { + // Branch to construct's merge block. + const bool src_is_loop_header = header_info.continue_for_header != 0; + const bool src_is_continue_header = + header_info.header_for_continue != 0; + edge_kind = (src_is_loop_header || src_is_continue_header) + ? EdgeKind::kLoopBreak + : EdgeKind::kToMerge; + } else { + const auto* loop_or_continue_construct = + src_construct.enclosing_loop_or_continue; + if (loop_or_continue_construct != nullptr) { + // Check for break block or continue block. + const auto* loop_header_info = + HeaderForLoopOrContinue(loop_or_continue_construct); + if (loop_header_info == nullptr) { + return Fail() << "internal error: invalid construction of loop " + "related to block " + << loop_or_continue_construct->begin_id; + } + if (dest == loop_header_info->merge_for_header) { + // It's a break block for the innermost loop. + edge_kind = EdgeKind::kLoopBreak; + } else if (dest == loop_header_info->continue_for_header) { + // It's a continue block for the innermost loop construct. + // In this case loop_or_continue_construct can't be a continue + // construct, because then the branch to the continue target is + // a backedge, and this code is only looking at forward edges. + edge_kind = EdgeKind::kLoopContinue; + } + } + } + + // A forward edge into a case construct that comes from something + // other than the OpSwitch is actually a fallthrough. + if (edge_kind == EdgeKind::kForward) { + const auto* switch_construct = + (dest_info->case_head_for ? dest_info->case_head_for + : dest_info->default_head_for); + if (switch_construct != nullptr) { + if (src != switch_construct->begin_id) { + edge_kind = EdgeKind::kCaseFallThrough; + } + } + } + + if ((edge_kind == EdgeKind::kForward) || + (edge_kind == EdgeKind::kCaseFallThrough)) { + // Check for an invalid forward exit out of this construct. + if (dest_info->pos >= src_construct.end_pos) { + return Fail() + << "Branch from block " << src << " to block " << dest + << " is an invalid exit from construct starting at block " + << src_construct.begin_id << "; branch bypasses block " + << src_construct.end_id; + } + + normal_forward_edges.push_back(dest); + + // Check dominance. + + // Look for edges that violate the dominance condition: a branch + // from X to Y where: + // If Y is in a nearest enclosing continue construct headed by + // CT: + // Y is not CT, and + // In the structured order, X appears before CT order or + // after CT's backedge block. + // Otherwise, if Y is in a nearest enclosing construct + // headed by H: + // Y is not H, and + // In the structured order, X appears before H or after H's + // merge block. + + const auto& dest_construct = *(dest_info->construct); + if (dest != dest_construct.begin_id && + !dest_construct.ContainsPos(src_pos)) { + return Fail() << "Branch from " << src << " to " << dest + << " bypasses " + << (dest_construct.kind == Construct::kContinue + ? "continue target " + : "header ") + << dest_construct.begin_id + << " (dominance rule violated)"; + } + } + } // end forward edge + } // end successor + + if (num_backedges > 1) { + return Fail() << "Block " << src + << " has too many backedges: " << num_backedges; + } + if ((normal_forward_edges.size() > 1) && + (src_info->merge_for_header == 0)) { + return Fail() << "Control flow diverges at block " << src << " (to " + << normal_forward_edges[0] << ", " + << normal_forward_edges[1] + << ") but it is not a structured header (it has no merge " + "instruction)"; + } + } + + return success(); +} + bool FunctionEmitter::EmitFunctionVariables() { if (failed()) { return false; diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h index e228b92d77..e5bf677503 100644 --- a/src/reader/spirv/function.h +++ b/src/reader/spirv/function.h @@ -38,6 +38,31 @@ namespace tint { namespace reader { namespace spirv { +/// Kinds of CFG edges. +enum class EdgeKind { + // A back-edge: An edge from a node to one of its ancestors in a depth-first + // search from the entry block. + kBack, + // An edge from a node to the merge block of the nearest enclosing structured + // construct. We write "ToMerge" to make it clear the destination block is + // the merge block, not the source block. + kToMerge, + // An edge from a node to the merge block of the nearest enclosing loop. + // The source block is a "break block" as defined by SPIR-V. + kLoopBreak, + // An edge from a node in a loop body to the associated continue target, where + // there are no other intervening loops or switches. + // The source block is a "continue block" as defined by SPIR-V. + kLoopContinue, + // An edge from one switch case to the next sibling switch case. + kCaseFallThrough, + // None of the above. By structured control flow rules, there can be at most + // one forward edge leaving a basic block. Otherwise there must have been a + // merge instruction declaring the divergence and associated reconvergence + // point. + kForward +}; + /// Bookkeeping info for a basic block. struct BlockInfo { /// Constructor @@ -87,6 +112,9 @@ struct BlockInfo { bool default_is_merge = false; /// The list of switch values that cause a branch to this block. std::unique_ptr> case_values; + + /// Maps the ID of a successor block (in the CFG) to its edge classification. + std::unordered_map succ_edge; }; inline std::ostream& operator<<(std::ostream& o, const BlockInfo& bi) { @@ -127,6 +155,9 @@ class FunctionEmitter { /// @returns a FailStream on which to emit diagnostics. FailStream& Fail() { return fail_stream_.Fail(); } + /// @returns the parser implementation + ParserImpl* parser() { return &parser_impl_; } + /// Emits the declaration, which comprises the name, parameters, and /// return type. The function AST node is appended to the module /// AST node. @@ -184,6 +215,14 @@ class FunctionEmitter { /// @returns false on failure bool FindSwitchCaseHeaders(); + /// Classifies the successor CFG edges for the ordered basic blocks. + /// Also checks validity of each edge (populates the |succ_edge| field of + /// BlockInfo). Implicitly checks dominance rules for headers and continue + /// constructs. Assumes each block has been labeled with its control flow + /// construct. + /// @returns false on failure + bool ClassifyCFGEdges(); + /// Emits declarations of function variables. /// @returns false if emission failed. bool EmitFunctionVariables(); @@ -243,6 +282,13 @@ class FunctionEmitter { ast::type::Type* GetVariableStoreType( const spvtools::opt::Instruction& var_decl_inst); + /// Finds the loop header block for a loop construct or continue construct. + /// The loop header block is the block with the corresponding OpLoopMerge + /// instruction. + /// @param c the loop or continue construct + /// @returns the block info for the loop header. + BlockInfo* HeaderForLoopOrContinue(const Construct* c); + ParserImpl& parser_impl_; ast::Module& ast_module_; spvtools::opt::IRContext& ir_context_; diff --git a/src/reader/spirv/function_cfg_test.cc b/src/reader/spirv/function_cfg_test.cc index 5805bbe06c..943045696e 100644 --- a/src/reader/spirv/function_cfg_test.cc +++ b/src/reader/spirv/function_cfg_test.cc @@ -61,6 +61,18 @@ std::string CommonTypes() { )"; } +/// Runs the necessary flow until and including classify CFG edges, +/// @returns the result of classify CFG edges. +bool FlowClassifyCFGEdges(FunctionEmitter* fe) { + fe->RegisterBasicBlocks(); + fe->ComputeBlockOrderAndPositions(); + EXPECT_TRUE(fe->VerifyHeaderContinueMergeOrder()) << fe->parser()->error(); + EXPECT_TRUE(fe->RegisterMerges()) << fe->parser()->error(); + EXPECT_TRUE(fe->LabelControlFlowConstructs()) << fe->parser()->error(); + EXPECT_TRUE(fe->FindSwitchCaseHeaders()) << fe->parser()->error(); + return fe->ClassifyCFGEdges(); +} + TEST_F(SpvParserTest, TerminatorsAreSane_SingleBlock) { auto* p = parser(test::Assemble(CommonTypes() + R"( %100 = OpFunction %void None %voidfn @@ -4153,11 +4165,1640 @@ TEST_F(SpvParserTest, DISABLED_BranchEscapesIfConstruct) { EXPECT_THAT(p->error(), Eq("something")); } -// TODO(dneto): Ok for a case target to branch directly to the merge -// TODO(dneto): Ok for a case target to be a "break block", i.e. branch to exit -// of enclosing loop. -// TODO(dneto): Ok for a case target to be a "continue block", i.e. branch to -// continue target of enclosing loop. +TEST_F(SpvParserTest, ClassifyCFGEdges_ReturnInContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel ; body + OpBranch %50 + + %50 = OpLabel + OpReturn + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)) << p->error(); + EXPECT_THAT(p->error(), Eq("Invalid function exit at block 50 from continue " + "construct starting at 50")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_KillInContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel ; body + OpBranch %50 + + %50 = OpLabel + OpKill + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), Eq("Invalid function exit at block 50 from continue " + "construct starting at 50")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_UnreachableInContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel ; body + OpBranch %50 + + %50 = OpLabel + OpUnreachable + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), Eq("Invalid function exit at block 50 from continue " + "construct starting at 50")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_BackEdge_NotInContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel ; body + OpBranch %20 ; bad backedge + + %50 = OpLabel ; continue target + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT( + p->error(), + Eq("Invalid backedge (30->20): 30 is not in a continue construct")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_BackEdge_NotInLastBlockOfContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel ; body + OpBranch %50 + + %50 = OpLabel ; continue target + OpBranchConditional %cond %20 %60 ; bad branch to %20 + + %60 = OpLabel ; end of continue construct + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + Eq("Invalid exit (50->20) from continue construct: 50 is not the " + "last block in the continue construct starting at 50 " + "(violates post-dominance rule)")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_BackEdge_ToWrongHeader) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %99 + + %20 = OpLabel + OpLoopMerge %89 %50 None + OpBranchConditional %cond %30 %89 + + %30 = OpLabel ; loop body + OpBranch %50 + + %50 = OpLabel ; continue target + OpBranch %10 + + %89 = OpLabel ; inner merge + OpBranch %99 + + %99 = OpLabel ; outer merge + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), Eq("Invalid backedge (50->10): does not branch to " + "the corresponding loop header, expected 20")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_BackEdge_SingleBlockLoop) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %20 None + OpBranchConditional %cond %20 %99 + + %99 = OpLabel ; outer merge + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi20 = fe.GetBlockInfo(20); + ASSERT_NE(bi20, nullptr); + EXPECT_EQ(bi20->succ_edge.count(20), 1); + EXPECT_EQ(bi20->succ_edge[20], EdgeKind::kBack); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_BackEdge_MultiBlockLoop_SingleBlockContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %40 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %40 + + %40 = OpLabel ; continue target + OpBranch %20 ; good back edge + + %99 = OpLabel ; outer merge + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi40 = fe.GetBlockInfo(40); + ASSERT_NE(bi40, nullptr); + EXPECT_EQ(bi40->succ_edge.count(20), 1); + EXPECT_EQ(bi40->succ_edge[20], EdgeKind::kBack); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_BackEdge_MultiBlockLoop_MultiBlockContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %40 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %40 + + %40 = OpLabel ; continue target + OpBranch %50 + + %50 = OpLabel + OpBranch %20 ; good back edge + + %99 = OpLabel ; outer merge + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi50 = fe.GetBlockInfo(50); + ASSERT_NE(bi50, nullptr); + EXPECT_EQ(bi50->succ_edge.count(20), 1); + EXPECT_EQ(bi50->succ_edge[20], EdgeKind::kBack); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_PrematureExitFromContinueConstruct) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %40 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %40 + + %40 = OpLabel ; continue construct + OpBranchConditional %cond2 %99 %50 ; invalid early exit + + %50 = OpLabel + OpBranch %20 ; back edge + + %99 = OpLabel ; outer merge + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + Eq("Invalid exit (40->99) from continue construct: 40 is not the " + "last block in the continue construct starting at 40 " + "(violates post-dominance rule)")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_LoopBreak_FromLoopHeader_SingleBlockLoop) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel ; single block loop + OpLoopMerge %99 %20 None + OpBranchConditional %cond %20 %99 + + %99 = OpLabel ; outer merge + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(20); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_LoopBreak_FromLoopHeader_MultiBlockLoop) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %30 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(20); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopBreak_FromContinueConstructHeader) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %30 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel ; Single block continue construct + OpBranchConditional %cond2 %20 %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_ToMerge_FromIfHeader) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %99 + + %20 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(20); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_ToMerge_FromIfThenElse) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %50 + + %20 = OpLabel + OpBranch %99 + + %50 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + // Then clause + auto* bi20 = fe.GetBlockInfo(20); + ASSERT_NE(bi20, nullptr); + EXPECT_EQ(bi20->succ_edge.count(99), 1); + EXPECT_EQ(bi20->succ_edge[99], EdgeKind::kToMerge); + + // Else clause + auto* bi50 = fe.GetBlockInfo(50); + ASSERT_NE(bi50, nullptr); + EXPECT_EQ(bi50->succ_edge.count(99), 1); + EXPECT_EQ(bi50->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_ToMerge_FromSwitchCaseDirect) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %30 20 %99 ; directly to merge + + %30 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(10); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_ToMerge_FromSwitchCaseBody) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %99 20 %20 + + %20 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(20); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_ToMerge_FromSwitchDefaultBody) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %30 20 %20 + + %20 = OpLabel + OpBranch %99 + + %30 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_ToMerge_FromSwitchDefaultIsMerge) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %99 20 %20 + + %20 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(10); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopBreak_FromLoopBody) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranchConditional %cond2 %50 %99 ; break-unless + + %50 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopBreak_FromContinueConstructTail) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %50 + + %50 = OpLabel ; continue target + OpBranch %60 + + %60 = OpLabel ; continue construct tail + OpBranchConditional %cond2 %20 %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(60); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopBreak_FromLoopBodyDirect) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %99 ; unconditional break + + %50 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(99), 1); + EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopContinue_LoopBodyToContinue) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %80 ; a forward edge + + %80 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(80), 1); + EXPECT_EQ(bi->succ_edge[80], EdgeKind::kLoopContinue); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopContinue_FromNestedIf) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpBranchConditional %cond2 %40 %79 + + %40 = OpLabel + OpBranch %80 ; continue + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(40); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(80), 1); + EXPECT_EQ(bi->succ_edge[80], EdgeKind::kLoopContinue); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_LoopContinue_ConditionalFromNestedIf) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpBranchConditional %cond2 %40 %79 + + %40 = OpLabel + OpBranchConditional %cond2 %80 %79 ; continue-if + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(40); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(80), 1); + EXPECT_EQ(bi->succ_edge[80], EdgeKind::kLoopContinue); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_LoopContinue_FromNestedSwitchCaseBody_Unconditional) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpSwitch %selector %79 40 %40 + + %40 = OpLabel + OpBranch %80 + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(40); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(80), 1); + EXPECT_EQ(bi->succ_edge[80], EdgeKind::kLoopContinue); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_LoopContinue_FromNestedSwitchCaseDirect_Error) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpSwitch %selector %79 40 %80 ; continue here + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + fe.RegisterBasicBlocks(); + fe.ComputeBlockOrderAndPositions(); + EXPECT_TRUE(fe.VerifyHeaderContinueMergeOrder()); + EXPECT_TRUE(fe.RegisterMerges()); + EXPECT_TRUE(fe.LabelControlFlowConstructs()); + EXPECT_FALSE(fe.FindSwitchCaseHeaders()); + EXPECT_THAT(p->error(), Eq("Switch branch from block 30 to case target block " + "80 escapes the selection construct")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_LoopContinue_FromNestedSwitchDefaultDirect_Error) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpSwitch %selector %80 40 %79 ; continue here + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + fe.RegisterBasicBlocks(); + fe.ComputeBlockOrderAndPositions(); + EXPECT_TRUE(fe.VerifyHeaderContinueMergeOrder()); + EXPECT_TRUE(fe.RegisterMerges()); + EXPECT_TRUE(fe.LabelControlFlowConstructs()); + EXPECT_FALSE(fe.FindSwitchCaseHeaders()); + EXPECT_THAT(p->error(), Eq("Switch branch from block 30 to default block 80 " + "escapes the selection construct")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_LoopContinue_FromNestedSwitchDefaultBody_Conditional) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpSwitch %selector %40 79 %79 + + %40 = OpLabel + OpBranchConditional %cond2 %80 %79 + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(40); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(80), 1); + EXPECT_EQ(bi->succ_edge[80], EdgeKind::kLoopContinue); +} + +TEST_F( + SpvParserTest, + ClassifyCFGEdges_LoopContinue_FromNestedSwitchDefaultBody_Unconditional) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpSelectionMerge %79 None + OpSwitch %selector %40 79 %79 + + %40 = OpLabel + OpBranch %80 + + %79 = OpLabel ; inner merge + OpBranch %80 + + %80 = OpLabel ; continue target + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(40); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(80), 1); + EXPECT_EQ(bi->succ_edge[80], EdgeKind::kLoopContinue); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Fallthrough_CaseTailToCase) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %99 20 %20 40 %40 + + %20 = OpLabel ; case 20 + OpBranch %30 + + %30 = OpLabel + OpBranch %40 ; fallthrough + + %40 = OpLabel ; case 40 + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(40), 1); + EXPECT_EQ(bi->succ_edge[40], EdgeKind::kCaseFallThrough); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Fallthrough_CaseTailToDefaultNotMerge) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %40 20 %20 + + %20 = OpLabel ; case 20 + OpBranch %30 + + %30 = OpLabel + OpBranch %40 ; fallthrough + + %40 = OpLabel ; case 40 + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(40), 1); + EXPECT_EQ(bi->succ_edge[40], EdgeKind::kCaseFallThrough); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Fallthrough_DefaultToCase) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %20 40 %40 + + %20 = OpLabel ; default + OpBranch %30 + + %30 = OpLabel + OpBranch %40 ; fallthrough + + %40 = OpLabel ; case 40 + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(30); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(40), 1); + EXPECT_EQ(bi->succ_edge[40], EdgeKind::kCaseFallThrough); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_Fallthrough_CaseNonTailToCase_TrueBranch) { + // This is an unusual one, and is an error. Structurally it looks like this: + // switch (val) { + // case 0: { + // if (cond) { + // fallthrough; + // } + // something = 1; + // } + // case 1: { } + // } + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %99 20 %20 50 %50 + + %20 = OpLabel + OpSelectionMerge %49 None + OpBranchConditional %cond %30 %49 + + %30 = OpLabel + OpBranch %50 ; attempt to fallthrough + + %49 = OpLabel + OpBranch %99 + + %50 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT( + p->error(), + Eq("Branch from 10 to 50 bypasses header 20 (dominance rule violated)")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_Fallthrough_CaseNonTailToCase_FalseBranch) { + // Like previous test, but taking the false branch. + + // This is an unusual one, and is an error. Structurally it looks like this: + // switch (val) { + // case 0: { + // if (cond) { + // fallthrough; + // } + // something = 1; + // } + // case 1: { } + // } + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %99 20 %20 50 %50 + + %20 = OpLabel + OpSelectionMerge %49 None + OpBranchConditional %cond %49 %30 ;; this is the difference + + %30 = OpLabel + OpBranch %50 ; attempt to fallthrough + + %49 = OpLabel + OpBranch %99 + + %50 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT( + p->error(), + Eq("Branch from 10 to 50 bypasses header 20 (dominance rule violated)")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Forward_IfToThen) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %99 + + %20 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(10); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(20), 1); + EXPECT_EQ(bi->succ_edge[20], EdgeKind::kForward); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Forward_IfToElse) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %99 %30 + + %30 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(10); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(30), 1); + EXPECT_EQ(bi->succ_edge[30], EdgeKind::kForward); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Forward_SwitchToCase) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %99 20 %20 + + %20 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(10); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(20), 1); + EXPECT_EQ(bi->succ_edge[20], EdgeKind::kForward); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Forward_SwitchToDefaultNotMerge) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpSwitch %selector %30 20 %20 + + %20 = OpLabel + OpBranch %99 + + %30 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(10); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(30), 1); + EXPECT_EQ(bi->succ_edge[30], EdgeKind::kForward); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Forward_LoopHeadToBody) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %80 + + %80 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(20); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(30), 1); + EXPECT_EQ(bi->succ_edge[30], EdgeKind::kForward); +} + +TEST_F(SpvParserTest, DISABLED_ClassifyCFGEdges_Forward_LoopToContinue) { + FAIL(); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_DomViolation_BeforeIfToSelectionInterior) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %50 ;%50 is a bad branch + + %20 = OpLabel + OpSelectionMerge %89 None + OpBranchConditional %cond %50 %89 + + %50 = OpLabel + OpBranch %89 + + %89 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT( + p->error(), + Eq("Branch from 10 to 50 bypasses header 20 (dominance rule violated)")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_DomViolation_BeforeSwitchToSelectionInterior) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %50 ;%50 is a bad branch + + %20 = OpLabel + OpSelectionMerge %89 None + OpSwitch %selector %89 50 %50 + + %50 = OpLabel + OpBranch %89 + + %89 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT( + p->error(), + Eq("Branch from 10 to 50 bypasses header 20 (dominance rule violated)")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_DomViolation_BeforeLoopToLoopBodyInterior) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %50 ;%50 is a bad branch + + %20 = OpLabel + OpLoopMerge %89 %80 None + OpBranchConditional %cond %50 %89 + + %50 = OpLabel + OpBranch %89 + + %80 = OpLabel + OpBranch %20 + + %89 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + // Weird error, but still we caught it. + // Preferred: Eq("Branch from 10 to 50 bypasses header 20 + // (dominance rule violated)")) + Eq("Branch from 10 to 50 bypasses continue target 80 (dominance " + "rule violated)")) + << Dump(fe.block_order()); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_DomViolation_BeforeContinueToContinueInterior) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranch %60 + + %50 = OpLabel + OpBranch %60 + + %60 = OpLabel + OpBranch %20 + + %89 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + Eq("Branch from block 30 to block 60 is an invalid exit from " + "construct starting at block 20; branch bypasses block 50")); +} + +TEST_F(SpvParserTest, + ClassifyCFGEdges_DomViolation_AfterContinueToContinueInterior) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %80 %50 None + OpBranchConditional %cond %30 %80 + + %30 = OpLabel + OpBranch %50 + + %50 = OpLabel + OpBranch %60 + + %60 = OpLabel + OpBranch %20 + + %80 = OpLabel + OpBranch %60 ; bad branch +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + Eq("Branch from block 50 to block 60 is an invalid exit from " + "construct starting at block 50; branch bypasses block 80")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_TooManyBackedges) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %50 None + OpBranchConditional %cond %30 %99 + + %30 = OpLabel + OpBranchConditional %cond2 %20 %50 + + %50 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT( + p->error(), + Eq("Invalid backedge (30->20): 30 is not in a continue construct")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_NeededMerge_BranchConditional) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %20 = OpLabel + OpBranchConditional %cond %30 %40 + + %30 = OpLabel + OpBranch %99 + + %40 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + Eq("Control flow diverges at block 20 (to 30, 40) but it is not " + "a structured header (it has no merge instruction)")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_NeededMerge_Switch) { + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSwitch %selector %99 20 %20 30 %30 + + %20 = OpLabel + OpBranch %99 + + %30 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_FALSE(FlowClassifyCFGEdges(&fe)); + EXPECT_THAT(p->error(), + Eq("Control flow diverges at block 10 (to 99, 20) but it is not " + "a structured header (it has no merge instruction)")); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Pathological_Forward_LoopHeadSplitBody) { + // In this case the branch-conditional in the loop header is really also a + // selection header. + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpBranch %20 + + %20 = OpLabel + OpLoopMerge %99 %80 None + OpBranchConditional %cond %30 %50 ; what to make of this? + + %30 = OpLabel + OpBranch %99 + + %50 = OpLabel + OpBranch %99 + + %80 = OpLabel + OpBranch %20 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi = fe.GetBlockInfo(20); + ASSERT_NE(bi, nullptr); + EXPECT_EQ(bi->succ_edge.count(30), 1); + EXPECT_EQ(bi->succ_edge[30], EdgeKind::kForward); + EXPECT_EQ(bi->succ_edge.count(50), 1); + EXPECT_EQ(bi->succ_edge[50], EdgeKind::kForward); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Pathological_Forward_Premerge) { + // Two arms of an if-selection converge early, before the merge block + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %30 + + %20 = OpLabel + OpBranch %50 + + %30 = OpLabel + OpBranch %50 + + %50 = OpLabel ; this is an early merge! + OpBranch %60 + + %60 = OpLabel ; still early merge + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi20 = fe.GetBlockInfo(20); + ASSERT_NE(bi20, nullptr); + EXPECT_EQ(bi20->succ_edge.count(50), 1); + EXPECT_EQ(bi20->succ_edge[50], EdgeKind::kForward); + + auto* bi30 = fe.GetBlockInfo(30); + ASSERT_NE(bi30, nullptr); + EXPECT_EQ(bi30->succ_edge.count(50), 1); + EXPECT_EQ(bi30->succ_edge[50], EdgeKind::kForward); + + auto* bi50 = fe.GetBlockInfo(50); + ASSERT_NE(bi50, nullptr); + EXPECT_EQ(bi50->succ_edge.count(60), 1); + EXPECT_EQ(bi50->succ_edge[60], EdgeKind::kForward); + + auto* bi60 = fe.GetBlockInfo(60); + ASSERT_NE(bi60, nullptr); + EXPECT_EQ(bi60->succ_edge.count(99), 1); + EXPECT_EQ(bi60->succ_edge[99], EdgeKind::kToMerge); +} + +TEST_F(SpvParserTest, ClassifyCFGEdges_Pathological_Forward_Regardless) { + // Both arms of an OpBranchConditional go to the same target. + auto assembly = CommonTypes() + R"( + %100 = OpFunction %void None %voidfn + + %10 = OpLabel + OpSelectionMerge %99 None + OpBranchConditional %cond %20 %20 ; same target! + + %20 = OpLabel + OpBranch %99 + + %99 = OpLabel + OpReturn +)"; + auto* p = parser(test::Assemble(assembly)); + ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error(); + FunctionEmitter fe(p, *spirv_function(100)); + EXPECT_TRUE(FlowClassifyCFGEdges(&fe)); + + auto* bi10 = fe.GetBlockInfo(10); + ASSERT_NE(bi10, nullptr); + EXPECT_EQ(bi10->succ_edge.count(20), 1); + EXPECT_EQ(bi10->succ_edge[20], EdgeKind::kForward); + + auto* bi20 = fe.GetBlockInfo(20); + ASSERT_NE(bi20, nullptr); + EXPECT_EQ(bi20->succ_edge.count(99), 1); + EXPECT_EQ(bi20->succ_edge[99], EdgeKind::kToMerge); +} } // namespace } // namespace spirv