resolver: Correctly validate 'break' inside 'continuing'

We haven't been correctly checking the esoteric set of rules around breaks in continuing statements.

Bug: chromium:1288919
Change-Id: Ica6a0e71d06d9b204c359fea5f778db2383e6fa1
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/78860
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: David Neto <dneto@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/docs/origin-trial-changes.md b/docs/origin-trial-changes.md
index 8fd00ec..8045445 100644
--- a/docs/origin-trial-changes.md
+++ b/docs/origin-trial-changes.md
@@ -6,6 +6,7 @@
 
 * The `@interpolate(flat)` attribute must now be specified on integral user-defined IO. [tint:1224](crbug.com/tint/1224)
 * The `ignore()` intrinsic has been removed. Use phoney-assignment instead: `ignore(expr);` -> `_ = expr;`.
+* `break` statements in `continuing` blocks are now correctly validated.
 
 ## Changes for M99
 
diff --git a/src/program_builder.h b/src/program_builder.h
index c63a5f1..c072b55 100644
--- a/src/program_builder.h
+++ b/src/program_builder.h
@@ -2073,6 +2073,24 @@
 
   /// Creates a ast::IfStatement with input condition, body, and optional
   /// variadic else statements
+  /// @param source the source information for the if statement
+  /// @param condition the if statement condition expression
+  /// @param body the if statement body
+  /// @param elseStatements optional variadic else statements
+  /// @returns the if statement pointer
+  template <typename CONDITION, typename... ELSE_STATEMENTS>
+  const ast::IfStatement* If(const Source& source,
+                             CONDITION&& condition,
+                             const ast::BlockStatement* body,
+                             ELSE_STATEMENTS&&... elseStatements) {
+    return create<ast::IfStatement>(
+        source, Expr(std::forward<CONDITION>(condition)), body,
+        ast::ElseStatementList{
+            std::forward<ELSE_STATEMENTS>(elseStatements)...});
+  }
+
+  /// Creates a ast::IfStatement with input condition, body, and optional
+  /// variadic else statements
   /// @param condition the if statement condition expression
   /// @param body the if statement body
   /// @param elseStatements optional variadic else statements
