[spirv-reader] Classify CFG edges

Classify CFG edges:
 - loop backedge
 - a structured exit:
   - loop break
   - loop continue
   - selection break
 - fallthrough
 - forward (any of the rest)

Also error out when there should have been a merge instruction.
(More than one unique fallthrough or forward edge).

Includes lots of tests.

Bug: tint:3
Change-Id: I70f27680bdf098213056522abf04ac58a6b478ab
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/20481
Reviewed-by: dan sinclair <dsinclair@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index f728e10..7d23923 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -443,6 +443,11 @@
   if (!FindSwitchCaseHeaders()) {
     return false;
   }
+  if (!ClassifyCFGEdges()) {
+    return false;
+  }
+  // TODO(dneto): FindIfSelectionHeaders
+  // TODO(dneto): FindInvalidNestingBetweenSelections
 
   if (!EmitFunctionVariables()) {
     return false;
@@ -915,6 +920,237 @@
   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<uint32_t> successors;
+    {
+      std::unordered_set<uint32_t> 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<uint32_t> 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 e228b92..e5bf677 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -38,6 +38,31 @@
 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 @@
   bool default_is_merge = false;
   /// The list of switch values that cause a branch to this block.
   std::unique_ptr<std::vector<uint64_t>> case_values;
+
+  /// Maps the ID of a successor block (in the CFG) to its edge classification.
+  std::unordered_map<uint32_t, EdgeKind> succ_edge;
 };
 
 inline std::ostream& operator<<(std::ostream& o, const BlockInfo& bi) {
@@ -127,6 +155,9 @@
   /// @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 @@
   /// @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 @@
   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 5805bbe..9430456 100644
--- a/src/reader/spirv/function_cfg_test.cc
+++ b/src/reader/spirv/function_cfg_test.cc
@@ -61,6 +61,18 @@
   )";
 }
 
+/// 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 @@
   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