[spirv-reader] Classify kSwitchBreak from deep in control flow

This also refactors break detection.

Bug: tint:3
Change-Id: I3a3e01c8d76d7c6fc2a14b3dbff136acd487e802
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/21220
Reviewed-by: dan sinclair <dsinclair@google.com>
diff --git a/src/reader/spirv/construct.cc b/src/reader/spirv/construct.cc
index b1ef7de..ded3d71 100644
--- a/src/reader/spirv/construct.cc
+++ b/src/reader/spirv/construct.cc
@@ -26,18 +26,36 @@
                      uint32_t the_begin_pos,
                      uint32_t the_end_pos)
     : parent(the_parent),
+      enclosing_loop(
+          // Compute the enclosing loop construct. Doing this in the
+          // constructor member list lets us make the member const.
+          // Compare parent depth because loop and continue are siblings and
+          // it's incidental which will appear on the stack first.
+          the_kind == kLoop
+              ? this
+              : ((parent && parent->depth < the_depth) ? parent->enclosing_loop
+                                                       : nullptr)),
       enclosing_continue(
           // Compute the enclosing continue construct. Doing this in the
           // constructor member list lets us make the member const.
-          the_kind == kContinue
+          // Compare parent depth because loop and continue are siblings and
+          // it's incidental which will appear on the stack first.
+          the_kind == kContinue ? this
+                                : ((parent && parent->depth < the_depth)
+                                       ? parent->enclosing_continue
+                                       : nullptr)),
+      enclosing_loop_or_continue_or_switch(
+          // Compute the enclosing loop or continue or switch construct.
+          // Doing this in the constructor member list lets us make the
+          // member const.
+          // Compare parent depth because loop and continue are siblings and
+          // it's incidental which will appear on the stack first.
+          (the_kind == kLoop || the_kind == kContinue ||
+           the_kind == kSwitchSelection)
               ? this
-              : (parent ? parent->enclosing_continue : nullptr)),
-      enclosing_loop_or_continue(
-          // Compute the enclosing loop or continue construct. Doing this
-          // in the constructor member list lets us make the member const.
-          (the_kind == kLoop || the_kind == kContinue)
-              ? this
-              : (parent ? parent->enclosing_loop_or_continue : nullptr)),
+              : ((parent && parent->depth < the_depth)
+                     ? parent->enclosing_loop_or_continue_or_switch
+                     : nullptr)),
       depth(the_depth),
       kind(the_kind),
       begin_id(the_begin_id),
diff --git a/src/reader/spirv/construct.h b/src/reader/spirv/construct.h
index a63437d..55ea5fc 100644
--- a/src/reader/spirv/construct.h
+++ b/src/reader/spirv/construct.h
@@ -68,12 +68,18 @@
   /// The nearest enclosing construct (other than itself), or nullptr if
   /// this construct represents the entire function.
   const Construct* const parent = nullptr;
-  /// The nearest continue construct, if one exists. A construct encloses
-  /// itself.
+  /// The nearest enclosing loop construct, if one exists. A construct
+  /// encloses itself.
+  const Construct* const enclosing_loop = nullptr;
+  /// The nearest enclosing continue construct, if one exists. A construct
+  /// encloses itself.
   const Construct* const enclosing_continue = nullptr;
-  /// The nearest enclosing loop or continue construct, if one exists.
+  /// The nearest enclosing loop construct or continue construct or
+  /// switch-selection construct, if one exists. The signficance is
+  /// that a high level language "break" will branch to the merge block
+  /// of such an enclosing construct.
   /// A construct encloses itself.
-  const Construct* const enclosing_loop_or_continue = nullptr;
+  const Construct* const enclosing_loop_or_continue_or_switch = nullptr;
 
   /// Control flow nesting depth. The entry block is at nesting depth 0.
   const int depth = 0;
@@ -136,12 +142,17 @@
 
   o << " parent:" << ToStringBrief(c.parent);
 
