AST fuzzer: change binary operator

A mutation and mutation finder that changes the operator in a binary
expression to something type-compatible.

Fixes: tint:1085
Change-Id: I2e35d3cdfdbcc52d4dc5981b187da217fc48e462
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/84640
Reviewed-by: Ryan Harrison <rharrison@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Alastair Donaldson <afdx@google.com>
Auto-Submit: Alastair Donaldson <afdx@google.com>
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn b/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
index 036ec8a..70941ac 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
+++ b/src/tint/fuzzers/tint_ast_fuzzer/BUILD.gn
@@ -46,8 +46,12 @@
       "mutation.h",
       "mutation_finder.cc",
       "mutation_finder.h",
+      "mutation_finders/change_binary_operators.cc",
+      "mutation_finders/change_binary_operators.h",
       "mutation_finders/replace_identifiers.cc",
       "mutation_finders/replace_identifiers.h",
+      "mutations/change_binary_operator.cc",
+      "mutations/change_binary_operator.h",
       "mutations/replace_identifier.cc",
       "mutations/replace_identifier.h",
       "mutator.cc",
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
index 757cd53..a31d411 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
+++ b/src/tint/fuzzers/tint_ast_fuzzer/CMakeLists.txt
@@ -40,7 +40,9 @@
         ../random_generator_engine.h
         mutation.h
         mutation_finder.h
+        mutation_finders/change_binary_operators.h
         mutation_finders/replace_identifiers.h
+        mutations/change_binary_operator.h
         mutations/replace_identifier.h
         mutator.h
         node_id_map.h
@@ -55,7 +57,9 @@
         ../random_generator_engine.cc
         mutation.cc
         mutation_finder.cc
+        mutation_finders/change_binary_operators.cc
         mutation_finders/replace_identifiers.cc
+        mutations/change_binary_operator.cc
         mutations/replace_identifier.cc
         mutator.cc
         node_id_map.cc
