[spirv-reader] Add fallthrough

Bug: tint:3
Change-Id: Ib2d337156d419ed13ef9c67aa94ac3ee90f79548
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/23041
Reviewed-by: dan sinclair <dsinclair@google.com>
diff --git a/src/reader/spirv/function.cc b/src/reader/spirv/function.cc
index 723f351..a5cefcb 100644
--- a/src/reader/spirv/function.cc
+++ b/src/reader/spirv/function.cc
@@ -2040,9 +2040,12 @@
 
       // The fallthrough case is special because WGSL requires the fallthrough
       // statement to be last in the case clause.
-      if (true_kind == EdgeKind::kCaseFallThrough ||
-          false_kind == EdgeKind::kCaseFallThrough) {
-        return Fail() << "fallthrough is unhandled";
+      if (true_kind == EdgeKind::kCaseFallThrough) {
+        return EmitConditionalCaseFallThrough(block_info, std::move(cond),
+                                              false_kind, *false_info, true);
+      } else if (false_kind == EdgeKind::kCaseFallThrough) {
+        return EmitConditionalCaseFallThrough(block_info, std::move(cond),
+                                              true_kind, *true_info, false);
       }
 
       // At this point, at most one edge is kForward or kIfBreak.
@@ -2067,16 +2070,21 @@
   return success();
 }
 