+  if (c.enclosing_loop) {
+    o << " in-l:" << ToStringBrief(c.enclosing_loop);
+  }
+
   if (c.enclosing_continue) {
     o << " in-c:" << ToStringBrief(c.enclosing_continue);
   }
 
-  if (c.enclosing_loop_or_continue != c.enclosing_continue) {
-    o << " in-c-l:" << ToStringBrief(c.enclosing_loop_or_continue);
+  if ((c.enclosing_loop_or_continue_or_switch != c.enclosing_loop) &&
+      (c.enclosing_loop_or_continue_or_switch != c.enclosing_continue)) {
+    o << " in-c-l-s:" << ToStringBrief(c.enclosing_loop_or_continue_or_switch);
   }
 
   o << " }";
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 40292df..2176aae 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -927,13 +927,20 @@
   return success();
 }
 
-BlockInfo* FunctionEmitter::HeaderForLoopOrContinue(const Construct* c) {
-  if (c->kind == Construct::kLoop) {
-    return GetBlockInfo(c->begin_id);
+BlockInfo* FunctionEmitter::HeaderIfBreakable(const Construct* c) {
+  if (c == nullptr) {
+    return nullptr;
   }
-  if (c->kind == Construct::kContinue) {
-    auto* continue_block = GetBlockInfo(c->begin_id);
-    return GetBlockInfo(continue_block->header_for_continue);
+  switch (c->kind) {
+    case Construct::kLoop:
+    case Construct::kSwitchSelection:
+      return GetBlockInfo(c->begin_id);
+    case Construct::kContinue: {
+      const auto* continue_target = GetBlockInfo(c->begin_id);
+      return GetBlockInfo(continue_target->header_for_continue);
+    }
+    default:
+      break;
   }
   return nullptr;
 }
@@ -1051,44 +1058,42 @@
 
         // 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;
-          if (src_is_loop_header || src_is_continue_header) {
-            edge_kind = EdgeKind::kLoopBreak;
-          } else if (src_construct.kind == Construct::kSwitchSelection) {
-            edge_kind = EdgeKind::kSwitchBreak;
-          } else {
-            edge_kind = 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 (edge_kind == EdgeKind::kForward) {
+          // Check for a 'break' from a loop or from a switch.
+          const auto* breakable_header = HeaderIfBreakable(
+              src_construct.enclosing_loop_or_continue_or_switch);
+          if (breakable_header != nullptr) {
+            if (dest == breakable_header->merge_for_header) {
+              // It's a break.
+              edge_kind = (breakable_header->construct->kind ==
+                           Construct::kSwitchSelection)
+                              ? EdgeKind::kSwitchBreak
+                              : EdgeKind::kLoopBreak;
             }
-            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.
+          }
+        }
+
+        if (edge_kind == EdgeKind::kForward) {
+          // Check for a 'continue' from within a loop.
+          const auto* loop_header =
+              HeaderIfBreakable(src_construct.enclosing_loop);
+          if (loop_header != nullptr) {
+            if (dest == loop_header->continue_for_header) {
+              // It's a continue.
               edge_kind = EdgeKind::kLoopContinue;
             }
           }
         }
 
+        if (edge_kind == EdgeKind::kForward) {
+          const auto& header_info = *GetBlockInfo(src_construct.begin_id);
+          if (dest == header_info.merge_for_header) {
+            // Branch to construct's merge block.  The loop break and
+            // switch break cases have already been covered.
+            edge_kind = EdgeKind::kToMerge;
+          }
+        }
+
         // A forward edge into a case construct that comes from something
         // other than the OpSwitch is actually a fallthrough.
         if (edge_kind == EdgeKind::kForward) {
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index c2e8b1c..51c32c5 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -289,12 +289,17 @@
   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);
+  /// Finds the header block for a structured construct that we can "break"
+  /// out from, from deeply nested control flow, if such a block exists.
+  /// If the construct is:
+  ///  - a switch selection: return the selection header (ending in OpSwitch)
+  ///  - a loop construct: return the loop header block
+  ///  - a continue construct: return the loop header block
+  /// Otherwise, return nullptr.
+  /// @param c a structured construct, or nullptr
+  /// @returns the block info for the structured header we can "break" from,
+  /// or nullptr
+  BlockInfo* HeaderIfBreakable(const Construct* c);
 
   ParserImpl& parser_impl_;
   ast::Module& ast_module_;
diff --git a/src/reader/spirv/function_cfg_test.cc b/src/reader/spirv/function_cfg_test.cc
index e1d96d3..2db7687 100644
--- a/src/reader/spirv/function_cfg_test.cc
+++ b/src/reader/spirv/function_cfg_test.cc
@@ -2938,7 +2938,7 @@
   EXPECT_EQ(constructs.size(), 2u);
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
-  Construct{ SwitchSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 }
+  Construct{ SwitchSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 in-c-l-s:SwitchSelection@10 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3018,12 +3018,10 @@
   fe.RegisterMerges();
   EXPECT_TRUE(fe.LabelControlFlowConstructs());
   const auto& constructs = fe.constructs();
-  // A single-block loop consists *only* of a continue target with one block in
-  // it.
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [3,5) begin_id:40 end_id:99 depth:1 parent:Function@10 in-c:Continue@40 }
-  Construct{ Loop [1,3) begin_id:20 end_id:40 depth:1 parent:Function@10 in-c-l:Loop@20 }
+  Construct{ Loop [1,3) begin_id:20 end_id:40 depth:1 parent:Function@10 in-l:Loop@20 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
@@ -3119,7 +3117,7 @@
   Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,2) begin_id:10 end_id:50 depth:1 parent:Function@10 }
   Construct{ Continue [3,4) begin_id:60 end_id:99 depth:1 parent:Function@10 in-c:Continue@60 }
-  Construct{ Loop [2,3) begin_id:50 end_id:60 depth:1 parent:Function@10 in-c-l:Loop@50 }
+  Construct{ Loop [2,3) begin_id:50 end_id:60 depth:1 parent:Function@10 in-l:Loop@50 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3237,9 +3235,9 @@
   // The ordering among siblings depends on the computed block order.
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,8) begin_id:10 end_id:0 depth:0 parent:null }
-  Construct{ SwitchSelection [0,7) begin_id:10 end_id:99 depth:1 parent:Function@10 }
-  Construct{ IfSelection [1,3) begin_id:50 end_id:89 depth:2 parent:SwitchSelection@10 }
-  Construct{ IfSelection [4,6) begin_id:20 end_id:49 depth:2 parent:SwitchSelection@10 }
+  Construct{ SwitchSelection [0,7) begin_id:10 end_id:99 depth:1 parent:Function@10 in-c-l-s:SwitchSelection@10 }
+  Construct{ IfSelection [1,3) begin_id:50 end_id:89 depth:2 parent:SwitchSelection@10 in-c-l-s:SwitchSelection@10 }
+  Construct{ IfSelection [4,6) begin_id:20 end_id:49 depth:2 parent:SwitchSelection@10 in-c-l-s:SwitchSelection@10 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3287,7 +3285,7 @@
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,5) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,4) begin_id:10 end_id:99 depth:1 parent:Function@10 }
-  Construct{ SwitchSelection [1,3) begin_id:20 end_id:89 depth:2 parent:IfSelection@10 }
+  Construct{ SwitchSelection [1,3) begin_id:20 end_id:89 depth:2 parent:IfSelection@10 in-c-l-s:SwitchSelection@20 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -3341,8 +3339,8 @@
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,8) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [4,6) begin_id:50 end_id:89 depth:1 parent:Function@10 in-c:Continue@50 }
-  Construct{ Loop [1,4) begin_id:20 end_id:50 depth:1 parent:Function@10 in-c-l:Loop@20 }
-  Construct{ Continue [2,3) begin_id:30 end_id:40 depth:2 parent:Loop@20 in-c:Continue@30 }
+  Construct{ Loop [1,4) begin_id:20 end_id:50 depth:1 parent:Function@10 in-l:Loop@20 }
+  Construct{ Continue [2,3) begin_id:30 end_id:40 depth:2 parent:Loop@20 in-l:Loop@20 in-c:Continue@30 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
@@ -3396,8 +3394,8 @@
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,7) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [5,6) begin_id:80 end_id:99 depth:1 parent:Function@10 in-c:Continue@80 }
-  Construct{ Loop [1,5) begin_id:20 end_id:80 depth:1 parent:Function@10 in-c-l:Loop@20 }
-  Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Loop@20 in-c-l:Loop@20 }
+  Construct{ Loop [1,5) begin_id:20 end_id:80 depth:1 parent:Function@10 in-l:Loop@20 }
+  Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Loop@20 in-l:Loop@20 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[0].get());
@@ -3447,7 +3445,7 @@
   EXPECT_THAT(ToString(constructs), Eq(R"(ConstructList{
   Construct{ Function [0,6) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ Continue [2,5) begin_id:30 end_id:99 depth:1 parent:Function@10 in-c:Continue@30 }
-  Construct{ Loop [1,2) begin_id:20 end_id:30 depth:1 parent:Function@10 in-c-l:Loop@20 }
+  Construct{ Loop [1,2) begin_id:20 end_id:30 depth:1 parent:Function@10 in-l:Loop@20 }
   Construct{ IfSelection [2,4) begin_id:30 end_id:49 depth:2 parent:Continue@30 in-c:Continue@30 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
@@ -3541,7 +3539,7 @@
   Construct{ Function [0,7) begin_id:10 end_id:0 depth:0 parent:null }
   Construct{ IfSelection [0,6) begin_id:10 end_id:99 depth:1 parent:Function@10 }
   Construct{ Continue [3,5) begin_id:40 end_id:89 depth:2 parent:IfSelection@10 in-c:Continue@40 }
-  Construct{ Loop [1,3) begin_id:20 end_id:40 depth:2 parent:IfSelection@10 in-c-l:Loop@20 }
+  Construct{ Loop [1,3) begin_id:20 end_id:40 depth:2 parent:IfSelection@10 in-l:Loop@20 }
 })")) << constructs;
   // The block records the nearest enclosing construct.
   EXPECT_EQ(fe.GetBlockInfo(10)->construct, constructs[1].get());
@@ -4715,6 +4713,135 @@
   EXPECT_EQ(bi->succ_edge[99], EdgeKind::kSwitchBreak);
 }
 
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedIf_Unconditional) {
+  auto assembly = CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20
+
+     %20 = OpLabel
+     OpSelectionMerge %80 None
+     OpBranchConditional %cond %30 %80
+
+     %30 = OpLabel
+     OpBranch %99
+
+     %80 = 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_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::kSwitchBreak);
+}
+
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedIf_Conditional) {
+  auto assembly = CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20
+
+     %20 = OpLabel
+     OpSelectionMerge %80 None
+     OpBranchConditional %cond %30 %80
+
+     %30 = OpLabel
+     OpBranchConditional %cond2 %99 %80 ; break-if
+
+     %80 = 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_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::kSwitchBreak);
+}
+
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedLoop_IsError) {
+  // It's an error because the break can only go as far as the loop.
+  auto assembly = CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20
+
+     %20 = OpLabel
+     OpLoopMerge %80 %70 None
+     OpBranchConditional %cond %30 %80
+
+     %30 = OpLabel ; in loop construct
+     OpBranch %99 ; break
+
+     %70 = OpLabel
+     OpBranch %20
+
+     %80 = OpLabel
+     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("Branch from block 30 to block 99 is an invalid exit from "
+                 "construct starting at block 20; branch bypasses block 70"));
+}
+
+TEST_F(SpvParserTest, ClassifyCFGEdges_SwitchBreak_FromNestedSwitch_IsError) {
+  // It's an error because the break can only go as far as inner switch
+  auto assembly = CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20
+
+     %20 = OpLabel
+     OpSelectionMerge %80 None
+     OpSwitch %selector %80 30 %30
+
+     %30 = OpLabel
+     OpBranch %99 ; break
+
+     %80 = OpLabel
+     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("Branch from block 30 to block 99 is an invalid exit from "
+                 "construct starting at block 20; branch bypasses block 80"));
+}
+
 TEST_F(SpvParserTest, ClassifyCFGEdges_LoopBreak_FromLoopBody) {
   auto assembly = CommonTypes() + R"(
      %100 = OpFunction %void None %voidfn
@@ -4811,6 +4938,125 @@
   EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak);
 }
 
