AST fuzzer: mutation that deletes statements

Adds a mutation that deletes a statement.

Fixes: tint:1110
Change-Id: I8a8595378481d12e7586103e2e88a0858d869ef9
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/88446
Commit-Queue: Alastair Donaldson <afdx@google.com>
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn b/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
index 69ef3fe..58a36ea 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
+++ b/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
@@ -46,6 +46,8 @@
       "expression_size.cc",
       "expression_size.h",
       "fuzzer.cc",
+      "jump_tracker.cc",
+      "jump_tracker.h",
       "mutation.cc",
       "mutation.h",
       "mutation_finder.cc",
@@ -54,6 +56,8 @@
       "mutation_finders/change_binary_operators.h",
       "mutation_finders/change_unary_operators.cc",
       "mutation_finders/change_unary_operators.h",
+      "mutation_finders/delete_statements.cc",
+      "mutation_finders/delete_statements.h",
       "mutation_finders/replace_identifiers.cc",
       "mutation_finders/replace_identifiers.h",
       "mutation_finders/wrap_unary_operators.cc",
@@ -62,6 +66,8 @@
       "mutations/change_binary_operator.h",
       "mutations/change_unary_operator.cc",
       "mutations/change_unary_operator.h",
+      "mutations/delete_statement.cc",
+      "mutations/delete_statement.h",
       "mutations/replace_identifier.cc",
       "mutations/replace_identifier.h",
       "mutations/wrap_unary_operator.cc",
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
index 5bd8c50..0d1f5a2 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
@@ -39,14 +39,17 @@
         ../random_generator.h
         ../random_generator_engine.h
         expression_size.h
+        jump_tracker.h
         mutation.h
         mutation_finder.h
         mutation_finders/change_binary_operators.h
         mutation_finders/change_unary_operators.h
+        mutation_finders/delete_statements.h
         mutation_finders/replace_identifiers.h
         mutation_finders/wrap_unary_operators.h
         mutations/change_binary_operator.h
         mutations/change_unary_operator.h
+        mutations/delete_statement.h
         mutations/replace_identifier.h
         mutations/wrap_unary_operator.h
         mutator.h
@@ -61,14 +64,17 @@
         ../random_generator.cc
         ../random_generator_engine.cc
         expression_size.cc
+        jump_tracker.cc
         mutation.cc
         mutation_finder.cc
         mutation_finders/change_binary_operators.cc
         mutation_finders/change_unary_operators.cc
+        mutation_finders/delete_statements.cc
         mutation_finders/replace_identifiers.cc
         mutation_finders/wrap_unary_operators.cc
         mutations/change_binary_operator.cc
         mutations/change_unary_operator.cc
+        mutations/delete_statement.cc
         mutations/replace_identifier.cc
         mutations/wrap_unary_operator.cc
         mutator.cc