-std::unique_ptr<ast::Statement> FunctionEmitter::MakeBranch(
+std::unique_ptr<ast::Statement> FunctionEmitter::MakeBranchInternal(
     const BlockInfo& src_info,
-    const BlockInfo& dest_info) const {
+    const BlockInfo& dest_info,
+    bool forced) const {
   auto kind = src_info.succ_edge.find(dest_info.id)->second;
   switch (kind) {
     case EdgeKind::kBack:
       // Nothing to do. The loop backedge is implicit.
       break;
     case EdgeKind::kSwitchBreak: {
-      // Don't bother with a break at the end of a case.
+      if (forced) {
+        return std::make_unique<ast::BreakStatement>();
+      }
+      // Unless forced, don't bother with a break at the end of a case/default
+      // clause.
       const auto header = dest_info.header_for_merge;
       assert(header != 0);
       const auto* exiting_construct = GetBlockInfo(header)->construct;
@@ -2148,6 +2156,52 @@
   return if_stmt;
 }
 
+bool FunctionEmitter::EmitConditionalCaseFallThrough(
+    const BlockInfo& src_info,
+    std::unique_ptr<ast::Expression> cond,
+    EdgeKind other_edge_kind,
+    const BlockInfo& other_dest,
+    bool fall_through_is_true_branch) {
+  // In WGSL, the fallthrough statement must come last in the case clause.
+  // So we'll emit an if statement for the other branch, and then emit
+  // the fallthrough.
+
+  // We have two distinct destinations. But we only get here if this
+  // is a normal terminator; in particular the source block is *not* the
+  // start of an if-selection.  So at most one branch is a kForward or
+  // kCaseFallThrough.
+  if (other_edge_kind == EdgeKind::kForward) {
+    return Fail()
+           << "internal error: normal terminator OpBranchConditional has "
+              "both forward and fallthrough edges";
+  }
+  if (other_edge_kind == EdgeKind::kIfBreak) {
+    return Fail()
+           << "internal error: normal terminator OpBranchConditional has "
+              "both IfBreak and fallthrough edges.  Violates nesting rule";
+  }
+  if (other_edge_kind == EdgeKind::kBack) {
+    return Fail()
+           << "internal error: normal terminator OpBranchConditional has "
+              "both backedge and fallthrough edges.  Violates nesting rule";
+  }
+  auto other_branch = MakeForcedBranch(src_info, other_dest);
+  if (other_branch == nullptr) {
+    return Fail() << "internal error: expected a branch for edge-kind "
+                  << int(other_edge_kind);
+  }
+  if (fall_through_is_true_branch) {
+    AddStatement(
+        MakeSimpleIf(std::move(cond), nullptr, std::move(other_branch)));
+  } else {
+    AddStatement(
+        MakeSimpleIf(std::move(cond), std::move(other_branch), nullptr));
+  }
+  AddStatement(std::make_unique<ast::FallthroughStatement>());
+
+  return success();
+}
+
 bool FunctionEmitter::EmitStatementsInBasicBlock(const BlockInfo& block_info,
                                                  bool* already_emitted) {
   if (*already_emitted) {
diff --git a/src/reader/spirv/function.h b/src/reader/spirv/function.h
index a5a5347..1719def 100644
--- a/src/reader/spirv/function.h
+++ b/src/reader/spirv/function.h
@@ -330,12 +330,42 @@
 
   /// Returns a new statement to represent the given branch representing a
   /// "normal" terminator, as in the sense of EmitNormalTerminator.  If no
-  /// WGSL statement is required, the statement will be nullptr.
+  /// WGSL statement is required, the statement will be nullptr. This method
+  /// tries to avoid emitting a 'break' statement when that would be redundant
+  /// in WGSL due to implicit breaking out of a switch.
   /// @param src_info the source block
   /// @param dest_info the destination block
   /// @returns the new statement, or a null statement
   std::unique_ptr<ast::Statement> MakeBranch(const BlockInfo& src_info,
-                                             const BlockInfo& dest_info) const;
+                                             const BlockInfo& dest_info) const {
+    return MakeBranchInternal(src_info, dest_info, false);
+  }
+
+  /// Returns a new statement to represent the given branch representing a
+  /// "normal" terminator, as in the sense of EmitNormalTerminator.  If no
+  /// WGSL statement is required, the statement will be nullptr.
+  /// @param src_info the source block
+  /// @param dest_info the destination block
+  /// @returns the new statement, or a null statement
+  std::unique_ptr<ast::Statement> MakeForcedBranch(
+      const BlockInfo& src_info,
+      const BlockInfo& dest_info) const {
+    return MakeBranchInternal(src_info, dest_info, true);
+  }
+
+  /// Returns a new statement to represent the given branch representing a
+  /// "normal" terminator, as in the sense of EmitNormalTerminator.  If no
+  /// WGSL statement is required, the statement will be nullptr. When |forced|
+  /// is false, this method tries to avoid emitting a 'break' statement when
+  /// that would be redundant in WGSL due to implicit breaking out of a switch.
+  /// When |forced| is true, the method won't try to avoid emitting that break.
+  /// @param src_info the source block
+  /// @param dest_info the destination block
+  /// @param forced if true, always emit the branch (if it exists in WGSL)
+  /// @returns the new statement, or a null statement
+  std::unique_ptr<ast::Statement> MakeBranchInternal(const BlockInfo& src_info,
+                                                     const BlockInfo& dest_info,
+                                                     bool forced) const;
 
   /// Returns a new if statement with the given statements as the then-clause
   /// and the else-clause.  Either or both clauses might be nullptr. If both
@@ -349,6 +379,24 @@
       std::unique_ptr<ast::Statement> then_stmt,
       std::unique_ptr<ast::Statement> else_stmt) const;
 
+  /// Emits the statements for an normal-terminator OpBranchConditional
+  /// where one branch is a case fall through (the true branch if and only
+  /// if |fall_through_is_true_branch| is true), and the other branch is
+  /// goes to a different destination, named by |other_dest|.
+  /// @param src_info the basic block from which we're branching
+  /// @param cond the branching condition
+  /// @param other_edge_kind the edge kind from the source block to the other
+  /// destination
+  /// @param other_dest the other branching destination
+  /// @param fall_through_is_true_branch true when the fall-through is the true
+  /// branch
+  /// @returns the false if emission fails
+  bool EmitConditionalCaseFallThrough(const BlockInfo& src_info,
+                                      std::unique_ptr<ast::Expression> cond,
+                                      EdgeKind other_edge_kind,
+                                      const BlockInfo& other_dest,
+                                      bool fall_through_is_true_branch);
+
   /// Emits a normal instruction: not a terminator, label, or variable
   /// declaration.
   /// @param inst the instruction
diff --git a/src/reader/spirv/function_cfg_test.cc b/src/reader/spirv/function_cfg_test.cc
index 4cf029d..820f969 100644
--- a/src/reader/spirv/function_cfg_test.cc
+++ b/src/reader/spirv/function_cfg_test.cc
@@ -10237,8 +10237,63 @@
 )")) << ToString(fe.ast_body());
 }
 
-TEST_F(SpvParserTest, DISABLED_EmitBody_Branch_Fallthrough) {
-  // TODO(dneto): support fallthrough first.
+TEST_F(SpvParserTest, EmitBody_Branch_Fallthrough) {
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_1
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20 30 %30
+
+     %20 = OpLabel
+     OpStore %var %uint_20
+     OpBranch %30 ; uncondtional fallthrough
+
+     %30 = OpLabel
+     OpStore %var %uint_30
+     OpBranch %99
+
+     %99 = OpLabel
+     OpStore %var %uint_7
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+  EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+  Identifier{var}
+  ScalarConstructor{1}
+}
+Switch{
+  ScalarConstructor{42}
+  {
+    Case 20{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{20}
+      }
+      Fallthrough{}
+    }
+    Case 30{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{30}
+      }
+    }
+    Default{
+    }
+  }
+}
+Assignment{
+  Identifier{var}
+  ScalarConstructor{7}
+}
+Return{}
+)")) << ToString(fe.ast_body());
 }
 
 TEST_F(SpvParserTest, EmitBody_Branch_Forward) {
@@ -10299,8 +10354,8 @@
 //      kLoopBreak: dup general case
 //      kLoopContinue: TESTED
 //      kIfBreak: invalid: switch and if must have distinct merge blocks
-//      kCaseFallThrough: TODO(dneto)
-//      kForward: TESTED
+//      kCaseFallThrough: not possible, because switch break conflicts with loop
+//      break kForward: TESTED
 //
 //    kLoopContinue with:
 //      kBack : symmetry
@@ -11039,13 +11094,143 @@
 }
 
 TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_SwitchBreak_Fallthrough_OnTrue) {
-  // TODO(dneto): needs fallthrough support
+       EmitBody_BranchConditional_SwitchBreak_Fallthrough_OnTrue) {
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_1
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20 30 %30
+
+     %20 = OpLabel
+     OpStore %var %uint_20
+     OpBranchConditional %cond %30 %99; fallthrough on true
+
+     %30 = OpLabel
+     OpStore %var %uint_30
+     OpBranch %99
+
+     %99 = OpLabel
+     OpStore %var %uint_7
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+  EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+  Identifier{var}
+  ScalarConstructor{1}
+}
+Switch{
+  ScalarConstructor{42}
+  {
+    Case 20{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{20}
+      }
+      If{
+        (
+          ScalarConstructor{false}
+        )
+        {
+        }
+      }
+      Else{
+        {
+          Break{}
+        }
+      }
+      Fallthrough{}
+    }
+    Case 30{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{30}
+      }
+    }
+    Default{
+    }
+  }
+}
+Assignment{
+  Identifier{var}
+  ScalarConstructor{7}
+}
+Return{}
+)")) << ToString(fe.ast_body());
 }
 
 TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_SwitchBreak_Fallthrough_OnFalse) {
-  // TODO(dneto): needs fallthrough support
+       EmitBody_BranchConditional_SwitchBreak_Fallthrough_OnFalse) {
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_1
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20 30 %30
+
+     %20 = OpLabel
+     OpStore %var %uint_20
+     OpBranchConditional %cond %99 %30; fallthrough on false
+
+     %30 = OpLabel
+     OpStore %var %uint_30
+     OpBranch %99
+
+     %99 = OpLabel
+     OpStore %var %uint_7
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+  EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+  Identifier{var}
+  ScalarConstructor{1}
+}
+Switch{
+  ScalarConstructor{42}
+  {
+    Case 20{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{20}
+      }
+      If{
+        (
+          ScalarConstructor{false}
+        )
+        {
+          Break{}
+        }
+      }
+      Fallthrough{}
+    }
+    Case 30{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{30}
+      }
+    }
+    Default{
+    }
+  }
+}
+Assignment{
+  Identifier{var}
+  ScalarConstructor{7}
+}
+Return{}
+)")) << ToString(fe.ast_body());
 }
 
 TEST_F(SpvParserTest,
@@ -11349,12 +11534,53 @@
 }
 
 TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_LoopBreak_Fallthrough_OnTrue) {
-  // TODO(dneto): needs fallthrough support
-}
-TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_LoopBreak_Fallthrough_OnFalse) {
-  // TODO(dneto): needs fallthrough support
+       EmitBody_BranchConditional_LoopBreak_Fallthrough_IsError) {
+  // It's an error because switch break conflicts with loop break.
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_0
+     OpBranch %20
+
+     %20 = OpLabel
+     OpStore %var %uint_1
+     OpLoopMerge %99 %80 None
+     OpBranch %30
+
+     %30 = OpLabel
+     OpSelectionMerge %79 None
+     OpSwitch %selector %79 40 %40 50 %50
+
+     %40 = OpLabel
+     OpStore %var %uint_40
+     ; error: branch to 99 bypasses switch's merge
+     OpBranchConditional %cond %99 %50 ; loop break; fall through
+
+     %50 = OpLabel
+     OpStore %var %uint_50
+     OpBranch %79
+
+     %79 = OpLabel ; switch merge
+     OpBranch %80
+
+     %80 = OpLabel ; continue target
+     OpStore %var %uint_4
+     OpBranch %20
+
+     %99 = OpLabel
+     OpStore %var %uint_5
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_FALSE(fe.EmitBody()) << p->error();
+  EXPECT_THAT(
+      p->error(),
+      Eq("Branch from block 40 to block 99 is an invalid exit from construct "
+         "starting at block 30; branch bypasses merge block 79"));
 }
 
 TEST_F(SpvParserTest, EmitBody_BranchConditional_LoopBreak_Forward_OnTrue) {
@@ -12057,13 +12283,214 @@
 )")) << ToString(fe.ast_body());
 }
 
-TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_Continue_Fallthrough_OnTrue) {
-  // TODO(dneto): needs fallthrough support
+TEST_F(SpvParserTest, EmitBody_BranchConditional_Continue_Fallthrough_OnTrue) {
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_0
+     OpBranch %20
+
+     %20 = OpLabel
+     OpStore %var %uint_1
+     OpLoopMerge %99 %80 None
+     OpBranch %30
+
+     %30 = OpLabel
+     OpStore %var %uint_2
+     OpSelectionMerge %79 None
+     OpSwitch %selector %79 40 %40 50 %50
+
+     %40 = OpLabel
+     OpStore %var %uint_40
+     OpBranchConditional %cond %50 %80 ; loop continue; fall through on true
+
+     %50 = OpLabel
+     OpStore %var %uint_50
+     OpBranch %79
+
+     %79 = OpLabel ; switch merge
+     OpStore %var %uint_3
+     OpBranch %80
+
+     %80 = OpLabel ; continue target
+     OpStore %var %uint_4
+     OpBranch %20
+
+     %99 = OpLabel
+     OpStore %var %uint_5
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_TRUE(fe.EmitBody()) << p->error();
+  EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+  Identifier{var}
+  ScalarConstructor{0}
 }
-TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_Continue_Fallthrough_OnFalse) {
-  // TODO(dneto): needs fallthrough support
+Loop{
+  Assignment{
+    Identifier{var}
+    ScalarConstructor{1}
+  }
+  Assignment{
+    Identifier{var}
+    ScalarConstructor{2}
+  }
+  Switch{
+    ScalarConstructor{42}
+    {
+      Case 40{
+        Assignment{
+          Identifier{var}
+          ScalarConstructor{40}
+        }
+        If{
+          (
+            ScalarConstructor{false}
+          )
+          {
+          }
+        }
+        Else{
+          {
+            Continue{}
+          }
+        }
+        Fallthrough{}
+      }
+      Case 50{
+        Assignment{
+          Identifier{var}
+          ScalarConstructor{50}
+        }
+      }
+      Default{
+      }
+    }
+  }
+  Assignment{
+    Identifier{var}
+    ScalarConstructor{3}
+  }
+  continuing {
+    Assignment{
+      Identifier{var}
+      ScalarConstructor{4}
+    }
+  }
+}
+Assignment{
+  Identifier{var}
+  ScalarConstructor{5}
+}
+Return{}
+)")) << ToString(fe.ast_body());
+}
+
+TEST_F(SpvParserTest, EmitBody_BranchConditional_Continue_Fallthrough_OnFalse) {
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_0
+     OpBranch %20
+
+     %20 = OpLabel
+     OpStore %var %uint_1
+     OpLoopMerge %99 %80 None
+     OpBranch %30
+
+     %30 = OpLabel
+     OpStore %var %uint_2
+     OpSelectionMerge %79 None
+     OpSwitch %selector %79 40 %40 50 %50
+
+     %40 = OpLabel
+     OpStore %var %uint_40
+     OpBranchConditional %cond %80 %50 ; loop continue; fall through on false
+
+     %50 = OpLabel
+     OpStore %var %uint_50
+     OpBranch %79
+
+     %79 = OpLabel ; switch merge
+     OpStore %var %uint_3
+     OpBranch %80
+
+     %80 = OpLabel ; continue target
+     OpStore %var %uint_4
+     OpBranch %20
+
+     %99 = OpLabel
+     OpStore %var %uint_5
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_TRUE(fe.EmitBody()) << p->error();
+  EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+  Identifier{var}
+  ScalarConstructor{0}
+}
+Loop{
+  Assignment{
+    Identifier{var}
+    ScalarConstructor{1}
+  }
+  Assignment{
+    Identifier{var}
+    ScalarConstructor{2}
+  }
+  Switch{
+    ScalarConstructor{42}
+    {
+      Case 40{
+        Assignment{
+          Identifier{var}
+          ScalarConstructor{40}
+        }
+        If{
+          (
+            ScalarConstructor{false}
+          )
+          {
+            Continue{}
+          }
+        }
+        Fallthrough{}
+      }
+      Case 50{
+        Assignment{
+          Identifier{var}
+          ScalarConstructor{50}
+        }
+      }
+      Default{
+      }
+    }
+  }
+  Assignment{
+    Identifier{var}
+    ScalarConstructor{3}
+  }
+  continuing {
+    Assignment{
+      Identifier{var}
+      ScalarConstructor{4}
+    }
+  }
+}
+Assignment{
+  Identifier{var}
+  ScalarConstructor{5}
+}
+Return{}
+)")) << ToString(fe.ast_body());
 }
 
 TEST_F(SpvParserTest, EmitBody_BranchConditional_Continue_Forward_OnTrue) {
@@ -12298,16 +12725,103 @@
          "starting at block 20; branch bypasses merge block 89"));
 }
 