+TEST_F(SpvParserTest,
+       ClassifyCFGEdges_LoopBreak_FromLoopBodyNestedSelection_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 %50 None
+     OpBranchConditional %cond2 %40 %50
+
+     %40 = OpLabel
+     OpBranch %99 ; deeply nested break
+
+     %50 = OpLabel ; inner merge
+     OpBranch %80
+
+     %80 = OpLabel
+     OpBranch %20  ; backedge
+
+     %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(99), 1);
+  EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak);
+}
+
+TEST_F(SpvParserTest,
+       ClassifyCFGEdges_LoopBreak_FromLoopBodyNestedSelection_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 %50 None
+     OpBranchConditional %cond2 %40 %50
+
+     %40 = OpLabel
+     OpBranchConditional %cond3 %99 %50 ; break-if
+
+     %50 = OpLabel ; inner merge
+     OpBranch %80
+
+     %80 = OpLabel
+     OpBranch %20  ; backedge
+
+     %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(99), 1);
+  EXPECT_EQ(bi->succ_edge[99], EdgeKind::kLoopBreak);
+}
+
+TEST_F(SpvParserTest,
+       ClassifyCFGEdges_LoopBreak_FromContinueConstructNestedFlow_IsError) {
+  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
+     OpSelectionMerge %79 None
+     OpBranchConditional %cond2 %50 %79
+
+     %50 = OpLabel
+     OpBranchConditional %cond3 %99 %79 ; attempt to break to 99 should fail
+
+     %79 = OpLabel
+     OpBranch %80  ; inner merge
+
+     %80 = OpLabel
+     OpBranch %20  ; backedge
+
+     %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->99) from continue construct: 50 is not the "
+                 "last block in the continue construct starting at 40 "
+                 "(violates post-dominance rule)"));
+}
+
 TEST_F(SpvParserTest, ClassifyCFGEdges_LoopContinue_LoopBodyToContinue) {
   auto assembly = CommonTypes() + R"(
      %100 = OpFunction %void None %voidfn
@@ -4949,7 +5195,7 @@
   auto* p = parser(test::Assemble(assembly));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   FunctionEmitter fe(p, *spirv_function(100));
-  EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+  EXPECT_TRUE(FlowClassifyCFGEdges(&fe)) << p->error();
 
   auto* bi = fe.GetBlockInfo(40);
   ASSERT_NE(bi, nullptr);
@@ -5064,7 +5310,7 @@
   auto* p = parser(test::Assemble(assembly));
   ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
   FunctionEmitter fe(p, *spirv_function(100));
-  EXPECT_TRUE(FlowClassifyCFGEdges(&fe));
+  EXPECT_TRUE(FlowClassifyCFGEdges(&fe)) << p->error();
 
   auto* bi = fe.GetBlockInfo(40);
   ASSERT_NE(bi, nullptr);