@@ -107,8 +113,10 @@
 if (${TINT_BUILD_TESTS})
     set(TEST_SOURCES
             expression_size_test.cc
+            jump_tracker_test.cc
             mutations/change_binary_operator_test.cc
             mutations/change_unary_operator_test.cc
+            mutations/delete_statement_test.cc
             mutations/replace_identifier_test.cc
             mutations/wrap_unary_operator_test.cc)
 
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.cc b/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.cc
new file mode 100644
index 0000000..66bbaf7
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.cc
@@ -0,0 +1,83 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h"
+
+#include <cassert>
+#include <unordered_set>
+
+#include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/for_loop_statement.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/while_statement.h"
+#include "src/tint/sem/statement.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+JumpTracker::JumpTracker(const Program& program) {
+    // Consider every AST node, looking for break, return and discard statements.
+    for (auto* node : program.ASTNodes().Objects()) {
+        auto* stmt = node->As<ast::Statement>();
+        if (stmt == nullptr) {
+            continue;
+        }
+        if (stmt->As<ast::BreakStatement>()) {
+            // This break statement either exits a loop or a switch statement.
+            // Walk up the AST until either a loop or switch statement is found. In the former case,
+            // it is the innermost loop containing the break statement, and thus all the nodes
+            // encountered along the way are nodes that contain a break from the innermost loop.
+
+            // This records the statements encountered when walking up the AST from the break
+            // statement to the innermost enclosing loop or switch statement.
+            std::unordered_set<const ast::Statement*> candidate_statements;
+            for (const ast::Statement* current = stmt;;
+                 current =
+                     As<sem::Statement>(program.Sem().Get(current))->Parent()->Declaration()) {
+                if (current->Is<ast::ForLoopStatement>() || current->Is<ast::LoopStatement>() ||
+                    current->Is<ast::WhileStatement>()) {
+                    // A loop has been encountered, thus all that statements recorded until this
+                    // point contain a break from their innermost loop.
+                    for (auto* candidate : candidate_statements) {
+                        contains_break_for_innermost_loop_.insert(candidate);
+                    }
+                    break;
+                }
+                if (current->Is<ast::SwitchStatement>()) {
+                    // A switch statement has been encountered, so the break does not correspond to
+                    // a loop break.
+                    break;
+                }
+                candidate_statements.insert(current);
+            }
+        } else if (stmt->As<ast::ReturnStatement>() || stmt->As<ast::DiscardStatement>()) {
+            // Walk up the AST from the return or discard statement, recording that every node
+            // encountered along the way contains a return/discard.
+            auto& target_set = stmt->As<ast::ReturnStatement>() ? contains_return_
+                                                                : contains_intraprocedural_discard_;
+            const ast::Statement* current = stmt;
+            while (true) {
+                target_set.insert(current);
+                auto* parent = program.Sem().Get(current)->Parent();
+                if (parent == nullptr) {
+                    break;
+                }
+                current = parent->Declaration();
+            }
+        }
+    }
+}
+}  // namespace tint::fuzzers::ast_fuzzer
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h b/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h
new file mode 100644
index 0000000..614ceb5
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h
@@ -0,0 +1,68 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_JUMP_TRACKER_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_JUMP_TRACKER_H_
+
+#include <unordered_set>
+
+#include "src/tint/ast/statement.h"
+#include "src/tint/program.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+/// This class computes information on which statements contain loop breaks, returns and discards.
+/// It could be extended to handle other jumps, such as switch breaks and loop continues, should
+/// such information prove useful.
+class JumpTracker {
+  public:
+    /// Initializes jump tracking information for the given program.
+    /// @param program - the program for which jumps will be tracked;
+    ///     must remain in scope as long as this instance exists.
+    explicit JumpTracker(const Program& program);
+
+    /// Indicates whether a statement contains a break statement for the innermost loop (if any).
+    /// @param statement - the statement of interest.
+    /// @return true if and only if the statement is, or contains, a break for the innermost
+    ///     enclosing loop.
+    bool ContainsBreakForInnermostLoop(const ast::Statement& statement) const {
+        return contains_break_for_innermost_loop_.count(&statement) > 0;
+    }
+
+    /// Indicates whether a statement contains a return statement.
+    /// @param statement - the statement of interest.
+    /// @return true if and only if the statement is, or contains, a return statement.
+    bool ContainsReturn(const ast::Statement& statement) const {
+        return contains_return_.count(&statement) > 0;
+    }
+
+    /// Indicates whether a statement contains a discard statement.
+    /// @param statement - the statement of interest.
+    /// @return true if and only if the statement is, or contains, a discard statement. This is
+    ///    determined in an intraprocedural fashion: the answer will be "false" if no discard occurs
+    ///    inside the statement, even if the statement calls a function that may lead to a discard
+    ///    being performed.
+    bool ContainsIntraproceduralDiscard(const ast::Statement& statement) const {
+        return contains_intraprocedural_discard_.count(&statement) > 0;
+    }
+
+  private:
+    std::unordered_set<const ast::Statement*> contains_break_for_innermost_loop_;
+    std::unordered_set<const ast::Statement*> contains_return_;
+    std::unordered_set<const ast::Statement*> contains_intraprocedural_discard_;
+};
+
+}  // namespace tint::fuzzers::ast_fuzzer
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_JUMP_TRACKER_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker_test.cc
new file mode 100644
index 0000000..f83e639
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/jump_tracker_test.cc
@@ -0,0 +1,332 @@
+// Copyright 2022 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h"
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/break_statement.h"
+#include "src/tint/ast/discard_statement.h"
+#include "src/tint/ast/for_loop_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/module.h"
+#include "src/tint/ast/return_statement.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/ast/while_statement.h"
+#include "src/tint/program.h"
+#include "src/tint/reader/wgsl/parser.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+namespace {
+
+TEST(JumpTrackerTest, Breaks) {
+    std::string content = R"(
+fn main() {
+  var x : u32;
+  for (var i : i32 = 0; i < 100; i++) {
+    if (i == 40) {
+      {
+        break;
+      }
+    }
+    for (var j : i32 = 0; j < 10; j++) {
+      loop {
+        if (i > j) {
+          break;
+        }
+        continuing {
+          i++;
+          j-=2;
+        }
+      }
+      switch (j) {
+        case 0: {
+          if (i == j) {
+            break;
+          }
+          i = i + 1;
+          continue;
+        }
+        default: {
+          break;
+        }
+      }
+    }
+  }
+}
+  )";
+    Source::File file("test.wgsl", content);
+    auto program = reader::wgsl::Parse(&file);
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    JumpTracker jump_tracker(program);
+
+    const auto* outer_loop_body =
+        program.AST().Functions()[0]->body->statements[1]->As<ast::ForLoopStatement>()->body;
+    const auto* first_if = outer_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* first_if_body = first_if->body;
+    const auto* block_in_first_if = first_if_body->statements[0]->As<ast::BlockStatement>();
+    const auto* break_in_first_if = block_in_first_if->statements[0]->As<ast::BreakStatement>();
+
+    const auto* innermost_loop_body = outer_loop_body->statements[1]
+                                          ->As<ast::ForLoopStatement>()
+                                          ->body->statements[0]
+                                          ->As<ast::LoopStatement>()
+                                          ->body;
+    const auto* innermost_loop_if = innermost_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* innermost_loop_if_body = innermost_loop_if->body;
+    const auto* break_in_innermost_loop =
+        innermost_loop_if_body->statements[0]->As<ast::BreakStatement>();
+
+    std::unordered_set<const ast::Statement*> containing_loop_break = {
+        outer_loop_body,        first_if,
+        first_if_body,          block_in_first_if,
+        break_in_first_if,      innermost_loop_body,
+        innermost_loop_if,      innermost_loop_if_body,
+        break_in_innermost_loop};
+
+    for (auto* node : program.ASTNodes().Objects()) {
+        auto* stmt = node->As<ast::Statement>();
+        if (stmt == nullptr) {
+            continue;
+        }
+        if (containing_loop_break.count(stmt) > 0) {
+            ASSERT_TRUE(jump_tracker.ContainsBreakForInnermostLoop(*stmt));
+        } else {
+            ASSERT_FALSE(jump_tracker.ContainsBreakForInnermostLoop(*stmt));
+        }
+    }
+}
+
+TEST(JumpTrackerTest, Returns) {
+    std::string content = R"(
+fn main() {
+  var x : u32;
+  for (var i : i32 = 0; i < 100; i++) {
+    if (i == 40) {
+      {
+        return;
+      }
+    }
+    for (var j : i32 = 0; j < 10; j++) {
+      loop {
+        if (i > j) {
+          return;
+        }
+        continuing {
+          i++;
+          j-=2;
+        }
+      }
+      switch (j) {
+        case 0: {
+          if (i == j) {
+            break;
+          }
+          i = i + 1;
+          continue;
+        }
+        default: {
+          return;
+        }
+      }
+    }
+  }
+}
+  )";
+    Source::File file("test.wgsl", content);
+    auto program = reader::wgsl::Parse(&file);
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    JumpTracker jump_tracker(program);
+
+    const auto* function_body = program.AST().Functions()[0]->body;
+    const auto* outer_loop = function_body->statements[1]->As<ast::ForLoopStatement>();
+    const auto* outer_loop_body = outer_loop->body;
+    const auto* first_if = outer_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* first_if_body = first_if->body;
+    const auto* block_in_first_if = first_if_body->statements[0]->As<ast::BlockStatement>();
+    const auto* return_in_first_if = block_in_first_if->statements[0]->As<ast::ReturnStatement>();
+    const auto* inner_for_loop = outer_loop_body->statements[1]->As<ast::ForLoopStatement>();
+    const auto* inner_for_loop_body = inner_for_loop->body;
+    const auto* innermost_loop = inner_for_loop_body->statements[0]->As<ast::LoopStatement>();
+    const auto* innermost_loop_body = innermost_loop->body;
+    const auto* innermost_loop_if = innermost_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* innermost_loop_if_body = innermost_loop_if->body;
+    const auto* return_in_innermost_loop =
+        innermost_loop_if_body->statements[0]->As<ast::ReturnStatement>();
+    const auto* switch_statement = inner_for_loop_body->statements[1]->As<ast::SwitchStatement>();
+    const auto* default_statement = switch_statement->body[1];
+    const auto* default_statement_body = default_statement->body;
+    const auto* return_in_default_statement =
+        default_statement_body->statements[0]->As<ast::ReturnStatement>();
+
+    std::unordered_set<const ast::Statement*> containing_return = {
+        function_body,          outer_loop,
+        outer_loop_body,        first_if,
+        first_if_body,          block_in_first_if,
+        return_in_first_if,     inner_for_loop,
+        inner_for_loop_body,    innermost_loop,
+        innermost_loop_body,    innermost_loop_if,
+        innermost_loop_if_body, return_in_innermost_loop,
+        switch_statement,       default_statement,
+        default_statement_body, return_in_default_statement};
+
+    for (auto* node : program.ASTNodes().Objects()) {
+        auto* stmt = node->As<ast::Statement>();
+        if (stmt == nullptr) {
+            continue;
+        }
+        if (containing_return.count(stmt) > 0) {
+            ASSERT_TRUE(jump_tracker.ContainsReturn(*stmt));
+        } else {
+            ASSERT_FALSE(jump_tracker.ContainsReturn(*stmt));
+        }
+    }
+}
+
+TEST(JumpTrackerTest, Discards) {
+    std::string content = R"(
+fn main() {
+  var x : u32;
+  for (var i : i32 = 0; i < 100; i++) {
+    if (i == 40) {
+      {
+        discard;
+      }
+    }
+    for (var j : i32 = 0; j < 10; j++) {
+      loop {
+        if (i > j) {
+          discard;
+        }
+        continuing {
+          i++;
+          j-=2;
+        }
+      }
+      switch (j) {
+        case 0: {
+          if (i == j) {
+            break;
+          }
+          i = i + 1;
+          continue;
+        }
+        default: {
+          discard;
+        }
+      }
+    }
+  }
+}
+  )";
+    Source::File file("test.wgsl", content);
+    auto program = reader::wgsl::Parse(&file);
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    JumpTracker jump_tracker(program);
+
+    const auto* function_body = program.AST().Functions()[0]->body;
+    const auto* outer_loop = function_body->statements[1]->As<ast::ForLoopStatement>();
+    const auto* outer_loop_body = outer_loop->body;
+    const auto* first_if = outer_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* first_if_body = first_if->body;
+    const auto* block_in_first_if = first_if_body->statements[0]->As<ast::BlockStatement>();
+    const auto* discard_in_first_if = block_in_first_if->statements[0]->As<ast::DiscardStatement>();
+    const auto* inner_for_loop = outer_loop_body->statements[1]->As<ast::ForLoopStatement>();
+    const auto* inner_for_loop_body = inner_for_loop->body;
+    const auto* innermost_loop = inner_for_loop_body->statements[0]->As<ast::LoopStatement>();
+    const auto* innermost_loop_body = innermost_loop->body;
+    const auto* innermost_loop_if = innermost_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* innermost_loop_if_body = innermost_loop_if->body;
+    const auto* discard_in_innermost_loop =
+        innermost_loop_if_body->statements[0]->As<ast::DiscardStatement>();
+    const auto* switch_statement = inner_for_loop_body->statements[1]->As<ast::SwitchStatement>();
+    const auto* default_statement = switch_statement->body[1];
+    const auto* default_statement_body = default_statement->body;
+    const auto* discard_in_default_statement =
+        default_statement_body->statements[0]->As<ast::DiscardStatement>();
+
+    std::unordered_set<const ast::Statement*> containing_discard = {
+        function_body,          outer_loop,
+        outer_loop_body,        first_if,
+        first_if_body,          block_in_first_if,
+        discard_in_first_if,    inner_for_loop,
+        inner_for_loop_body,    innermost_loop,
+        innermost_loop_body,    innermost_loop_if,
+        innermost_loop_if_body, discard_in_innermost_loop,
+        switch_statement,       default_statement,
+        default_statement_body, discard_in_default_statement};
+
+    for (auto* node : program.ASTNodes().Objects()) {
+        auto* stmt = node->As<ast::Statement>();
+        if (stmt == nullptr) {
+            continue;
+        }
+        if (containing_discard.count(stmt) > 0) {
+            ASSERT_TRUE(jump_tracker.ContainsIntraproceduralDiscard(*stmt));
+        } else {
+            ASSERT_FALSE(jump_tracker.ContainsIntraproceduralDiscard(*stmt));
+        }
+    }
+}
+
+TEST(JumpTrackerTest, WhileLoop) {
+    std::string content = R"(
+fn main() {
+  var x : u32;
+  x = 0;
+  while (x < 100) {
+    if (x > 50) {
+      break;
+    }
+    x = x + 1;
+  }
+}
+  )";
+    Source::File file("test.wgsl", content);
+    auto program = reader::wgsl::Parse(&file);
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    JumpTracker jump_tracker(program);
+
+    const auto* while_loop_body =
+        program.AST().Functions()[0]->body->statements[2]->As<ast::WhileStatement>()->body;
+    const auto* if_statement = while_loop_body->statements[0]->As<ast::IfStatement>();
+    const auto* if_statement_body = if_statement->body;
+    const auto* break_in_if = if_statement_body->statements[0]->As<ast::BreakStatement>();
+
+    std::unordered_set<const ast::Statement*> containing_loop_break = {
+        while_loop_body, if_statement, if_statement_body, break_in_if};
+
+    for (auto* node : program.ASTNodes().Objects()) {
+        auto* stmt = node->As<ast::Statement>();
+        if (stmt == nullptr) {
+            continue;
+        }
+        if (containing_loop_break.count(stmt) > 0) {
+            ASSERT_TRUE(jump_tracker.ContainsBreakForInnermostLoop(*stmt));
+        } else {
+            ASSERT_FALSE(jump_tracker.ContainsBreakForInnermostLoop(*stmt));
+        }
+    }
+}
+
+}  // namespace
+}  // namespace tint::fuzzers::ast_fuzzer
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
index 6f60fc3..62c970b 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
@@ -18,6 +18,7 @@
 
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_unary_operator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.h"
 