diff --git a/src/resolver/resolver.cc b/src/resolver/resolver.cc
index 6ab3630..e5924e9 100644
--- a/src/resolver/resolver.cc
+++ b/src/resolver/resolver.cc
@@ -965,7 +965,8 @@
 
 sem::ElseStatement* Resolver::ElseStatement(const ast::ElseStatement* stmt) {
   auto* sem = builder_->create<sem::ElseStatement>(
-      stmt, current_compound_statement_, current_function_);
+      stmt, current_compound_statement_->As<sem::IfStatement>(),
+      current_function_);
   return StatementScope(stmt, sem, [&] {
     if (auto* cond_expr = stmt->condition) {
       auto* cond = Expression(cond_expr);
diff --git a/src/resolver/resolver_behavior_test.cc b/src/resolver/resolver_behavior_test.cc
index e771163..bcc2a17 100644
--- a/src/resolver/resolver_behavior_test.cc
+++ b/src/resolver/resolver_behavior_test.cc
@@ -467,8 +467,8 @@
   EXPECT_EQ(r()->error(), "12:34 error: loop does not exit");
 }
 
-TEST_F(ResolverBehaviorTest, StmtLoopEmpty_ContBreak) {
-  auto* stmt = Loop(Block(), Block(Break()));
+TEST_F(ResolverBehaviorTest, StmtLoopEmpty_ContIfTrueBreak) {
+  auto* stmt = Loop(Block(), Block(If(true, Block(Break()))));
   WrapInFunction(stmt);
 
   ASSERT_TRUE(r()->Resolve()) << r()->error();
diff --git a/src/resolver/resolver_validation.cc b/src/resolver/resolver_validation.cc
index 0668bd9..f9dadab 100644
--- a/src/resolver/resolver_validation.cc
+++ b/src/resolver/resolver_validation.cc
@@ -1263,6 +1263,64 @@
              stmt->Declaration()->source);
     return false;
   }
+  if (auto* continuing = ClosestContinuing(/*stop_at_loop*/ true)) {
+    auto fail = [&](const char* note_msg, const Source& note_src) {
+      constexpr const char* kErrorMsg =
+          "break statement in a continuing block must be the single statement "
+          "of an if statement's true or false block, and that if statement "
+          "must be the last statement of the continuing block";
+      AddError(kErrorMsg, stmt->Declaration()->source);
+      AddNote(note_msg, note_src);
+      return false;
+    };
+
+    if (auto* block = stmt->Parent()->As<sem::BlockStatement>()) {
+      auto* block_parent = block->Parent();
+      auto* if_stmt = block_parent->As<sem::IfStatement>();
+      auto* el_stmt = block_parent->As<sem::ElseStatement>();
+      if (el_stmt) {
+        if_stmt = el_stmt->Parent();
+      }
+      if (!if_stmt) {
+        return fail("break statement is not directly in if statement block",
+                    stmt->Declaration()->source);
+      }
+      if (block->Declaration()->statements.size() != 1) {
+        return fail("if statement block contains multiple statements",
+                    block->Declaration()->source);
+      }
+      for (auto* el : if_stmt->Declaration()->else_statements) {
+        if (el->condition) {
+          return fail("else has condition", el->condition->source);
+        }
+        bool el_contains_break = el_stmt && el == el_stmt->Declaration();
+        if (el_contains_break) {
+          if (auto* true_block = if_stmt->Declaration()->body;
+              !true_block->Empty()) {
+            return fail("non-empty true block", true_block->source);
+          }
+        } else {
+          if (!el->body->Empty()) {
+            return fail("non-empty false block", el->body->source);
+          }
+        }
+      }
+      if (if_stmt->Parent()->Declaration() != continuing) {
+        return fail(
+            "if statement containing break statement is not directly in "
+            "continuing block",
+            if_stmt->Declaration()->source);
+      }
+      if (auto* cont_block = continuing->As<ast::BlockStatement>()) {
+        if (if_stmt->Declaration() != cont_block->Last()) {
+          return fail(
+              "if statement containing break statement is not the last "
+              "statement of the continuing block",
+              if_stmt->Declaration()->source);
+        }
+      }
+    }
+  }
   return true;
 }
 
diff --git a/src/resolver/validation_test.cc b/src/resolver/validation_test.cc
index 65b9108..a7df8bf 100644
--- a/src/resolver/validation_test.cc
+++ b/src/resolver/validation_test.cc
@@ -1043,6 +1043,166 @@
   EXPECT_TRUE(r()->Resolve()) << r()->error();
 }
 