@@ -92,6 +96,7 @@
 # Add tests.
 if (${TINT_BUILD_TESTS})
     set(TEST_SOURCES
+            mutations/change_binary_operator_test.cc
             mutations/replace_identifier_test.cc)
 
     add_executable(tint_ast_fuzzer_unittests ${TEST_SOURCES})
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
index 63e00b3..1c4fb2e 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation.cc
@@ -16,6 +16,7 @@
 
 #include <cassert>
 
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
 
 namespace tint {
@@ -30,6 +31,9 @@
     case protobufs::Mutation::kReplaceIdentifier:
       return std::make_unique<MutationReplaceIdentifier>(
           message.replace_identifier());
+    case protobufs::Mutation::kChangeBinaryOperator:
+      return std::make_unique<MutationChangeBinaryOperator>(
+          message.change_binary_operator());
     case protobufs::Mutation::MUTATION_NOT_SET:
       assert(false && "Mutation is not set");
       break;
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.cc
new file mode 100644
index 0000000..dbfc36e
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.cc
@@ -0,0 +1,92 @@
+// 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/mutation_finders/change_binary_operators.h"
+
+#include <memory>
+#include <vector>
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+MutationList MutationFinderChangeBinaryOperators::FindMutations(
+    const tint::Program& program,
+    NodeIdMap* node_id_map,
+    ProbabilityContext* probability_context) const {
+  MutationList result;
+
+  // Go through each binary expression in the AST and add a mutation that
+  // replaces its operator with some other type-compatible operator.
+
+  const std::vector<ast::BinaryOp> all_binary_operators = {
+      ast::BinaryOp::kAnd,
+      ast::BinaryOp::kOr,
+      ast::BinaryOp::kXor,
+      ast::BinaryOp::kLogicalAnd,
+      ast::BinaryOp::kLogicalOr,
+      ast::BinaryOp::kEqual,
+      ast::BinaryOp::kNotEqual,
+      ast::BinaryOp::kLessThan,
+      ast::BinaryOp::kGreaterThan,
+      ast::BinaryOp::kLessThanEqual,
+      ast::BinaryOp::kGreaterThanEqual,
+      ast::BinaryOp::kShiftLeft,
+      ast::BinaryOp::kShiftRight,
+      ast::BinaryOp::kAdd,
+      ast::BinaryOp::kSubtract,
+      ast::BinaryOp::kMultiply,
+      ast::BinaryOp::kDivide,
+      ast::BinaryOp::kModulo};
+
+  for (const auto* node : program.ASTNodes().Objects()) {
+    const auto* binary_expr = As<ast::BinaryExpression>(node);
+    if (!binary_expr) {
+      continue;
+    }
+
+    // Get vector of all operators this could be replaced with.
+    std::vector<ast::BinaryOp> allowed_replacements;
+    for (auto candidate_op : all_binary_operators) {
+      if (MutationChangeBinaryOperator::CanReplaceBinaryOperator(
+              program, *binary_expr, candidate_op)) {
+        allowed_replacements.push_back(candidate_op);
+      }
+    }
+
+    if (!allowed_replacements.empty()) {
+      // Choose an available replacement operator at random.
+      const ast::BinaryOp replacement =
+          allowed_replacements[probability_context->GetRandomIndex(
+              allowed_replacements)];
+      // Add a mutation according to the chosen replacement.
+      result.push_back(std::make_unique<MutationChangeBinaryOperator>(
+          node_id_map->GetId(binary_expr), replacement));
+    }
+  }
+
+  return result;
+}
+
+uint32_t MutationFinderChangeBinaryOperators::GetChanceOfApplyingMutation(
+    ProbabilityContext* probability_context) const {
+  return probability_context->GetChanceOfChangingBinaryOperators();
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h
new file mode 100644
index 0000000..460196a
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h
@@ -0,0 +1,42 @@
+// 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_MUTATION_FINDERS_CHANGE_BINARY_OPERATORS_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_CHANGE_BINARY_OPERATORS_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finder.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// Looks for opportunities to apply `MutationChangeBinaryOperator`.
+///
+/// Concretely, for each binary expression in the module, tries to replace it
+/// with a different, type-compatible operator.
+class MutationFinderChangeBinaryOperators : 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 ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATION_FINDERS_CHANGE_BINARY_OPERATORS_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
new file mode 100644
index 0000000..174424a
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.cc
@@ -0,0 +1,528 @@
+// 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/mutations/change_binary_operator.h"
+
+#include <utility>
+
+#include "src/tint/sem/reference_type.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+namespace {
+
+bool IsSuitableForShift(const sem::Type* lhs_type, const sem::Type* rhs_type) {
+  // `a << b` requires b to be an unsigned scalar or vector, and `a` to be an
+  // integer scalar or vector with the same width as `b`. Similar for `a >> b`.
+
+  if (rhs_type->is_unsigned_integer_scalar()) {
+    return lhs_type->is_integer_scalar();
+  }
+  if (rhs_type->is_unsigned_integer_vector()) {
+    return lhs_type->is_unsigned_integer_vector();
+  }
+  return false;
+}
+
+bool CanReplaceAddSubtractWith(const sem::Type* lhs_type,
+                               const sem::Type* rhs_type,
+                               ast::BinaryOp new_operator) {
+  // The program is assumed to be well-typed, so this method determines when
+  // 'new_operator' can be used as a type-preserving replacement in an '+' or
+  // '-' expression.
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+      // '+' and '-' are fully type compatible.
+      return true;
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // These operators do not have a mixed vector-scalar form, and only work
+      // on integer types.
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kMultiply:
+      // '+' and '*' are largely type-compatible, but for matrices they are only
+      // type-compatible if the matrices are square.
+      return !lhs_type->is_float_matrix() || lhs_type->is_square_float_matrix();
+    case ast::BinaryOp::kDivide:
+      // '/' is not defined for matrices.
+      return lhs_type->is_numeric_scalar_or_vector() &&
+             rhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kModulo:
+      // TODO(https://crbug.com/tint/1370): once fixed, the rules should be the
+      //  same as for divide.
+      if (lhs_type->is_float_vector() || rhs_type->is_float_vector()) {
+        return lhs_type == rhs_type;
+      }
+      return !lhs_type->is_float_matrix() && !rhs_type->is_float_matrix();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceMultiplyWith(const sem::Type* lhs_type,
+                            const sem::Type* rhs_type,
+                            ast::BinaryOp new_operator) {
+  // The program is assumed to be well-typed, so this method determines when
+  // 'new_operator' can be used as a type-preserving replacement in a '*'
+  // expression.
+  switch (new_operator) {
+    case ast::BinaryOp::kMultiply:
+      return true;
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+      // '*' is type-compatible with '+' and '-' for square matrices, and for
+      // numeric scalars/vectors.
+      if (lhs_type->is_square_float_matrix() &&
+          rhs_type->is_square_float_matrix()) {
+        return true;
+      }
+      return lhs_type->is_numeric_scalar_or_vector() &&
+             rhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // These operators require homogeneous integer types.
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kDivide:
+      // '/' is not defined for matrices.
+      return lhs_type->is_numeric_scalar_or_vector() &&
+             rhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kModulo:
+      // TODO(https://crbug.com/tint/1370): once fixed, this should be the same
+      // as for divide
+      if (lhs_type->is_float_vector() || rhs_type->is_float_vector()) {
+        return lhs_type == rhs_type;
+      }
+      return !lhs_type->is_float_matrix() && !rhs_type->is_float_matrix();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceDivideWith(const sem::Type* lhs_type,
+                          const sem::Type* rhs_type,
+                          ast::BinaryOp new_operator) {
+  // The program is assumed to be well-typed, so this method determines when
+  // 'new_operator' can be used as a type-preserving replacement in a '/'
+  // expression.
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+      // These operators work in all contexts where '/' works.
+      return true;
+    case ast::BinaryOp::kModulo:
+      // TODO(https://crbug.com/tint/1370): this special case should not be
+      // required; modulo and divide should work in the same contexts.
+      return lhs_type->is_integer_scalar_or_vector() || lhs_type == rhs_type;
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // These operators require homogeneous integer types.
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+// TODO(https://crbug.com/tint/1370): once fixed, this method will be removed
+//  and the same method will be used to check Divide and Modulo.
+bool CanReplaceModuloWith(const sem::Type* lhs_type,
+                          const sem::Type* rhs_type,
+                          ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+      return true;
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      return lhs_type == rhs_type && lhs_type->is_integer_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceLogicalAndLogicalOrWith(ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      // These operators all work whenever '&&' and '||' work.
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceAndOrWith(const sem::Type* lhs_type,
+                         const sem::Type* rhs_type,
+                         ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+      // '&' and '|' work in all the same contexts.
+      return true;
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+    case ast::BinaryOp::kXor:
+      // '&' and '|' can be applied to booleans. In all other contexts,
+      // integer numeric operators work.
+      return !lhs_type->is_bool_scalar_or_vector();
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      // '&' and '|' can be applied to booleans, and for boolean scalar
+      // scalar contexts, their logical counterparts work.
+      return lhs_type->Is<sem::Bool>();
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      // '&' and '|' can be applied to booleans, and in these contexts equality
+      // comparison operators also work.
+      return lhs_type->is_bool_scalar_or_vector();
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceXorWith(const sem::Type* lhs_type,
+                       const sem::Type* rhs_type,
+                       ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // '^' only works on integer types, and in any such context, all other
+      // integer operators also work.
+      return true;
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return IsSuitableForShift(lhs_type, rhs_type);
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceShiftLeftShiftRightWith(const sem::Type* lhs_type,
+                                       const sem::Type* rhs_type,
+                                       ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      // These operators are type-compatible.
+      return true;
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+    case ast::BinaryOp::kMultiply:
+    case ast::BinaryOp::kDivide:
+    case ast::BinaryOp::kModulo:
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+    case ast::BinaryOp::kXor:
+      // Shift operators allow mixing of signed and unsigned arguments, but in
+      // the case where the arguments are homogeneous, they are type-compatible
+      // with other numeric operators.
+      return lhs_type == rhs_type;
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceEqualNotEqualWith(const sem::Type* lhs_type,
+                                 ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      // These operators are type-compatible.
+      return true;
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+      // An equality comparison between numeric types can be changed to an
+      // ordered comparison.
+      return lhs_type->is_numeric_scalar_or_vector();
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      // An equality comparison between boolean scalars can be turned into a
+      // logical operation.
+      return lhs_type->Is<sem::Bool>();
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+      // An equality comparison between boolean scalars or vectors can be turned
+      // into a component-wise non-short-circuit logical operation.
+      return lhs_type->is_bool_scalar_or_vector();
+    default:
+      return false;
+  }
+}
+
+bool CanReplaceLessThanLessThanEqualGreaterThanGreaterThanEqualWith(
+    ast::BinaryOp new_operator) {
+  switch (new_operator) {
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+      // Ordered comparison operators can be interchanged, and equality
+      // operators can be used in their place.
+      return true;
+    default:
+      return false;
+  }
+}
+}  // namespace
+
+MutationChangeBinaryOperator::MutationChangeBinaryOperator(
+    protobufs::MutationChangeBinaryOperator message)
+    : message_(std::move(message)) {}
+
+MutationChangeBinaryOperator::MutationChangeBinaryOperator(
+    uint32_t binary_expr_id,
+    ast::BinaryOp new_operator) {
+  message_.set_binary_expr_id(binary_expr_id);
+  message_.set_new_operator(static_cast<uint32_t>(new_operator));
+}
+
+bool MutationChangeBinaryOperator::CanReplaceBinaryOperator(
+    const Program& program,
+    const ast::BinaryExpression& binary_expr,
+    ast::BinaryOp new_operator) {
+  if (new_operator == binary_expr.op) {
+    // An operator should not be replaced with itself, as this would be a no-op.
+    return false;
+  }
+
+  // Get the types of the operators.
+  const auto* lhs_type = program.Sem().Get(binary_expr.lhs)->Type();
+  const auto* rhs_type = program.Sem().Get(binary_expr.rhs)->Type();
+
+  // If these are reference types, unwrap them to get the pointee type.
+  const sem::Type* lhs_basic_type =
+      lhs_type->Is<sem::Reference>()
+          ? lhs_type->As<sem::Reference>()->StoreType()
+          : lhs_type;
+  const sem::Type* rhs_basic_type =
+      rhs_type->Is<sem::Reference>()
+          ? rhs_type->As<sem::Reference>()->StoreType()
+          : rhs_type;
+
+  switch (binary_expr.op) {
+    case ast::BinaryOp::kAdd:
+    case ast::BinaryOp::kSubtract:
+      return CanReplaceAddSubtractWith(lhs_basic_type, rhs_basic_type,
+                                       new_operator);
+    case ast::BinaryOp::kMultiply:
+      return CanReplaceMultiplyWith(lhs_basic_type, rhs_basic_type,
+                                    new_operator);
+    case ast::BinaryOp::kDivide:
+      return CanReplaceDivideWith(lhs_basic_type, rhs_basic_type, new_operator);
+    case ast::BinaryOp::kModulo:
+      return CanReplaceModuloWith(lhs_basic_type, rhs_basic_type, new_operator);
+    case ast::BinaryOp::kAnd:
+    case ast::BinaryOp::kOr:
+      return CanReplaceAndOrWith(lhs_basic_type, rhs_basic_type, new_operator);
+    case ast::BinaryOp::kXor:
+      return CanReplaceXorWith(lhs_basic_type, rhs_basic_type, new_operator);
+    case ast::BinaryOp::kShiftLeft:
+    case ast::BinaryOp::kShiftRight:
+      return CanReplaceShiftLeftShiftRightWith(lhs_basic_type, rhs_basic_type,
+                                               new_operator);
+    case ast::BinaryOp::kLogicalAnd:
+    case ast::BinaryOp::kLogicalOr:
+      return CanReplaceLogicalAndLogicalOrWith(new_operator);
+    case ast::BinaryOp::kEqual:
+    case ast::BinaryOp::kNotEqual:
+      return CanReplaceEqualNotEqualWith(lhs_basic_type, new_operator);
+    case ast::BinaryOp::kLessThan:
+    case ast::BinaryOp::kLessThanEqual:
+    case ast::BinaryOp::kGreaterThan:
+    case ast::BinaryOp::kGreaterThanEqual:
+    case ast::BinaryOp::kNone:
+      return CanReplaceLessThanLessThanEqualGreaterThanGreaterThanEqualWith(
+          new_operator);
+      assert(false && "Unreachable");
+      return false;
+  }
+}
+
+bool MutationChangeBinaryOperator::IsApplicable(
+    const Program& program,
+    const NodeIdMap& node_id_map) const {
+  const auto* binary_expr_node =
+      As<ast::BinaryExpression>(node_id_map.GetNode(message_.binary_expr_id()));
+  if (binary_expr_node == nullptr) {
+    // Either the id does not exist, or does not correspond to a binary
+    // expression.
+    return false;
+  }
+  // Check whether the replacement is acceptable.
+  const auto new_operator = static_cast<ast::BinaryOp>(message_.new_operator());
+  return CanReplaceBinaryOperator(program, *binary_expr_node, new_operator);
+}
+
+void MutationChangeBinaryOperator::Apply(const NodeIdMap& node_id_map,
+                                         CloneContext* clone_context,
+                                         NodeIdMap* new_node_id_map) const {
+  // Get the node whose operator is to be replaced.
+  const auto* binary_expr_node =
+      As<ast::BinaryExpression>(node_id_map.GetNode(message_.binary_expr_id()));
+
+  // Clone the binary expression, with the appropriate new operator.
+  const ast::BinaryExpression* cloned_replacement;
+  switch (static_cast<ast::BinaryOp>(message_.new_operator())) {
+    case ast::BinaryOp::kAnd:
+      cloned_replacement =
+          clone_context->dst->And(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kOr:
+      cloned_replacement =
+          clone_context->dst->Or(clone_context->Clone(binary_expr_node->lhs),
+                                 clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kXor:
+      cloned_replacement =
+          clone_context->dst->Xor(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLogicalAnd:
+      cloned_replacement = clone_context->dst->LogicalAnd(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLogicalOr:
+      cloned_replacement = clone_context->dst->LogicalOr(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kEqual:
+      cloned_replacement = clone_context->dst->Equal(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kNotEqual:
+      cloned_replacement = clone_context->dst->NotEqual(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLessThan:
+      cloned_replacement = clone_context->dst->LessThan(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kGreaterThan:
+      cloned_replacement = clone_context->dst->GreaterThan(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kLessThanEqual:
+      cloned_replacement = clone_context->dst->LessThanEqual(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kGreaterThanEqual:
+      cloned_replacement = clone_context->dst->GreaterThanEqual(
+          clone_context->Clone(binary_expr_node->lhs),
+          clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kShiftLeft:
+      cloned_replacement =
+          clone_context->dst->Shl(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kShiftRight:
+      cloned_replacement =
+          clone_context->dst->Shr(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kAdd:
+      cloned_replacement =
+          clone_context->dst->Add(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kSubtract:
+      cloned_replacement =
+          clone_context->dst->Sub(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kMultiply:
+      cloned_replacement =
+          clone_context->dst->Mul(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kDivide:
+      cloned_replacement =
+          clone_context->dst->Div(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kModulo:
+      cloned_replacement =
+          clone_context->dst->Mod(clone_context->Clone(binary_expr_node->lhs),
+                                  clone_context->Clone(binary_expr_node->rhs));
+      break;
+    case ast::BinaryOp::kNone:
+      cloned_replacement = nullptr;
+      assert(false && "Unreachable");
+  }
+  // Set things up so that the original binary expression will be replaced with
+  // its clone, and update the id mapping.
+  clone_context->Replace(binary_expr_node, cloned_replacement);
+  new_node_id_map->Add(cloned_replacement, message_.binary_expr_id());
+}
+
+protobufs::Mutation MutationChangeBinaryOperator::ToMessage() const {
+  protobufs::Mutation mutation;
+  *mutation.mutable_change_binary_operator() = message_;
+  return mutation;
+}
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
new file mode 100644
index 0000000..73ac22b
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator.h
@@ -0,0 +1,85 @@
+// 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_MUTATIONS_CHANGE_BINARY_OPERATOR_H_
+#define SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_CHANGE_BINARY_OPERATOR_H_
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation.h"
+
+#include "src/tint/ast/binary_expression.h"
+#include "src/tint/program.h"
+#include "src/tint/sem/variable.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+
+/// @see MutationChangeBinaryOperator::Apply
+class MutationChangeBinaryOperator : public Mutation {
+ public:
+  /// @brief Constructs an instance of this mutation from a protobuf message.
+  /// @param message - protobuf message
+  explicit MutationChangeBinaryOperator(
+      protobufs::MutationChangeBinaryOperator message);
+
+  /// @brief Constructor.
+  /// @param binary_expr_id - the id of a binary expression.
+  /// @param new_operator - a new binary operator to replace the one used in the
+  /// expression.
+  MutationChangeBinaryOperator(uint32_t binary_expr_id,
+                               ast::BinaryOp new_operator);
+
+  /// @copybrief Mutation::IsApplicable
+  ///
+  /// The mutation is applicable iff:
+  /// - `binary_expr_id` is a valid id of an `ast::BinaryExpression`.
+  /// - `new_operator` is type-compatible with the arguments of the binary
+  /// expression.
+  ///
+  /// @copydetails Mutation::IsApplicable
+  bool IsApplicable(const tint::Program& program,
+                    const NodeIdMap& node_id_map) const override;
+
+  /// @copybrief Mutation::Apply
+  ///
+  /// Replaces binary operator in the binary expression corresponding to
+  /// `binary_expr_id` with `new_operator`.
+  ///
+  /// @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;
+
+  /// @brief Determines whether replacing the operator of a binary expression
+  ///     with another operator would preserve well-typedness.
+  /// @param program - the program that owns the binary expression.
+  /// @param binary_expr - the binary expression being considered for mutation.
+  /// @param new_operator - a new binary operator to be checked as a candidate
+  ///     replacement for the binary expression's operator.
+  /// @return `true` if and only if the replacement would be well-typed.
+  static bool CanReplaceBinaryOperator(const Program& program,
+                                       const ast::BinaryExpression& binary_expr,
+                                       ast::BinaryOp new_operator);
+
+ private:
+  protobufs::MutationChangeBinaryOperator message_;
+};
+
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
+
+#endif  // SRC_TINT_FUZZERS_TINT_AST_FUZZER_MUTATIONS_CHANGE_BINARY_OPERATOR_H_
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
new file mode 100644
index 0000000..1120d6f
--- /dev/null
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/change_binary_operator_test.cc
@@ -0,0 +1,749 @@
+// 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/mutations/change_binary_operator.h"
+
+#include <string>
+#include <unordered_set>
+#include <vector>
+
+#include "gtest/gtest.h"
+
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
+#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
+#include "src/tint/program_builder.h"
+#include "src/tint/reader/wgsl/parser.h"
+#include "src/tint/writer/wgsl/generator.h"
+
+namespace tint {
+namespace fuzzers {
+namespace ast_fuzzer {
+namespace {
+
+std::string OpToString(ast::BinaryOp op) {
+  switch (op) {
+    case ast::BinaryOp::kNone:
+      assert(false && "Unreachable");
+      return "";
+    case ast::BinaryOp::kAnd:
+      return "&";
+    case ast::BinaryOp::kOr:
+      return "|";
+    case ast::BinaryOp::kXor:
+      return "^";
+    case ast::BinaryOp::kLogicalAnd:
+      return "&&";
+    case ast::BinaryOp::kLogicalOr:
+      return "||";
+    case ast::BinaryOp::kEqual:
+      return "==";
+    case ast::BinaryOp::kNotEqual:
+      return "!=";
+    case ast::BinaryOp::kLessThan:
+      return "<";
+    case ast::BinaryOp::kGreaterThan:
+      return ">";
+    case ast::BinaryOp::kLessThanEqual:
+      return "<=";
+    case ast::BinaryOp::kGreaterThanEqual:
+      return ">=";
+    case ast::BinaryOp::kShiftLeft:
+      return "<<";
+    case ast::BinaryOp::kShiftRight:
+      return ">>";
+    case ast::BinaryOp::kAdd:
+      return "+";
+    case ast::BinaryOp::kSubtract:
+      return "-";
+    case ast::BinaryOp::kMultiply:
+      return "*";
+    case ast::BinaryOp::kDivide:
+      return "/";
+    case ast::BinaryOp::kModulo:
+      return "%";
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, NotApplicable_Simple) {
+  std::string content = R"(
+    fn main() {
+      let a : i32 = 1 + 2;
+    }
+  )";
+  Source::File file("test.wgsl", content);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_stmts = program.AST().Functions()[0]->body->statements;
+
+  const auto* a_var =
+      main_fn_stmts[0]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(a_var, nullptr);
+
+  auto a_var_id = node_id_map.GetId(a_var);
+
+  const auto* sum_expr = a_var->constructor->As<ast::BinaryExpression>();
+  ASSERT_NE(sum_expr, nullptr);
+
+  auto sum_expr_id = node_id_map.GetId(sum_expr);
+  ASSERT_NE(sum_expr_id, 0);
+
+  // binary_expr_id is invalid.
+  EXPECT_FALSE(MutationChangeBinaryOperator(0, ast::BinaryOp::kSubtract)
+                   .IsApplicable(program, node_id_map));
+
+  // binary_expr_id is not a binary expression.
+  EXPECT_FALSE(MutationChangeBinaryOperator(a_var_id, ast::BinaryOp::kSubtract)
+                   .IsApplicable(program, node_id_map));
+
+  // new_operator is applicable to the argument types.
+  EXPECT_FALSE(MutationChangeBinaryOperator(0, ast::BinaryOp::kLogicalAnd)
+                   .IsApplicable(program, node_id_map));
+
+  // new_operator does not have the right result type.
+  EXPECT_FALSE(MutationChangeBinaryOperator(0, ast::BinaryOp::kLessThan)
+                   .IsApplicable(program, node_id_map));
+}
+
+TEST(ChangeBinaryOperatorTest, Applicable_Simple) {
+  std::string shader = R"(fn main() {
+  let a : i32 = (1 + 2);
+}
+)";
+  Source::File file("test.wgsl", shader);
+  auto program = reader::wgsl::Parse(&file);
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  NodeIdMap node_id_map(program);
+
+  const auto& main_fn_stmts = program.AST().Functions()[0]->body->statements;
+
+  const auto* a_var =
+      main_fn_stmts[0]->As<ast::VariableDeclStatement>()->variable;
+  ASSERT_NE(a_var, nullptr);
+
+  const auto* sum_expr = a_var->constructor->As<ast::BinaryExpression>();
+  ASSERT_NE(sum_expr, nullptr);
+
+  auto sum_expr_id = node_id_map.GetId(sum_expr);
+  ASSERT_NE(sum_expr_id, 0);
+
+  ASSERT_TRUE(MaybeApplyMutation(
+      program,
+      MutationChangeBinaryOperator(sum_expr_id, ast::BinaryOp::kSubtract),
+      node_id_map, &program, &node_id_map, nullptr));
+  ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+  writer::wgsl::Options options;
+  auto result = writer::wgsl::Generate(&program, options);
+  ASSERT_TRUE(result.success) << result.error;
+
+  std::string expected_shader = R"(fn main() {
+  let a : i32 = (1 - 2);
+}
+)";
+  ASSERT_EQ(expected_shader, result.wgsl);
+}
+
+void CheckMutations(
+    const std::string& lhs_type,
+    const std::string& rhs_type,
+    const std::string& result_type,
+    ast::BinaryOp original_operator,
+    const std::unordered_set<ast::BinaryOp>& allowed_replacement_operators) {
+  std::stringstream shader;
+  shader << "fn foo(a : " << lhs_type << ", b : " << rhs_type + ") {\n"
+         << "  let r : " << result_type
+         << " = (a " + OpToString(original_operator) << " b);\n}\n";
+
+  const std::vector<ast::BinaryOp> all_operators = {
+      ast::BinaryOp::kAnd,
+      ast::BinaryOp::kOr,
+      ast::BinaryOp::kXor,
+      ast::BinaryOp::kLogicalAnd,
+      ast::BinaryOp::kLogicalOr,
+      ast::BinaryOp::kEqual,
+      ast::BinaryOp::kNotEqual,
+      ast::BinaryOp::kLessThan,
+      ast::BinaryOp::kGreaterThan,
+      ast::BinaryOp::kLessThanEqual,
+      ast::BinaryOp::kGreaterThanEqual,
+      ast::BinaryOp::kShiftLeft,
+      ast::BinaryOp::kShiftRight,
+      ast::BinaryOp::kAdd,
+      ast::BinaryOp::kSubtract,
+      ast::BinaryOp::kMultiply,
+      ast::BinaryOp::kDivide,
+      ast::BinaryOp::kModulo};
+
+  for (auto new_operator : all_operators) {
+    Source::File file("test.wgsl", shader.str());
+    auto program = reader::wgsl::Parse(&file);
+    ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+    NodeIdMap node_id_map(program);
+
+    const auto& stmts = program.AST().Functions()[0]->body->statements;
+
+    const auto* r_var = stmts[0]->As<ast::VariableDeclStatement>()->variable;
+    ASSERT_NE(r_var, nullptr);
+
+    const auto* binary_expr = r_var->constructor->As<ast::BinaryExpression>();
+    ASSERT_NE(binary_expr, nullptr);
+
+    auto binary_expr_id = node_id_map.GetId(binary_expr);
+    ASSERT_NE(binary_expr_id, 0);
+
+    MutationChangeBinaryOperator mutation(binary_expr_id, new_operator);
+
+    std::stringstream expected_shader;
+    expected_shader << "fn foo(a : " << lhs_type << ", b : " << rhs_type
+                    << ") {\n"
+                    << "  let r : " << result_type << " = (a "
+                    << OpToString(new_operator) << " b);\n}\n";
+
+    if (allowed_replacement_operators.count(new_operator) == 0) {
+      ASSERT_FALSE(mutation.IsApplicable(program, node_id_map));
+      if (new_operator != binary_expr->op) {
+        Source::File invalid_file("test.wgsl", expected_shader.str());
+        auto invalid_program = reader::wgsl::Parse(&invalid_file);
+        ASSERT_FALSE(invalid_program.IsValid()) << program.Diagnostics().str();
+      }
+    } else {
+      ASSERT_TRUE(MaybeApplyMutation(program, mutation, node_id_map, &program,
+                                     &node_id_map, nullptr));
+      ASSERT_TRUE(program.IsValid()) << program.Diagnostics().str();
+
+      writer::wgsl::Options options;
+      auto result = writer::wgsl::Generate(&program, options);
+      ASSERT_TRUE(result.success) << result.error;
+
+      ASSERT_EQ(expected_shader.str(), result.wgsl);
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, AddSubtract) {
+  for (auto op : {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract}) {
+    const ast::BinaryOp other_op = op == ast::BinaryOp::kAdd
+                                       ? ast::BinaryOp::kSubtract
+                                       : ast::BinaryOp::kAdd;
+    for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+      CheckMutations(
+          type, type, type, op,
+          {other_op, ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide,
+           ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+           ast::BinaryOp::kXor});
+    }
+    for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+      CheckMutations(
+          type, type, type, op,
+          {other_op, ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide,
+           ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+           ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+           ast::BinaryOp::kShiftRight});
+    }
+    for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+      CheckMutations(type, type, type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+      std::string scalar_type = "i32";
+      CheckMutations(vector_type, scalar_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+      CheckMutations(scalar_type, vector_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+      std::string scalar_type = "u32";
+      CheckMutations(vector_type, scalar_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+      CheckMutations(scalar_type, vector_type, vector_type, op,
+                     {other_op, ast::BinaryOp::kMultiply,
+                      ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    }
+    for (std::string vector_type : {"vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+      std::string scalar_type = "f32";
+      CheckMutations(
+          vector_type, scalar_type, vector_type, op,
+          {
+              other_op, ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide
+              // TODO(https://crbug.com/tint/1370): once fixed, add kModulo
+          });
+      CheckMutations(
+          scalar_type, vector_type, vector_type, op,
+          {
+              other_op, ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide
+              // TODO(https://crbug.com/tint/1370): once fixed, add kModulo
+          });
+    }
+    for (std::string square_matrix_type :
+         {"mat2x2<f32>", "mat3x3<f32>", "mat4x4<f32>"}) {
+      CheckMutations(square_matrix_type, square_matrix_type, square_matrix_type,
+                     op, {other_op, ast::BinaryOp::kMultiply});
+    }
+    for (std::string non_square_matrix_type :
+         {"mat2x3<f32>", "mat2x4<f32>", "mat3x2<f32>", "mat3x4<f32>",
+          "mat4x2<f32>", "mat4x3<f32>"}) {
+      CheckMutations(non_square_matrix_type, non_square_matrix_type,
+                     non_square_matrix_type, op, {other_op});
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, Mul) {
+  for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kMultiply,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract, ast::BinaryOp::kDivide,
+         ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+         ast::BinaryOp::kXor});
+  }
+  for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kMultiply,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract, ast::BinaryOp::kDivide,
+         ast::BinaryOp::kModulo, ast::BinaryOp::kAnd, ast::BinaryOp::kOr,
+         ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+         ast::BinaryOp::kShiftRight});
+  }
+  for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    CheckMutations(type, type, type, ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    std::string scalar_type = "i32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    std::string scalar_type = "u32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kDivide, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    std::string scalar_type = "f32";
+    CheckMutations(
+        vector_type, scalar_type, vector_type, ast::BinaryOp::kMultiply,
+        {
+            ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+            ast::BinaryOp::kDivide
+            // TODO(https://crbug.com/tint/1370): once fixed, add kModulo
+        });
+    CheckMutations(
+        scalar_type, vector_type, vector_type, ast::BinaryOp::kMultiply,
+        {
+            ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+            ast::BinaryOp::kDivide
+            // TODO(https://crbug.com/tint/1370): once fixed, add kModulo
+        });
+  }
+  for (std::string square_matrix_type :
+       {"mat2x2<f32>", "mat3x3<f32>", "mat4x4<f32>"}) {
+    CheckMutations(square_matrix_type, square_matrix_type, square_matrix_type,
+                   ast::BinaryOp::kMultiply,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract});
+  }
+
+  CheckMutations("vec2<f32>", "mat2x2<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec2<f32>", "mat3x2<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec2<f32>", "mat4x2<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat2x2<f32>", "vec2<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x2<f32>", "mat3x2<f32>", "mat3x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x2<f32>", "mat4x2<f32>", "mat4x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat2x3<f32>", "vec2<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x3<f32>", "mat2x2<f32>", "mat2x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x3<f32>", "mat3x2<f32>", "mat3x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x3<f32>", "mat4x2<f32>", "mat4x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat2x4<f32>", "vec2<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x4<f32>", "mat2x2<f32>", "mat2x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x4<f32>", "mat3x2<f32>", "mat3x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat2x4<f32>", "mat4x2<f32>", "mat4x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("vec3<f32>", "mat2x3<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec3<f32>", "mat3x3<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec3<f32>", "mat4x3<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat3x2<f32>", "vec3<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x2<f32>", "mat2x3<f32>", "mat2x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x2<f32>", "mat3x3<f32>", "mat3x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x2<f32>", "mat4x3<f32>", "mat4x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat3x3<f32>", "vec3<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x3<f32>", "mat2x3<f32>", "mat2x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x3<f32>", "mat4x3<f32>", "mat4x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat3x4<f32>", "vec3<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x4<f32>", "mat2x3<f32>", "mat2x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x4<f32>", "mat3x3<f32>", "mat3x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat3x4<f32>", "mat4x3<f32>", "mat4x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("vec4<f32>", "mat2x4<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec4<f32>", "mat3x4<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("vec4<f32>", "mat4x4<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat4x2<f32>", "vec4<f32>", "vec2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x2<f32>", "mat2x4<f32>", "mat2x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x2<f32>", "mat3x4<f32>", "mat3x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x2<f32>", "mat4x4<f32>", "mat4x2<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat4x3<f32>", "vec4<f32>", "vec3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x3<f32>", "mat2x4<f32>", "mat2x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x3<f32>", "mat3x4<f32>", "mat3x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x3<f32>", "mat4x4<f32>", "mat4x3<f32>",
+                 ast::BinaryOp::kMultiply, {});
+
+  CheckMutations("mat4x4<f32>", "vec4<f32>", "vec4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x4<f32>", "mat2x4<f32>", "mat2x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+  CheckMutations("mat4x4<f32>", "mat3x4<f32>", "mat3x4<f32>",
+                 ast::BinaryOp::kMultiply, {});
+}
+
+TEST(ChangeBinaryOperatorTest, Divide) {
+  for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kDivide,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor});
+  }
+  for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kDivide,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+         ast::BinaryOp::kShiftRight});
+  }
+  for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    CheckMutations(type, type, type, ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    std::string scalar_type = "i32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    std::string scalar_type = "u32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kDivide,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kModulo});
+  }
+  for (std::string vector_type : {"vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    std::string scalar_type = "f32";
+    CheckMutations(
+        vector_type, scalar_type, vector_type, ast::BinaryOp::kDivide,
+        {
+            ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+            ast::BinaryOp::kMultiply
+            // TODO(https://crbug.com/tint/1370): once fixed, add kModulo
+        });
+    CheckMutations(
+        scalar_type, vector_type, vector_type, ast::BinaryOp::kDivide,
+        {
+            ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+            ast::BinaryOp::kMultiply
+            // TODO(https://crbug.com/tint/1370): once fixed, add kModulo
+        });
+  }
+}
+
+// TODO(https://crbug.com/tint/1370): once fixed, combine this with the Divide
+// test
+TEST(ChangeBinaryOperatorTest, Modulo) {
+  for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kModulo,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor});
+  }
+  for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    CheckMutations(
+        type, type, type, ast::BinaryOp::kModulo,
+        {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+         ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide, ast::BinaryOp::kAnd,
+         ast::BinaryOp::kOr, ast::BinaryOp::kXor, ast::BinaryOp::kShiftLeft,
+         ast::BinaryOp::kShiftRight});
+  }
+  for (std::string type : {"f32", "vec2<f32>", "vec3<f32>", "vec4<f32>"}) {
+    CheckMutations(type, type, type, ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+  }
+  for (std::string vector_type : {"vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+    std::string scalar_type = "i32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+  }
+  for (std::string vector_type : {"vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+    std::string scalar_type = "u32";
+    CheckMutations(vector_type, scalar_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+    CheckMutations(scalar_type, vector_type, vector_type,
+                   ast::BinaryOp::kModulo,
+                   {ast::BinaryOp::kAdd, ast::BinaryOp::kSubtract,
+                    ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide});
+  }
+  // TODO(https://crbug.com/tint/1370): mixed float scalars/vectors will be
+  // added when this test is combined with the Divide test
+}
+
+TEST(ChangeBinaryOperatorTest, AndOrXor) {
+  for (auto op :
+       {ast::BinaryOp::kAnd, ast::BinaryOp::kOr, ast::BinaryOp::kXor}) {
+    std::unordered_set<ast::BinaryOp> allowed_replacement_operators_signed{
+        ast::BinaryOp::kAdd,      ast::BinaryOp::kSubtract,
+        ast::BinaryOp::kMultiply, ast::BinaryOp::kDivide,
+        ast::BinaryOp::kModulo,   ast::BinaryOp::kAnd,
+        ast::BinaryOp::kOr,       ast::BinaryOp::kXor};
+    allowed_replacement_operators_signed.erase(op);
+    for (std::string type : {"i32", "vec2<i32>", "vec3<i32>", "vec4<i32>"}) {
+      CheckMutations(type, type, type, op,
+                     allowed_replacement_operators_signed);
+    }
+    std::unordered_set<ast::BinaryOp> allowed_replacement_operators_unsigned{
+        ast::BinaryOp::kAdd,        ast::BinaryOp::kSubtract,
+        ast::BinaryOp::kMultiply,   ast::BinaryOp::kDivide,
+        ast::BinaryOp::kModulo,     ast::BinaryOp::kShiftLeft,
+        ast::BinaryOp::kShiftRight, ast::BinaryOp::kAnd,
+        ast::BinaryOp::kOr,         ast::BinaryOp::kXor};
+    allowed_replacement_operators_unsigned.erase(op);
+    for (std::string type : {"u32", "vec2<u32>", "vec3<u32>", "vec4<u32>"}) {
+      CheckMutations(type, type, type, op,
+                     allowed_replacement_operators_unsigned);
+    }
+    if (op != ast::BinaryOp::kXor) {
+      for (std::string type :
+           {"bool", "vec2<bool>", "vec3<bool>", "vec4<bool>"}) {
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators_bool{
+            ast::BinaryOp::kAnd, ast::BinaryOp::kOr, ast::BinaryOp::kEqual,
+            ast::BinaryOp::kNotEqual};
+        allowed_replacement_operators_bool.erase(op);
+        if (type == "bool") {
+          allowed_replacement_operators_bool.insert(ast::BinaryOp::kLogicalAnd);
+          allowed_replacement_operators_bool.insert(ast::BinaryOp::kLogicalOr);
+        }
+        CheckMutations(type, type, type, op,
+                       allowed_replacement_operators_bool);
+      }
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, EqualNotEqual) {
+  for (auto op : {ast::BinaryOp::kEqual, ast::BinaryOp::kNotEqual}) {
+    for (std::string element_type : {"i32", "u32", "f32"}) {
+      for (size_t element_count = 1; element_count <= 4; element_count++) {
+        std::stringstream argument_type;
+        std::stringstream result_type;
+        if (element_count == 1) {
+          argument_type << element_type;
+          result_type << "bool";
+        } else {
+          argument_type << "vec" << element_count << "<" << element_type << ">";
+          result_type << "vec" << element_count << "<bool>";
+        }
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+            ast::BinaryOp::kLessThan,    ast::BinaryOp::kLessThanEqual,
+            ast::BinaryOp::kGreaterThan, ast::BinaryOp::kGreaterThanEqual,
+            ast::BinaryOp::kEqual,       ast::BinaryOp::kNotEqual};
+        allowed_replacement_operators.erase(op);
+        CheckMutations(argument_type.str(), argument_type.str(),
+                       result_type.str(), op, allowed_replacement_operators);
+      }
+    }
+    {
+      std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+          ast::BinaryOp::kLogicalAnd, ast::BinaryOp::kLogicalOr,
+          ast::BinaryOp::kAnd,        ast::BinaryOp::kOr,
+          ast::BinaryOp::kEqual,      ast::BinaryOp::kNotEqual};
+      allowed_replacement_operators.erase(op);
+      CheckMutations("bool", "bool", "bool", op, allowed_replacement_operators);
+    }
+    for (size_t element_count = 2; element_count <= 4; element_count++) {
+      std::stringstream argument_and_result_type;
+      argument_and_result_type << "vec" << element_count << "<bool>";
+      std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+          ast::BinaryOp::kAnd, ast::BinaryOp::kOr, ast::BinaryOp::kEqual,
+          ast::BinaryOp::kNotEqual};
+      allowed_replacement_operators.erase(op);
+      CheckMutations(
+          argument_and_result_type.str(), argument_and_result_type.str(),
+          argument_and_result_type.str(), op, allowed_replacement_operators);
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest,
+     LessThanLessThanEqualGreaterThanGreaterThanEqual) {
+  for (auto op :
+       {ast::BinaryOp::kLessThan, ast::BinaryOp::kLessThanEqual,
+        ast::BinaryOp::kGreaterThan, ast::BinaryOp::kGreaterThanEqual}) {
+    for (std::string element_type : {"i32", "u32", "f32"}) {
+      for (size_t element_count = 1; element_count <= 4; element_count++) {
+        std::stringstream argument_type;
+        std::stringstream result_type;
+        if (element_count == 1) {
+          argument_type << element_type;
+          result_type << "bool";
+        } else {
+          argument_type << "vec" << element_count << "<" << element_type << ">";
+          result_type << "vec" << element_count << "<bool>";
+        }
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+            ast::BinaryOp::kLessThan,    ast::BinaryOp::kLessThanEqual,
+            ast::BinaryOp::kGreaterThan, ast::BinaryOp::kGreaterThanEqual,
+            ast::BinaryOp::kEqual,       ast::BinaryOp::kNotEqual};
+        allowed_replacement_operators.erase(op);
+        CheckMutations(argument_type.str(), argument_type.str(),
+                       result_type.str(), op, allowed_replacement_operators);
+      }
+    }
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, LogicalAndLogicalOr) {
+  for (auto op : {ast::BinaryOp::kLogicalAnd, ast::BinaryOp::kLogicalOr}) {
+    std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+        ast::BinaryOp::kLogicalAnd, ast::BinaryOp::kLogicalOr,
+        ast::BinaryOp::kAnd,        ast::BinaryOp::kOr,
+        ast::BinaryOp::kEqual,      ast::BinaryOp::kNotEqual};
+    allowed_replacement_operators.erase(op);
+    CheckMutations("bool", "bool", "bool", op, allowed_replacement_operators);
+  }
+}
+
+TEST(ChangeBinaryOperatorTest, ShiftLeftShiftRight) {
+  for (auto op : {ast::BinaryOp::kShiftLeft, ast::BinaryOp::kShiftRight}) {
+    for (std::string lhs_element_type : {"i32", "u32"}) {
+      for (size_t element_count = 1; element_count <= 4; element_count++) {
+        std::stringstream lhs_and_result_type;
+        std::stringstream rhs_type;
+        if (element_count == 1) {
+          lhs_and_result_type << lhs_element_type;
+          rhs_type << "u32";
+        } else {
+          lhs_and_result_type << "vec" << element_count << "<"
+                              << lhs_element_type << ">";
+          rhs_type << "vec" << element_count << "<u32>";
+        }
+        std::unordered_set<ast::BinaryOp> allowed_replacement_operators{
+            ast::BinaryOp::kShiftLeft, ast::BinaryOp::kShiftRight};
+        allowed_replacement_operators.erase(op);
+        if (lhs_element_type == "u32") {
+          allowed_replacement_operators.insert(ast::BinaryOp::kAdd);
+          allowed_replacement_operators.insert(ast::BinaryOp::kSubtract);
+          allowed_replacement_operators.insert(ast::BinaryOp::kMultiply);
+          allowed_replacement_operators.insert(ast::BinaryOp::kDivide);
+          allowed_replacement_operators.insert(ast::BinaryOp::kModulo);
+          allowed_replacement_operators.insert(ast::BinaryOp::kAnd);
+          allowed_replacement_operators.insert(ast::BinaryOp::kOr);
+          allowed_replacement_operators.insert(ast::BinaryOp::kXor);
+        }
+        CheckMutations(lhs_and_result_type.str(), rhs_type.str(),
+                       lhs_and_result_type.str(), op,
+                       allowed_replacement_operators);
+      }
+    }
+  }
+}
+
+}  // namespace
+}  // namespace ast_fuzzer
+}  // namespace fuzzers
+}  // namespace tint
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
index 6ffc40a..bdb730c 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier_test.cc
@@ -12,17 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
+
 #include <string>
 
 #include "gtest/gtest.h"
 
-#include "src/tint/fuzzers/tint_ast_fuzzer/mutations/replace_identifier.h"
-#include "src/tint/fuzzers/tint_ast_fuzzer/mutator.h"
-#include "src/tint/fuzzers/tint_ast_fuzzer/probability_context.h"
-
-#include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
-
 #include "src/tint/ast/call_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/program_builder.h"
 #include "src/tint/reader/wgsl/parser.h"
 #include "src/tint/writer/wgsl/generator.h"
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
index 26ce653..63d62e2 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutator.cc
@@ -20,6 +20,7 @@
 #include <utility>
 #include <vector>
 
+#include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/change_binary_operators.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/mutation_finders/replace_identifiers.h"
 #include "src/tint/fuzzers/tint_ast_fuzzer/node_id_map.h"
 
@@ -45,6 +46,8 @@
     bool enable_all_mutations) {
   MutationFinderList result;
   do {
+    MaybeAddFinder<MutationFinderChangeBinaryOperators>(
+        enable_all_mutations, probability_context, &result);
     MaybeAddFinder<MutationFinderReplaceIdentifiers>(
         enable_all_mutations, probability_context, &result);
   } while (result.empty());
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
index 515078c..f41f5b9 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.cc
@@ -21,12 +21,15 @@
 namespace ast_fuzzer {
 namespace {
 
+const std::pair<uint32_t, uint32_t> kChanceOfChangingBinaryOperators = {30, 90};
 const std::pair<uint32_t, uint32_t> kChanceOfReplacingIdentifiers = {30, 70};
 
 }  // namespace
 
 ProbabilityContext::ProbabilityContext(RandomGenerator* generator)
     : generator_(generator),
+      chance_of_changing_binary_operators_(
+          RandomFromRange(kChanceOfChangingBinaryOperators)),
       chance_of_replacing_identifiers_(
           RandomFromRange(kChanceOfReplacingIdentifiers)) {
   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 0d6393a..b890aba 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
+++ b/src/tint/fuzzers/tint_ast_fuzzer/probability_context.h
@@ -55,6 +55,11 @@
     return static_cast<size_t>(generator_->GetUInt64(arr.size()));
   }
 
+  /// @return the probability of replacing some binary operator with another.
+  uint32_t GetChanceOfChangingBinaryOperators() const {
+    return chance_of_changing_binary_operators_;
+  }
+
   /// @return the probability of replacing some identifier with some other one.
   uint32_t GetChanceOfReplacingIdentifiers() const {
     return chance_of_replacing_identifiers_;
@@ -67,6 +72,7 @@
 
   RandomGenerator* generator_;
 
+  uint32_t chance_of_changing_binary_operators_;
   uint32_t chance_of_replacing_identifiers_;
 };
 
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 3682a0c..e60e35d 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
@@ -17,7 +17,10 @@
 package tint.fuzzers.ast_fuzzer.protobufs;
 
 message Mutation {
-  oneof mutation { MutationReplaceIdentifier replace_identifier = 1; };
+  oneof mutation {
+    MutationReplaceIdentifier replace_identifier = 1;
+    MutationChangeBinaryOperator change_binary_operator = 2;
+  };
 }
 
 message MutationSequence {
@@ -46,3 +49,13 @@
   // The id of a definition of a variable to replace the use with.
   uint32 replacement_id = 2;
 }
+
+message MutationChangeBinaryOperator {
+  // This transformation replaces one binary operator with another.
+
+  // The id of a binary expression in the AST.
+  uint32 binary_expr_id = 1;
+
+  // A BinaryOp representing the new binary operator.
+  uint32 new_operator = 2;
+}
diff --git a/src/tint/program_builder.h b/src/tint/program_builder.h
index c6a5403..1d40cb1 100644
--- a/src/tint/program_builder.h
+++ b/src/tint/program_builder.h
@@ -1723,7 +1723,7 @@
   /// @param rhs the right hand argument to the division operation
   /// @returns a `ast::BinaryExpression` dividing `lhs` by `rhs`
   template <typename LHS, typename RHS>
-  const ast::Expression* Div(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Div(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kDivide,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1733,7 +1733,7 @@
   /// @param rhs the right hand argument to the modulo operation
   /// @returns a `ast::BinaryExpression` applying modulo of `lhs` by `rhs`
   template <typename LHS, typename RHS>
-  const ast::Expression* Mod(LHS&& lhs, RHS&& rhs) {
+  const ast::BinaryExpression* Mod(LHS&& lhs, RHS&& rhs) {
     return create<ast::BinaryExpression>(ast::BinaryOp::kModulo,
                                          Expr(std::forward<LHS>(lhs)),
                                          Expr(std::forward<RHS>(rhs)));
@@ -1769,6 +1769,26 @@
                                          Expr(std::forward<RHS>(rhs)));
   }
 
+  /// @param lhs the left hand argument to the logical and operation
+  /// @param rhs the right hand argument to the logical and operation
+  /// @returns a `ast::BinaryExpression` of `lhs` && `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* LogicalAnd(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kLogicalAnd,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
+  /// @param lhs the left hand argument to the logical or operation
+  /// @param rhs the right hand argument to the logical or operation
+  /// @returns a `ast::BinaryExpression` of `lhs` || `rhs`
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* LogicalOr(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kLogicalOr,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
   /// @param lhs the left hand argument to the greater than operation
   /// @param rhs the right hand argument to the greater than operation
   /// @returns a `ast::BinaryExpression` of `lhs` > `rhs`
@@ -1819,6 +1839,17 @@
                                          Expr(std::forward<RHS>(rhs)));
   }
 
+  /// @param lhs the left hand argument to the not-equal expression
+  /// @param rhs the right hand argument to the not-equal expression
+  /// @returns a `ast::BinaryExpression` comparing `lhs` equal to `rhs` for
+  ///          disequality
+  template <typename LHS, typename RHS>
+  const ast::BinaryExpression* NotEqual(LHS&& lhs, RHS&& rhs) {
+    return create<ast::BinaryExpression>(ast::BinaryOp::kNotEqual,
+                                         Expr(std::forward<LHS>(lhs)),
+                                         Expr(std::forward<RHS>(rhs)));
+  }
+
   /// @param source the source information
   /// @param obj the object for the index accessor expression
   /// @param idx the index argument for the index accessor expression
diff --git a/src/tint/sem/type.cc b/src/tint/sem/type.cc
index 88dbf14..c14ec69 100644
--- a/src/tint/sem/type.cc
+++ b/src/tint/sem/type.cc
@@ -80,6 +80,12 @@
   return Is([](const Matrix* m) { return m->type()->is_float_scalar(); });
 }
 
+bool Type::is_square_float_matrix() const {
+  return Is([](const Matrix* m) {
+    return m->type()->is_float_scalar() && m->rows() == m->columns();
+  });
+}
+
 bool Type::is_float_vector() const {
   return Is([](const Vector* v) { return v->type()->is_float_scalar(); });
 }
diff --git a/src/tint/sem/type.h b/src/tint/sem/type.h
index ae30e08..c0abce3 100644
--- a/src/tint/sem/type.h
+++ b/src/tint/sem/type.h
@@ -77,6 +77,8 @@
   bool is_float_scalar() const;
   /// @returns true if this type is a float matrix
   bool is_float_matrix() const;
+  /// @returns true if this type is a square float matrix
+  bool is_square_float_matrix() const;
   /// @returns true if this type is a float vector
   bool is_float_vector() const;
   /// @returns true if this type is a float scalar or vector