-TEST_F(SpvParserTest,
-       DISABLED_EmitBody_BranchConditional_Fallthrough_Fallthrough_Same) {
-  // Can only be to the same target.
-  // TODO(dneto): needs fallthrough support
+TEST_F(SpvParserTest, EmitBody_BranchConditional_Fallthrough_Fallthrough_Same) {
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpStore %var %uint_1
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20 30 %30
+
+     %20 = OpLabel
+     OpStore %var %uint_20
+     OpBranchConditional %cond %30 %30 ; fallthrough fallthrough
+
+     %30 = OpLabel
+     OpStore %var %uint_30
+     OpBranch %99
+
+     %99 = OpLabel
+     OpStore %var %uint_7
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_TRUE(fe.EmitBody()) << p->error();
+
+  EXPECT_THAT(ToString(fe.ast_body()), Eq(R"(Assignment{
+  Identifier{var}
+  ScalarConstructor{1}
+}
+Switch{
+  ScalarConstructor{42}
+  {
+    Case 20{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{20}
+      }
+      Fallthrough{}
+    }
+    Case 30{
+      Assignment{
+        Identifier{var}
+        ScalarConstructor{30}
+      }
+    }
+    Default{
+    }
+  }
+}
+Assignment{
+  Identifier{var}
+  ScalarConstructor{7}
+}
+Return{}
+)")) << ToString(fe.ast_body());
 }
 