@@ -33,6 +34,8 @@
             return std::make_unique<MutationReplaceIdentifier>(message.replace_identifier());
         case protobufs::Mutation::kChangeBinaryOperator:
             return std::make_unique<MutationChangeBinaryOperator>(message.change_binary_operator());
+        case protobufs::Mutation::kDeleteStatement:
+            return std::make_unique<MutationDeleteStatement>(message.delete_statement());
         case protobufs::Mutation::kWrapUnaryOperator:
             return std::make_unique<MutationWrapUnaryOperator>(message.wrap_unary_operator());
         case protobufs::Mutation::MUTATION_NOT_SET:
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.cc
new file mode 100644
index 0000000..f1d7e4d
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.cc
@@ -0,0 +1,68 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.h"
+
+#include <memory>
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/util.h"
+#include "src/tint/sem/expression.h"
+#include "src/tint/sem/statement.h"
+#include "src/tint/sem/variable.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+MutationList MutationFinderDeleteStatements::FindMutations(const tint::Program& program,
+                                                           NodeIdMap* node_id_map,
+                                                           ProbabilityContext* /*unused*/) const {
+    MutationList result;
+
+    JumpTracker jump_tracker(program);
+
+    // Consider every statement node in the AST.
+    for (auto* node : program.ASTNodes().Objects()) {
+        auto* statement_node = tint::As<ast::Statement>(node);
+
+        if (!statement_node) {
+            continue;
+        }
+
+        const auto* statement_sem_node =
+            tint::As<sem::Statement>(program.Sem().Get(statement_node));
+
+        // Semantic information for the node is required in order to delete it.
+        if (!statement_sem_node) {
+            continue;
+        }
+
+        // Check that this kind of statement can be deleted.
+        if (!MutationDeleteStatement::CanBeDeleted(*statement_node, program, jump_tracker)) {
+            continue;
+        }
+
+        result.push_back(
+            std::make_unique<MutationDeleteStatement>(node_id_map->GetId(statement_node)));
+    }
+
+    return result;
+}
+
+uint32_t MutationFinderDeleteStatements::GetChanceOfApplyingMutation(
+    ProbabilityContext* probability_context) const {
+    return probability_context->GetChanceOfDeletingStatements();
+}
+
+}  // namespace tint::fuzzers::ast_fuzzer
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.h
new file mode 100644
index 0000000..97a4cbf
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.h
@@ -0,0 +1,36 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_DELETE_STATEMENTS_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_DELETE_STATEMENTS_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+/// Looks for opportunities to apply
+/// `MutationFinderDeleteStatements`.
+///
+/// Considers statements for deletion if their deletion would not make the module invalid.
+class MutationFinderDeleteStatements : public MutationFinder {
+  public:
+    MutationList FindMutations(const tint::Program& program,
+                               NodeIdMap* node_id_map,
+                               ProbabilityContext* probability_context) const override;
+    uint32_t GetChanceOfApplyingMutation(ProbabilityContext* probability_context) const override;
+};
+
+}  // namespace tint::fuzzers::ast_fuzzer
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_DELETE_STATEMENTS_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc
new file mode 100644
index 0000000..5fbbe23
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.cc
@@ -0,0 +1,208 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h"
+
+#include <algorithm>
+#include <utility>
+#include <vector>
+
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/fallthrough_statement.h"
+#include "src/tint/ast/for_loop_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/loop_statement.h"
+#include "src/tint/ast/variable_decl_statement.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/util.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/sem/for_loop_statement.h"
+#include "src/tint/sem/if_statement.h"
+#include "src/tint/sem/loop_statement.h"
+#include "src/tint/sem/statement.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+MutationDeleteStatement::MutationDeleteStatement(protobufs::MutationDeleteStatement message)
+    : message_(std::move(message)) {}
+
+MutationDeleteStatement::MutationDeleteStatement(uint32_t statement_id) {
+    message_.set_statement_id(statement_id);
+}
+
+bool MutationDeleteStatement::IsApplicable(const tint::Program& program,
+                                           const NodeIdMap& node_id_map) const {
+    auto* statement_node = tint::As<ast::Statement>(node_id_map.GetNode(message_.statement_id()));
+
+    if (!statement_node) {
+        // The statement id is invalid or does not refer to a statement.
+        return false;
+    }
+
+    const auto* statement_sem_node = tint::As<sem::Statement>(program.Sem().Get(statement_node));
+
+    if (!statement_sem_node) {
+        // Semantic information for the statement is not available. This
+        // information is required in order to perform the deletion.
+        return false;
+    }
+
+    // Check whether it is OK to delete this statement.
+    if (!CanBeDeleted(*statement_node, program, JumpTracker(program))) {
+        return false;
+    }
+
+    return true;
+}
+
+void MutationDeleteStatement::Apply(const NodeIdMap& node_id_map,
+                                    tint::CloneContext* clone_context,
+                                    NodeIdMap* /* unused */) const {
+    const auto* statement_node =
+        tint::As<ast::Statement>(node_id_map.GetNode(message_.statement_id()));
+    const auto* statement_sem_node =
+        tint::As<sem::Statement>(clone_context->src->Sem().Get(statement_node));
+    const auto* sem_parent = statement_sem_node->Parent();
+
+    if (tint::Is<sem::IfStatement>(sem_parent) &&
+        tint::As<ast::IfStatement>(sem_parent->Declaration())->else_statement == statement_node) {
+        // Remove the "else" part of an if statement.
+        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+    } else if (tint::Is<sem::ForLoopStatement>(sem_parent) &&
+               tint::As<ast::ForLoopStatement>(sem_parent->Declaration())->initializer ==
+                   statement_node) {
+        // Remove the initializer of a for loop.
+        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+    } else if (tint::Is<sem::ForLoopStatement>(sem_parent) &&
+               tint::As<ast::ForLoopStatement>(sem_parent->Declaration())->continuing ==
+                   statement_node) {
+        // Remove the "continuing" statement of a for loop.
+        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+    } else if (tint::Is<sem::LoopContinuingBlockStatement>(statement_sem_node)) {
+        // Remove the "continuing" block of a loop.
+        clone_context->Replace(statement_node, static_cast<const ast::Statement*>(nullptr));
+    } else if (tint::Is<ast::CaseStatement>(statement_node)) {
+        // Remove a case statement from its enclosing switch statement.
+        const std::vector<const ast::CaseStatement*>* case_statement_list =
+            &sem_parent->Declaration()->As<ast::SwitchStatement>()->body;
+        assert(std::find(case_statement_list->begin(), case_statement_list->end(),
+                         statement_node) != case_statement_list->end() &&
+               "Statement not found.");
+        clone_context->Remove(*case_statement_list, statement_node);
+    } else if (tint::Is<ast::BlockStatement>(statement_node)) {
+        // Remove a block statement from the block that encloses it. A special case is required for
+        // this, since a sem::Block has itself as its associated sem::Block, so it is necessary to
+        // look at the parent to get the enclosing block.
+        const auto& statement_list =
+            sem_parent->Declaration()->As<ast::BlockStatement>()->statements;
+        assert(std::find(statement_list.begin(), statement_list.end(), statement_node) !=
+                   statement_list.end() &&
+               "Statement not found.");
+        clone_context->Remove(statement_list, statement_node);
+    } else {
+        // Remove a non-block statement from the block that encloses it.
+        const auto& statement_list =
+            statement_sem_node->Block()->Declaration()->As<ast::BlockStatement>()->statements;
+        assert(std::find(statement_list.begin(), statement_list.end(), statement_node) !=
+                   statement_list.end() &&
+               "Statement not found.");
+        clone_context->Remove(statement_list, statement_node);
+    }
+}
+
+protobufs::Mutation MutationDeleteStatement::ToMessage() const {
+    protobufs::Mutation mutation;
+    *mutation.mutable_delete_statement() = message_;
+    return mutation;
+}
+
+bool MutationDeleteStatement::CanBeDeleted(const ast::Statement& statement_node,
+                                           const Program& program,
+                                           const JumpTracker& jump_tracker) {
+    if (statement_node.Is<ast::VariableDeclStatement>()) {
+        // This is conservative. It would be possible to delete variable declarations if they are
+        // not used. Further analysis could allow that.
+        return false;
+    }
+
+    if (jump_tracker.ContainsReturn(statement_node) ||
+        jump_tracker.ContainsIntraproceduralDiscard(statement_node)) {
+        // This is conservative. It would be possible to delete a return/discard statement as long
+        // as there is still a return/discard on every control flow path.
+        return false;
+    }
+
+    if (jump_tracker.ContainsBreakForInnermostLoop(statement_node)) {
+        // This is conservative. Disallowing the removal of breaks ensures that loops cannot become
+        // statically infinite. However, a loop might in practice have multiple breaks, some of
+        // which can be removed.
+        return false;
+    }
+
+    if (auto* case_statement = statement_node.As<ast::CaseStatement>()) {
+        // It is not OK to delete the final case statement in a switch statement if the penultimate
+        // case statement falls through to the final case statement.
+        auto* switch_statement =
+            program.Sem().Get(case_statement)->Parent()->Declaration()->As<ast::SwitchStatement>();
+
+        if (switch_statement->body.size() > 1 &&
+            switch_statement->body[switch_statement->body.size() - 1] == case_statement) {
+            // There are at least two case statements, and this is the final case statement.
+            auto& penultimate_case_statement_body_statements =
+                switch_statement->body[switch_statement->body.size() - 2]->body->statements;
+            if (penultimate_case_statement_body_statements.size() > 0 &&
+                penultimate_case_statement_body_statements
+                    [penultimate_case_statement_body_statements.size() - 1]
+                        ->Is<ast::FallthroughStatement>()) {
+                // The penultimate case statement falls through to the final case statement, thus
+                // the final case statement cannot be removed.
+                return false;
+            }
+        }
+    }
+
+    auto* parent_sem = program.Sem().Get(&statement_node)->Parent();
+    if (parent_sem == nullptr) {
+        // Semantic information for the parent node is required.
+        return false;
+    }
+
+    auto* parent_stmt = parent_sem->Declaration();
+
+    // It does not make sense to delete the entire body of a loop or if statement.
+    if (auto* for_loop = parent_stmt->As<ast::ForLoopStatement>()) {
+        if (for_loop->body == &statement_node) {
+            return false;
+        }
+    }
+    if (auto* loop = parent_stmt->As<ast::LoopStatement>()) {
+        if (loop->body == &statement_node) {
+            return false;
+        }
+    }
+    if (auto* while_loop = parent_stmt->As<ast::WhileStatement>()) {
+        if (while_loop->body == &statement_node) {
+            return false;
+        }
+    }
+    if (auto* if_statement = parent_stmt->As<ast::IfStatement>()) {
+        if (if_statement->body == &statement_node) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
+}  // namespace tint::fuzzers::ast_fuzzer
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h
new file mode 100644
index 0000000..c7c0192
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h
@@ -0,0 +1,75 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_DELETE_STATEMENT_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_DELETE_STATEMENT_H_
+
+#include "src/tint/ast/statement.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/jump_tracker.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+
+/// @see MutationDeleteStatement::Apply
+class MutationDeleteStatement : public Mutation {
+  public:
+    /// @brief Constructs an instance of this mutation from a protobuf message.
+    /// @param message - protobuf message
+    explicit MutationDeleteStatement(protobufs::MutationDeleteStatement message);
+
+    /// @brief Constructor.
+    /// @param statement_id - the id of the statement to delete.
+    explicit MutationDeleteStatement(uint32_t statement_id);
+
+    /// @copybrief Mutation::IsApplicable
+    ///
+    /// The mutation is applicable iff:
+    /// - `statement_id` corresponds to a statement in the AST.
+    /// - `statement_id` does not refer to a variable declaration, since the declared variables will
+    ///   be inaccessible if the statement is deleted.
+    /// - `statement_id` is not a return statement, since removing return statements arbitrarily can
+    ///   make the program invalid.
+    /// - `statement_id` is not a break statement, since removing break statements can lead to
+    ///   syntactically infinite loops.
+    ///
+    /// @copydetails Mutation::IsApplicable
+    bool IsApplicable(const tint::Program& program, const NodeIdMap& node_id_map) const override;
+
+    /// @copybrief Mutation::Apply
+    ///
+    /// Delete the statement referenced by `statement_id`.
+    ///
+    /// @copydetails Mutation::Apply
+    void Apply(const NodeIdMap& node_id_map,
+               tint::CloneContext* clone_context,
+               NodeIdMap* new_node_id_map) const override;
+
+    protobufs::Mutation ToMessage() const override;
+
+    /// Return whether the given statement is suitable for deletion.
+    /// @param statement_node - the statement to be considered for deletion.
+    /// @param program - the program containing  the statement.
+    /// @param jump_tracker - information about jump statements for the program.
+    /// @return true if and only if it is OK to delete the statement.
+    static bool CanBeDeleted(const ast::Statement& statement_node,
+                             const Program& program,
+                             const JumpTracker& jump_tracker);
+
+  private:
+    protobufs::MutationDeleteStatement message_;
+};
+
+}  // namespace tint::fuzzers::ast_fuzzer
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_DELETE_STATEMENT_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc
new file mode 100644
index 0000000..925ee80
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement_test.cc
@@ -0,0 +1,750 @@
+// Copyright 2021 The Tint Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/delete_statement.h"
+
+#include <functional>
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/ast/assignment_statement.h"
+#include "src/tint/ast/block_statement.h"
+#include "src/tint/ast/case_statement.h"
+#include "src/tint/ast/fallthrough_statement.h"
+#include "src/tint/ast/for_loop_statement.h"
+#include "src/tint/ast/if_statement.h"
+#include "src/tint/ast/switch_statement.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/probability_context.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint::fuzzers::ast_fuzzer {
+namespace {
+
+void CheckStatementDeletionWorks(
+    const std::string& original,
+    const std::string& expected,
+    const std::function<const ast::Statement*(const Program&)>& statement_finder) {
+    Source::File original_file("original.wgsl", original);
+    auto program = reader::wgsl::Parse(&original_file);
+
+    Source::File expected_file("expected.wgsl", expected);
+    auto expected_program = reader::wgsl::Parse(&expected_file);
+
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+    ASSERT_TRUE(expected_program.IsValid()) << expected_program.Diagnostics().str();
+
+    NodeIdMap node_id_map(program);
+    const auto* statement = statement_finder(program);
+    ASSERT_NE(statement, nullptr);
+    auto statement_id = node_id_map.GetId(statement);
+    ASSERT_NE(statement_id, 0);
+    ASSERT_TRUE(MaybeApplyMutation(program, MutationDeleteStatement(statement_id), node_id_map,
+                                   &program, &node_id_map, nullptr));
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+    writer::wgsl::Options options;
+    auto transformed_result = writer::wgsl::Generate(&program, options);
+    auto expected_result = writer::wgsl::Generate(&expected_program, options);
+    ASSERT_TRUE(transformed_result.success) << transformed_result.error;
+    ASSERT_TRUE(expected_result.success) << expected_result.error;
+    ASSERT_EQ(expected_result.wgsl, transformed_result.wgsl);
+}
+
+void CheckStatementDeletionNotAllowed(
+    const std::string& original,
+    const std::function<const ast::Statement*(const Program&)>& statement_finder) {
+    Source::File original_file("original.wgsl", original);
+    auto program = reader::wgsl::Parse(&original_file);
+
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    NodeIdMap node_id_map(program);
+    const auto* statement = statement_finder(program);
+    ASSERT_NE(statement, nullptr);
+    auto statement_id = node_id_map.GetId(statement);
+    ASSERT_NE(statement_id, 0);
+    ASSERT_FALSE(MaybeApplyMutation(program, MutationDeleteStatement(statement_id), node_id_map,
+                                    &program, &node_id_map, nullptr));
+}
+
+TEST(DeleteStatementTest, DeleteAssignStatement) {
+    auto original = R"(
+    fn main() {
+      {
+        var a : i32 = 5;
+        a = 6;
+      }
+    })";
+    auto expected = R"(fn main() {
+  {
+    var a : i32 = 5;
+  }
+}
+)";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::BlockStatement>()
+            ->statements[1]
+            ->As<ast::AssignmentStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteForStatement) {
+    auto original =
+        R"(
+    fn main() {
+      for (var i : i32 = 0; i < 10; i++) {
+      }
+    }
+  )";
+    auto expected = "fn main() { }";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::ForLoopStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteIfStatement) {
+    auto original =
+        R"(
+    fn main() {
+      if (true) { } else { }
+    }
+  )";
+    auto expected = "fn main() { }";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::IfStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteBlockStatement) {
+    auto original = "fn main() { { } }";
+    auto expected = "fn main() { }";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::BlockStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteSwitchStatement) {
+    auto original = R"(
+fn main() {
+  switch(1) {
+    case 0, 1: {
+    }
+    default: {
+      fallthrough;
+    }
+    case 2: {
+    }
+  }
+})";
+    auto expected = R"(fn main() { })";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::SwitchStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteCaseStatement) {
+    auto original = R"(
+fn main() {
+  switch(1) {
+    case 0, 1: {
+    }
+    default: {
+      fallthrough;
+    }
+    case 2: {
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  switch(1) {
+    default: {
+      fallthrough;
+    }
+    case 2: {
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::SwitchStatement>()
+            ->body[0]
+            ->As<ast::CaseStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteFallthroughStatement) {
+    auto original = R"(
+fn main() {
+  switch(1) {
+    case 0, 1: {
+    }
+    default: {
+      fallthrough;
+    }
+    case 2: {
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  switch(1) {
+    case 0, 1: {
+    }
+    default: {
+    }
+    case 2: {
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::SwitchStatement>()
+            ->body[1]
+            ->As<ast::CaseStatement>()
+            ->body->statements[0]
+            ->As<ast::FallthroughStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteElse) {
+    auto original = R"(
+fn main() {
+  if (true) {
+  } else {
+  }
+})";
+    auto expected = R"(
+fn main() {
+  if (true) {
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::IfStatement>()
+            ->else_statement;
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteCall) {
+    auto original = R"(
+fn main() {
+  sin(1.0);
+})";
+    auto expected = R"(
+fn main() {
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::CallStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteCompoundAssign) {
+    auto original = R"(
+fn main() {
+  var x : i32 = 0;
+  x += 2;;
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32 = 0;
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[1]
+            ->As<ast::CompoundAssignmentStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteLoop) {
+    auto original = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+    continuing {
+      x++;
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32 = 0;
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[1]->As<ast::LoopStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteContinuingBlock) {
+    auto original = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+    continuing {
+      x++;
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[1]
+            ->As<ast::LoopStatement>()
+            ->continuing;
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteContinue) {
+    auto original = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+    continue;
+    continuing {
+      x++;
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+    continuing {
+      x++;
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[1]
+            ->As<ast::LoopStatement>()
+            ->body->statements[1]
+            ->As<ast::ContinueStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteIncrement) {
+    auto original = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+    continuing {
+      x++;
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32 = 0;
+  loop {
+    if (x > 100) {
+      break;
+    }
+    continuing {
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[1]
+            ->As<ast::LoopStatement>()
+            ->continuing->statements[0]
+            ->As<ast::IncrementDecrementStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteForLoopInitializer) {
+    auto original = R"(
+fn main() {
+  var x : i32;
+  for (x = 0; x < 100; x++) {
+  }
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32;
+  for (; x < 100; x++) {
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[1]
+            ->As<ast::ForLoopStatement>()
+            ->initializer->As<ast::AssignmentStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DeleteForLoopContinuing) {
+    auto original = R"(
+fn main() {
+  var x : i32;
+  for (x = 0; x < 100; x++) {
+  }
+})";
+    auto expected = R"(
+fn main() {
+  var x : i32;
+  for (x = 0; x < 100;) {
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[1]
+            ->As<ast::ForLoopStatement>()
+            ->continuing->As<ast::IncrementDecrementStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, AllowDeletionOfInnerLoopWithBreak) {
+    auto original = R"(
+fn main() {
+  loop {
+    loop {
+      break;
+    }
+    break;
+  }
+})";
+    auto expected = R"(
+fn main() {
+  loop {
+    break;
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::LoopStatement>()
+            ->body->statements[0]
+            ->As<ast::LoopStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, AllowDeletionOfInnerCaseWithBreak) {
+    auto original = R"(
+fn main() {
+  loop {
+    switch(0) {
+      case 1: {
+        break;
+      }
+      default: {
+      }
+    }
+    break;
+  }
+})";
+    auto expected = R"(
+fn main() {
+  loop {
+    switch(0) {
+      default: {
+      }
+    }
+    break;
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::LoopStatement>()
+            ->body->statements[0]
+            ->As<ast::SwitchStatement>()
+            ->body[0];
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, AllowDeletionOfBreakFromSwitch) {
+    auto original = R"(
+fn main() {
+  switch(0) {
+    case 1: {
+      break;
+    }
+    default: {
+    }
+  }
+})";
+    auto expected = R"(
+fn main() {
+  switch(0) {
+    case 1: {
+    }
+    default: {
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::SwitchStatement>()
+            ->body[0]
+            ->body->statements[0]
+            ->As<ast::BreakStatement>();
+    };
+    CheckStatementDeletionWorks(original, expected, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotDeleteVariableDeclaration) {
+    auto original = R"(
+fn main() {
+  var x : i32;
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::VariableDeclStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotDeleteCaseDueToFallthrough) {
+    auto original = R"(
+fn main() {
+  switch(1) {
+    default: {
+      fallthrough;
+    }
+    case 2: {
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::SwitchStatement>()
+            ->body[1]
+            ->As<ast::CaseStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotMakeLoopInfinite1) {
+    auto original = R"(
+fn main() {
+  loop {
+    break;
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::LoopStatement>()
+            ->body->statements[0]
+            ->As<ast::BreakStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotMakeLoopInfinite2) {
+    auto original = R"(
+fn main() {
+  loop {
+    if (true) {
+      break;
+    }
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST()
+            .Functions()[0]
+            ->body->statements[0]
+            ->As<ast::LoopStatement>()
+            ->body->statements[0]
+            ->As<ast::IfStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveReturn) {
+    auto original = R"(
+fn main() {
+  return;
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::ReturnStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveStatementContainingReturn) {
+    auto original = R"(
+fn foo() -> i32 {
+  if (true) {
+    return 1;
+  } else {
+    return 2;
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::IfStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveDiscard) {
+    auto original = R"(
+fn main() {
+  discard;
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::DiscardStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveStatementContainingDiscard) {
+    auto original = R"(
+fn foo() -> i32 {
+  if (true) {
+    discard;
+  } else {
+    discard;
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::IfStatement>();
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveLoopBody) {
+    auto original = R"(
+fn foo() {
+  discard;
+}
+fn main() {
+  loop {
+    foo();
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[1]->body->statements[0]->As<ast::LoopStatement>()->body;
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveForLoopBody) {
+    auto original = R"(
+fn main() {
+  for(var i : i32 = 0; i < 10; i++) {
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::ForLoopStatement>()->body;
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveWhileBody) {
+    auto original = R"(
+fn main() {
+  var i : i32 = 0;
+  while(i < 10) {
+    i++;
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[1]->As<ast::WhileStatement>()->body;
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveIfBody) {
+    auto original = R"(
+fn main() {
+  if(true) {
+  }
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body->statements[0]->As<ast::IfStatement>()->body;
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+TEST(DeleteStatementTest, DoNotRemoveFunctionBody) {
+    auto original = R"(
+fn main() {
+})";
+    auto statement_finder = [](const Program& program) -> const ast::Statement* {
+        return program.AST().Functions()[0]->body;
+    };
+    CheckStatementDeletionNotAllowed(original, statement_finder);
+}
+
+}  // namespace
+}  // namespace tint::fuzzers::ast_fuzzer
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
index f70a947..7153f0c 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -22,6 +22,7 @@
 
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_unary_operators.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/delete_statements.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/wrap_unary_operators.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
@@ -48,6 +49,8 @@
                                                             probability_context, &result);
         MaybeAddFinder<MutationFinderChangeUnaryOperators>(enable_all_mutations,
                                                            probability_context, &result);
+        MaybeAddFinder<MutationFinderDeleteStatements>(enable_all_mutations, probability_context,
+                                                       &result);
         MaybeAddFinder<MutationFinderReplaceIdentifiers>(enable_all_mutations, probability_context,
                                                          &result);
         MaybeAddFinder<MutationFinderWrapUnaryOperators>(enable_all_mutations, probability_context,
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
index 639bd60..f2cb159 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
@@ -21,6 +21,7 @@
 
 const std::pair<uint32_t, uint32_t> kChanceOfChangingBinaryOperators = {30, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfChangingUnaryOperators = {30, 70};
+const std::pair<uint32_t, uint32_t> kChanceOfDeletingStatements = {30, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingIdentifiers = {30, 70};
 const std::pair<uint32_t, uint32_t> kChanceOfWrappingUnaryOperators = {30, 70};
 
@@ -30,6 +31,7 @@
     : generator_(generator),
       chance_of_changing_binary_operators_(RandomFromRange(kChanceOfChangingBinaryOperators)),
       chance_of_changing_unary_operators_(RandomFromRange(kChanceOfChangingUnaryOperators)),
+      chance_of_deleting_statements_(RandomFromRange(kChanceOfDeletingStatements)),
       chance_of_replacing_identifiers_(RandomFromRange(kChanceOfReplacingIdentifiers)),
       chance_of_wrapping_unary_operators_(RandomFromRange(kChanceOfWrappingUnaryOperators)) {
     assert(generator != nullptr && "generator must not be nullptr");
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
index ce46738..410d7cf 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
@@ -61,6 +61,9 @@
         return chance_of_changing_unary_operators_;
     }
 
+    /// @return the probability of changing operator for a binary expression.
+    uint32_t GetChanceOfDeletingStatements() const { return chance_of_deleting_statements_; }
+
     /// @return the probability of replacing some identifier with some other one.
     uint32_t GetChanceOfReplacingIdentifiers() const { return chance_of_replacing_identifiers_; }
 
@@ -78,6 +81,7 @@
 
     uint32_t chance_of_changing_binary_operators_;
     uint32_t chance_of_changing_unary_operators_;
+    uint32_t chance_of_deleting_statements_;
     uint32_t chance_of_replacing_identifiers_;
     uint32_t chance_of_wrapping_unary_operators_;
 };
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto b/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto
index fd3cd81..b1fa5e3 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto
+++ b/src/tint/fuzzers/tint_ast_fuzzer/protobufs/tint_ast_fuzzer.proto
@@ -22,6 +22,7 @@
     MutationChangeBinaryOperator change_binary_operator = 2;
     MutationWrapUnaryOperator wrap_unary_operator = 3;
     MutationChangeUnaryOperator change_unary_operator = 4;
+    MutationDeleteStatement delete_statement = 5;
   };
 }
 
@@ -64,6 +65,14 @@
     uint32 new_operator = 2;
 }
 
+message MutationDeleteStatement {
+    // This transformation deletes a statement, as long as doing so does not
+    // invalidate the program.
+
+    // The id of a statement to be deleted.
+    uint32 statement_id = 1;
+}
+
 message MutationReplaceIdentifier {
     // This transformation replaces a use of one variable with another.