+TEST_F(ResolverValidationTest, Stmt_BreakInIfTrueInContinuing) {
+  auto* cont = Block(                           // continuing {
+      If(true, Block(                           //   if(true) {
+                   Break(Source{{12, 34}}))));  //     break;
+                                                //   }
+                                                // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfElseInContinuing) {
+  auto* cont = Block(                      // continuing {
+      If(true, Block(),                    //   if(true) {
+         Else(Block(                       //   } else {
+             Break(Source{{12, 34}})))));  //     break;
+                                           //   }
+                                           // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_TRUE(r()->Resolve()) << r()->error();
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInContinuing) {
+  auto* cont = Block(                   // continuing {
+      Block(Break(Source{{12, 34}})));  //   break;
+                                        // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "12:34 note: break statement is not directly in if statement block");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfInIfInContinuing) {
+  auto* cont = Block(                                      // continuing {
+      If(true, Block(                                      //   if(true) {
+                   If(Source{{56, 78}}, true,              //     if(true) {
+                      Block(Break(Source{{12, 34}}))))));  //       break;
+                                                           //     }
+                                                           //   }
+                                                           // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: if statement containing break statement is not directly in "
+      "continuing block");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfTrueMultipleStmtsInContinuing) {
+  auto* cont = Block(                             // continuing {
+      If(true, Block(Source{{56, 78}},            //   if(true) {
+                     Assign(Phony(), 1),          //     _ = 1;
+                     Break(Source{{12, 34}}))));  //     break;
+                                                  //   }
+                                                  // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: if statement block contains multiple statements");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfElseMultipleStmtsInContinuing) {
+  auto* cont = Block(                             // continuing {
+      If(true, Block(),                           //   if(true) {
+         Else(Block(Source{{56, 78}},             //   } else {
+                    Assign(Phony(), 1),           //     _ = 1;
+                    Break(Source{{12, 34}})))));  //     break;
+                                                  //   }
+                                                  // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: if statement block contains multiple statements");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfElseIfInContinuing) {
+  auto* cont = Block(                             // continuing {
+      If(true, Block(),                           //   if(true) {
+         Else(Expr(Source{{56, 78}}, true),       //   } else if (true) {
+              Block(Break(Source{{12, 34}})))));  //     break;
+                                                  //   }
+                                                  // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: else has condition");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfNonEmptyElseInContinuing) {
+  auto* cont = Block(                        // continuing {
+      If(true,                               //   if(true) {
+         Block(Break(Source{{12, 34}})),     //     break;
+         Else(Block(Source{{56, 78}},        //   } else {
+                    Assign(Phony(), 1)))));  //     _ = 1;
+                                             //   }
+                                             // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: non-empty false block");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfElseNonEmptyTrueInContinuing) {
+  auto* cont = Block(                                  // continuing {
+      If(true,                                         //   if(true) {
+         Block(Source{{56, 78}}, Assign(Phony(), 1)),  //     _ = 1;
+         Else(Block(                                   //   } else {
+             Break(Source{{12, 34}})))));              //     break;
+                                                       //   }
+                                                       // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: non-empty true block");
+}
+
+TEST_F(ResolverValidationTest, Stmt_BreakInIfInContinuingNotLast) {
+  auto* cont = Block(                      // continuing {
+      If(Source{{56, 78}}, true,           //   if(true) {
+         Block(Break(Source{{12, 34}}))),  //     break;
+                                           //   }
+      Assign(Phony(), 1));                 //   _ = 1;
+                                           // }
+  WrapInFunction(Loop(Block(), cont));
+  EXPECT_FALSE(r()->Resolve());
+  EXPECT_EQ(
+      r()->error(),
+      "12:34 error: break statement in a continuing block must be the single "
+      "statement of an if statement's true or false block, and that if "
+      "statement must be the last statement of the continuing block\n"
+      "56:78 note: if statement containing break statement is not the last "
+      "statement of the continuing block");
+}
+
 TEST_F(ResolverValidationTest, Stmt_BreakNotInLoopOrSwitch) {
   WrapInFunction(Break(Source{{12, 34}}));
   EXPECT_FALSE(r()->Resolve());
diff --git a/src/sem/if_statement.cc b/src/sem/if_statement.cc
index 8fdc8ab..be01a6e 100644
--- a/src/sem/if_statement.cc
+++ b/src/sem/if_statement.cc
@@ -34,7 +34,7 @@
 }
 
 ElseStatement::ElseStatement(const ast::ElseStatement* declaration,
-                             const CompoundStatement* parent,
+                             const IfStatement* parent,
                              const sem::Function* function)
     : Base(declaration, parent, function) {}
 
diff --git a/src/sem/if_statement.h b/src/sem/if_statement.h
index 3d927c8..4b60d9f 100644
--- a/src/sem/if_statement.h
+++ b/src/sem/if_statement.h
@@ -67,7 +67,7 @@
   /// @param parent the owning statement
   /// @param function the owning function
   ElseStatement(const ast::ElseStatement* declaration,
-                const CompoundStatement* parent,
+                const IfStatement* parent,
                 const sem::Function* function);
 
   /// Destructor
@@ -76,6 +76,11 @@
   /// @returns the else-statement condition expression
   const Expression* Condition() const { return condition_; }
 
+  /// @return the statement that encloses this statement
+  const IfStatement* Parent() const {
+    return static_cast<const IfStatement*>(Statement::Parent());
+  }
+
   /// Sets the else-statement condition expression
   /// @param condition the else condition expression
   void SetCondition(const Expression* condition) { condition_ = condition; }