-TEST_F(
-    SpvParserTest,
-    DISABLED_EmitBody_BranchConditional_Fallthrough_Fallthrough_Different_IsError) {
-  // TODO(dneto): needs fallthrough support
+TEST_F(SpvParserTest,
+       EmitBody_BranchConditional_Fallthrough_NotLastInCase_IsError) {
+  // See also
+  // ClassifyCFGEdges_Fallthrough_BranchConditionalWith_Forward_IsError.
+  auto* p = parser(test::Assemble(CommonTypes() + R"(
+     %100 = OpFunction %void None %voidfn
+
+     %10 = OpLabel
+     OpSelectionMerge %99 None
+     OpSwitch %selector %99 20 %20 40 %40
+
+     %20 = OpLabel ; case 30
+     OpSelectionMerge %39 None
+     OpBranchConditional %cond %40 %30 ; fallthrough and forward
+
+     %30 = OpLabel
+     OpBranch %39
+
+     %39 = OpLabel
+     OpBranch %99
+
+     %40 = OpLabel  ; case 40
+     OpBranch %99
+
+     %99 = OpLabel
+     OpReturn
+
+     OpFunctionEnd
+  )"));
+  ASSERT_TRUE(p->BuildAndParseInternalModuleExceptFunctions()) << p->error();
+  FunctionEmitter fe(p, *spirv_function(100));
+  EXPECT_FALSE(fe.EmitBody());
+  // The weird forward branch pulls in 40 as part of the selection rather than
+  // as a case.
+  EXPECT_THAT(fe.block_order(), ElementsAre(10, 20, 40, 30, 39, 99));
+  EXPECT_THAT(
+      p->error(),
+      Eq("Branch from 10 to 40 bypasses header 20 (dominance rule violated)"));
 }
 
 TEST_F(SpvParserTest, EmitBody_BranchConditional_Forward_Forward_Same) {