Import Tint changes from Dawn

Changes:
  - 3b8b9699d69f1eeb6fadddcbe56ffc68ae57715e tint: Implement const eval of unary minus by Antonio Maiorano <amaiorano@google.com>
  - eb02cd33012e1a461109b13c38335db9e711355e Regex fuzzer: replace function calls with builtins by Alastair F. Donaldson <alastair.donaldson@imperial.ac.uk>
  - ca21fa019fa6e706320661a35e78ddcc6132384c Update dictionary for fuzzing by Alastair F. Donaldson <alastair.donaldson@imperial.ac.uk>
  - 46c32d882d6d7a83e6c11e0b986a3f9d91200f1b Early out Is checks. by dan sinclair <dsinclair@chromium.org>
  - 633cdf41cc40ff97619093c0784a87cd65a2b187 tint/resolver: Make the F16 ban error message show a sing... by Corentin Wallez <cwallez@chromium.org>
  - e4df87fd017cd1d3eacbfbed57e521f6f7733936 Regex fuzzer: Add break and continue statements by Alastair F. Donaldson <alastair.donaldson@imperial.ac.uk>
  - 84f7830874fc28c173448f8291ad8c2571fda573 Regex fuzzer: replace operators by Alastair F. Donaldson <alastair.donaldson@imperial.ac.uk>
  - 056f97a9e53215398dfbd9145714e678c872a070 tint: Add a benchmark for atan2 const eval by Ben Clayton <bclayton@google.com>
  - 853cbadc8fa6c5eaa2c6d2d55a5f10b6a9ab94d4 Clean regex fuzzer API by Alastair F. Donaldson <alastair.donaldson@imperial.ac.uk>
GitOrigin-RevId: 3b8b9699d69f1eeb6fadddcbe56ffc68ae57715e
Change-Id: I6a489442f5fff4d39396cc9ced675564730d2aa7
Reviewed-on: https://dawn-review.googlesource.com/c/tint/+/96660
Commit-Queue: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/src/tint/ast/f16.h b/src/tint/ast/f16.h
index c444a20..bae6291 100644
--- a/src/tint/ast/f16.h
+++ b/src/tint/ast/f16.h
@@ -22,7 +22,7 @@
 namespace tint::ast {
 
 /// A float 16 type
-class F16 : public Castable<F16, Type> {
+class F16 final : public Castable<F16, Type> {
   public:
     /// Constructor
     /// @param pid the identifier of the program that owns this node
diff --git a/src/tint/ast/int_literal_expression.h b/src/tint/ast/int_literal_expression.h
index b4e184a..10cbbee 100644
--- a/src/tint/ast/int_literal_expression.h
+++ b/src/tint/ast/int_literal_expression.h
@@ -20,7 +20,7 @@
 namespace tint::ast {
 
 /// An integer literal. The literal may have an 'i', 'u' or no suffix.
-class IntLiteralExpression : public Castable<IntLiteralExpression, LiteralExpression> {
+class IntLiteralExpression final : public Castable<IntLiteralExpression, LiteralExpression> {
   public:
     /// Literal suffix
     enum class Suffix {
diff --git a/src/tint/bench/benchmark.h b/src/tint/bench/benchmark.h
index d52a0d6..6801585 100644
--- a/src/tint/bench/benchmark.h
+++ b/src/tint/bench/benchmark.h
@@ -58,6 +58,7 @@
 /// files in `<tint>/test/benchmark`.
 #define TINT_BENCHMARK_WGSL_PROGRAMS(FUNC)                                   \
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "animometer.wgsl");                    \
+    TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "atan2-const-eval.wgsl");              \
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "bloom-vertical-blur.wgsl");           \
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "cluster-lights.wgsl");                \
     TINT_BENCHMARK_WGSL_PROGRAM(FUNC, "empty.wgsl");                         \
diff --git a/src/tint/castable.h b/src/tint/castable.h
index c7b8608..806d0f0 100644
--- a/src/tint/castable.h
+++ b/src/tint/castable.h
@@ -101,6 +101,36 @@
     /// The type hash code bitwise-or'd with all ancestor's hashcodes.
     const HashCode full_hashcode;
 
+    /// @returns true if `type` derives from the class `TO`
+    /// @param object the object type to test from, which must be, or derive from
+    /// type `FROM`.
+    /// @see CastFlags
+    template <typename TO, typename FROM, int FLAGS = 0>
+    static inline bool Is(const tint::TypeInfo* object) {
+        constexpr const bool downcast = std::is_base_of<FROM, TO>::value;
+        constexpr const bool upcast = std::is_base_of<TO, FROM>::value;
+        constexpr const bool nocast = std::is_same<FROM, TO>::value;
+        constexpr const bool assert_is_castable = (FLAGS & kDontErrorOnImpossibleCast) == 0;
+
+        static_assert(upcast || downcast || nocast || !assert_is_castable, "impossible cast");
+
+        return upcast || nocast || object->Is<TO>();
+    }
+
+    /// @returns true if this type derives from the class `T`
+    template <typename T>
+    inline bool Is() const {
+        auto* type = &Of<std::remove_cv_t<T>>();
+
+        if constexpr (std::is_final_v<T>) {
+            // T is final, so nothing can derive from T.
+            // We do not need to check ancestors, only whether this type is equal to the type T.
+            return type == this;
+        } else {
+            return Is(type);
+        }
+    }
+
     /// @param type the test type info
     /// @returns true if the class with this TypeInfo is of, or derives from the
     /// class with the given TypeInfo.
@@ -112,8 +142,8 @@
             return false;
         }
 
-        // Walk the base types, starting with this TypeInfo, to see if any of the
-        // pointers match `type`.
+        // Walk the base types, starting with this TypeInfo, to see if any of the pointers match
+        // `type`.
         for (auto* ti = this; ti != nullptr; ti = ti->base) {
             if (ti == type) {
                 return true;
@@ -122,26 +152,6 @@
         return false;
     }
 
-    /// @returns true if `type` derives from the class `TO`
-    /// @param type the object type to test from, which must be, or derive from
-    /// type `FROM`.
-    /// @see CastFlags
-    template <typename TO, typename FROM, int FLAGS = 0>
-    static inline bool Is(const tint::TypeInfo* type) {
-        constexpr const bool downcast = std::is_base_of<FROM, TO>::value;
-        constexpr const bool upcast = std::is_base_of<TO, FROM>::value;
-        constexpr const bool nocast = std::is_same<FROM, TO>::value;
-        constexpr const bool assert_is_castable = (FLAGS & kDontErrorOnImpossibleCast) == 0;
-
-        static_assert(upcast || downcast || nocast || !assert_is_castable, "impossible cast");
-
-        if (upcast || nocast) {
-            return true;
-        }
-
-        return type->Is(&Of<std::remove_cv_t<TO>>());
-    }
-
     /// @returns the static TypeInfo for the type T
     template <typename T>
     static const TypeInfo& Of() {
@@ -211,14 +221,12 @@
         if constexpr (kCount == 0) {
             return false;
         } else if constexpr (kCount == 1) {
-            return Is(&Of<std::tuple_element_t<0, TUPLE>>());
+            return Is<std::tuple_element_t<0, TUPLE>>();
         } else if constexpr (kCount == 2) {
-            return Is(&Of<std::tuple_element_t<0, TUPLE>>()) ||
-                   Is(&Of<std::tuple_element_t<1, TUPLE>>());
+            return Is<std::tuple_element_t<0, TUPLE>>() || Is<std::tuple_element_t<1, TUPLE>>();
         } else if constexpr (kCount == 3) {
-            return Is(&Of<std::tuple_element_t<0, TUPLE>>()) ||
-                   Is(&Of<std::tuple_element_t<1, TUPLE>>()) ||
-                   Is(&Of<std::tuple_element_t<2, TUPLE>>());
+            return Is<std::tuple_element_t<0, TUPLE>>() || Is<std::tuple_element_t<1, TUPLE>>() ||
+                   Is<std::tuple_element_t<2, TUPLE>>();
         } else {
             // Optimization: Compare the object's hashcode to the bitwise-or of all
             // the tested type's hashcodes. If there's no intersection of bits in
@@ -587,7 +595,7 @@
         // Attempt to dynamically cast the object to the handler type. If that
         // succeeds, call the case handler with the cast object.
         using CaseType = SwitchCaseType<CaseFunc>;
-        if (type->Is(&TypeInfo::Of<CaseType>())) {
+        if (type->Is<CaseType>()) {
             auto* ptr = static_cast<CaseType*>(object);
             if constexpr (kHasReturnType) {
                 new (result) RETURN_TYPE(static_cast<RETURN_TYPE>(std::get<0>(cases)(ptr)));
diff --git a/src/tint/fuzzers/dictionary.txt b/src/tint/fuzzers/dictionary.txt
index 8f15ff6..005735f 100644
--- a/src/tint/fuzzers/dictionary.txt
+++ b/src/tint/fuzzers/dictionary.txt
@@ -1,63 +1,165 @@
+"!"
+"!="
+"%"
+"%="
 "&"
 "&&"
-"->"
-"[["
-"]]"
-"/"
-"!"
-"["
-"]"
-"{"
-"}"
-":"
+"&="
+"("
+")"
+"*"
+"*="
+"+"
+"++"
+"+="
 ","
+"-"
+"--"
+"-="
+"->"
+"."
+"/"
+"/="
+":"
+";"
+"<"
+"<<"
+"<<="
+"<="
 "="
 "=="
 ">"
 ">="
-"<"
-"<="
-"%"
-"-"
-"::"
-"!="
-"."
-"+"
-"|"
-"||"
-"("
-")"
-";"
-"*"
+">>"
+">>="
+"@"
+"["
+"]"
 "^"
+"^="
+"_"
+"{"
+"|"
+"|="
+"||"
+"}"
+"~"
+"a"
+"abs"
+"acos"
+"acosh"
+"@align"
+"all"
+"any"
 "array"
-"binding"
+"arrayLength"
+"asin"
+"asinh"
+"atan"
+"atan2"
+"atanh"
+"atomic"
+"atomicAdd"
+"atomicAnd"
+"atomicLoad"
+"atomicMax"
+"atomicMin"
+"atomicOr"
+"atomicStore"
+"atomicSub"
+"atomicXor"
+"b"
+"@binding"
 "bitcast"
 "bool"
-"block"
 "break"
-"builtin"
+"@builtin"
+"@builtin(frag_depth)"
+"@builtin(front_facing)"
+"@builtin(global_invocation_id)"
+"@builtin(instance_index)"
+"@builtin(local_invocation_id)"
+"@builtin(local_invocation_index)"
+"@builtin(num_workgroups)"
+"@builtin(position)"
+"@builtin(sample_index)"
+"@builtin(sample_mask)"
+"@builtin(vertex_index)"
+"@builtin(workgroup_id)"
 "case"
-"compute"
+"ceil"
+"center"
+"centroid"
+"clamp"
+"@compute"
+"@const"
 "const"
 "continue"
 "continuing"
-"discard"
+"cos"
+"cosh"
+"countLeadingZeros"
+"countOneBits"
+"countTrailingZeros"
+"cross"
 "default"
+"degrees"
+"determinant"
+"discard"
+"distance"
+"dot"
+"dpdx"
+"dpdxCoarse"
+"dpdxFine"
+"dpdy"
+"dpdyCoarse"
+"dpdyFine"
 "else"
-"elseif"
+"enable"
+"exp"
+"exp2"
+"extractBits"
+"f16"
 "f32"
+"faceForward"
 "fallthrough"
 "false"
+"firstLeadingBit"
+"firstTrailingBit"
+"flat"
+"floor"
+"fma"
 "fn"
-"fragment"
+"for"
+"fract"
+"frag_depth"
+"@fragment"
+"frexp"
+"front_facing"
 "function"
+"fwidth"
+"fwidthCoarse"
+"fwidthFine"
+"g"
+"global_invocation_id"
+"@group"
 "i32"
+"@id"
 "if"
-"image"
-"import"
-"in"
-"location"
+"insertBits"
+"instance_index"
+"@interpolate"
+"@invariant"
+"inverseSqrt"
+"ldexp"
+"length"
+"let"
+"linear"
+"local_invocation_id"
+"local_invocation_index"
+"@location"
+"log"
+"log2"
 "loop"
 "mat2x2"
 "mat2x3"
@@ -68,45 +170,124 @@
 "mat4x2"
 "mat4x3"
 "mat4x4"
-"offset"
-"out"
+"max"
+"min"
+"mix"
+"modf"
+"normalize"
+"num_workgroups"
+"override"
+"pack2x16float"
+"pack2x16snorm"
+"pack2x16unorm"
+"pack4x8snorm"
+"pack4x8unorm"
+"perspective"
+"position"
+"pow"
 "private"
 "ptr"
+"quantizeToF16"
+"r"
+"r32float"
+"r32sint"
+"r32uint"
+"radians"
+"read"
+"read_write"
+"reflect"
+"refract"
 "return"
+"reverseBits"
+"rg32float"
+"rg32sint"
+"rg32uint"
+"rgba16float"
+"rgba16sint"
+"rgba16uint"
+"rgba32float"
+"rgba32sint"
+"rgba32uint"
+"rgba8sint"
+"rgba8snorm"
+"rgba8uint"
+"rgba8unorm"
+"round"
+"sample"
+"sample_index"
+"sample_mask"
 "sampler"
 "sampler_comparison"
-"set"
+"saturate"
+"select"
+"sign"
+"sin"
+"sinh"
+"@size"
+"smoothstep"
+"sqrt"
+"staticAssert"
+"step"
 "storage"
-"stage"
-"stride"
+"storageBarrier"
 "struct"
 "switch"
-"texture_depth_2d"
-"texture_depth_2d_array"
-"texture_depth_cube"
-"texture_depth_cube_array"
-"texture_depth_multisampled_2d"
-"texture_multisampled_2d"
-"texture_storage_1d"
-"texture_storage_2d_array"
-"texture_storage_2d"
-"texture_storage_2d_array"
-"texture_storage_3d"
+"tan"
+"tanh"
 "texture_1d"
 "texture_2d"
 "texture_2d_array"
 "texture_3d"
 "texture_cube"
 "texture_cube_array"
+"texture_depth_2d"
+"texture_depth_2d_array"
+"texture_depth_cube"
+"texture_depth_cube_array"
+"texture_depth_multisampled_2d"
+"textureDimensions"
+"textureGather"
+"textureGatherCompare"
+"textureLoad"
+"texture_multisampled_2d"
+"textureNumLayers"
+"textureNumLevels"
+"textureNumSamples"
+"textureSample"
+"textureSampleBias"
+"textureSampleCompare"
+"textureSampleCompareLevel"
+"textureSampleGrad"
+"textureSampleLevel"
+"texture_storage_1d"
+"texture_storage_2d"
+"texture_storage_2d_array"
+"texture_storage_3d"
+"textureStore"
+"transpose"
 "true"
+"trunc"
 "type"
 "u32"
 "uniform"
+"unpack2x16float"
+"unpack2x16snorm"
+"unpack2x16unorm"
+"unpack4x8snorm"
+"unpack4x8unorm"
 "var"
 "vec2"
 "vec3"
 "vec4"
-"vertex"
-"void"
+"@vertex"
+"vertex_index"
+"w"
+"while"
 "workgroup"
-"workgroup_size"
+"workgroupBarrier"
+"workgroup_id"
+"@workgroup_size"
+"write"
+"x"
+"y"
+"z"
diff --git a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
index 8b52732..d6612f5 100644
--- a/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
+++ b/src/tint/fuzzers/tint_ast_fuzzer/mutations/wrap_unary_operator.cc
@@ -18,6 +18,8 @@
 #include <vector>
 
 #include "src/tint/program_builder.h"
+#include "src/tint/sem/abstract_float.h"
+#include "src/tint/sem/abstract_int.h"
 
 namespace tint::fuzzers::ast_fuzzer {
 
@@ -98,7 +100,8 @@
         return {ast::UnaryOp::kNot};
     }
 
-    if (expr_type->is_signed_scalar_or_vector()) {
+    if (expr_type->is_signed_scalar_or_vector() ||
+        expr_type->is_abstract_integer_scalar_or_vector()) {
         return {ast::UnaryOp::kNegation, ast::UnaryOp::kComplement};
     }
 
@@ -106,7 +109,7 @@
         return {ast::UnaryOp::kComplement};
     }
 
-    if (expr_type->is_float_scalar_or_vector()) {
+    if (expr_type->is_float_scalar_or_vector() || expr_type->is_abstract_float_scalar_or_vector()) {
         return {ast::UnaryOp::kNegation};
     }
 
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc b/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc
index ac34684..ea1aea1 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc
+++ b/src/tint/fuzzers/tint_regex_fuzzer/fuzzer.cc
@@ -37,6 +37,9 @@
     kReplaceIdentifier,
     kReplaceLiteral,
     kInsertReturnStatement,
+    kReplaceOperator,
+    kInsertBreakOrContinue,
+    kReplaceFunctionCallWithBuiltin,
     kNumMutationKinds
 };
 
@@ -64,43 +67,60 @@
     MutationKind mutation_kind = static_cast<MutationKind>(
         generator.GetUInt32(static_cast<uint32_t>(MutationKind::kNumMutationKinds)));
 
+    WgslMutator mutator(generator);
     switch (mutation_kind) {
         case MutationKind::kSwapIntervals:
-            if (!SwapRandomIntervals(delimiter, wgsl_code, generator)) {
+            if (!mutator.SwapRandomIntervals(delimiter, wgsl_code)) {
                 return 0;
             }
             break;
 
         case MutationKind::kDeleteInterval:
-            if (!DeleteRandomInterval(delimiter, wgsl_code, generator)) {
+            if (!mutator.DeleteRandomInterval(delimiter, wgsl_code)) {
                 return 0;
             }
             break;
 
         case MutationKind::kDuplicateInterval:
-            if (!DuplicateRandomInterval(delimiter, wgsl_code, generator)) {
+            if (!mutator.DuplicateRandomInterval(delimiter, wgsl_code)) {
                 return 0;
             }
             break;
 
         case MutationKind::kReplaceIdentifier:
-            if (!ReplaceRandomIdentifier(wgsl_code, generator)) {
+            if (!mutator.ReplaceRandomIdentifier(wgsl_code)) {
                 return 0;
             }
             break;
 
         case MutationKind::kReplaceLiteral:
-            if (!ReplaceRandomIntLiteral(wgsl_code, generator)) {
+            if (!mutator.ReplaceRandomIntLiteral(wgsl_code)) {
                 return 0;
             }
             break;
 
         case MutationKind::kInsertReturnStatement:
-            if (!InsertReturnStatement(wgsl_code, generator)) {
+            if (!mutator.InsertReturnStatement(wgsl_code)) {
                 return 0;
             }
             break;
 
+        case MutationKind::kReplaceOperator:
+            if (!mutator.ReplaceRandomOperator(wgsl_code)) {
+                return 0;
+            }
+            break;
+
+        case MutationKind::kInsertBreakOrContinue:
+            if (!mutator.InsertBreakOrContinue(wgsl_code)) {
+                return 0;
+            }
+            break;
+        case MutationKind::kReplaceFunctionCallWithBuiltin:
+            if (!mutator.ReplaceFunctionCallWithBuiltin(wgsl_code)) {
+                return 0;
+            }
+            break;
         default:
             assert(false && "Unreachable");
             return 0;
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc b/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc
index 891a111..9e0b1a0 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc
+++ b/src/tint/fuzzers/tint_regex_fuzzer/regex_fuzzer_tests.cc
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <optional>
 #include <string>
 
 #include "gtest/gtest.h"
@@ -21,26 +22,47 @@
 namespace tint::fuzzers::regex_fuzzer {
 namespace {
 
+class WgslMutatorTest : public WgslMutator {
+  public:
+    explicit WgslMutatorTest(RandomGenerator& generator) : WgslMutator(generator) {}
+
+    using WgslMutator::DeleteInterval;
+    using WgslMutator::DuplicateInterval;
+    using WgslMutator::FindClosingBrace;
+    using WgslMutator::FindOperatorOccurrence;
+    using WgslMutator::GetFunctionBodyPositions;
+    using WgslMutator::GetFunctionCallIdentifiers;
+    using WgslMutator::GetIdentifiers;
+    using WgslMutator::GetIntLiterals;
+    using WgslMutator::GetLoopBodyPositions;
+    using WgslMutator::ReplaceRegion;
+    using WgslMutator::SwapIntervals;
+};
+
 // Swaps two non-consecutive regions in the edge
 TEST(SwapRegionsTest, SwapIntervalsEdgeNonConsecutive) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;";
     std::string all_regions = R1 + R2 + R3;
 
     // this call should swap R1 with R3.
-    SwapIntervals(0, R1.length(), R1.length() + R2.length(), R3.length(), all_regions);
+    mutator.SwapIntervals(0, R1.length(), R1.length() + R2.length(), R3.length(), all_regions);
 
     ASSERT_EQ(R3 + R2 + R1, all_regions);
 }
 
 // Swaps two non-consecutive regions not in the edge
 TEST(SwapRegionsTest, SwapIntervalsNonConsecutiveNonEdge) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // this call should swap R2 with R4.
-    SwapIntervals(R1.length(), R2.length(), R1.length() + R2.length() + R3.length(), R4.length(),
-                  all_regions);
+    mutator.SwapIntervals(R1.length(), R2.length(), R1.length() + R2.length() + R3.length(),
+                          R4.length(), all_regions);
 
     ASSERT_EQ(R1 + R4 + R3 + R2 + R5, all_regions);
 }
@@ -48,12 +70,15 @@
 // Swaps two consecutive regions not in the edge (sorrounded by other
 // regions)
 TEST(SwapRegionsTest, SwapIntervalsConsecutiveEdge) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4;
 
     // this call should swap R2 with R3.
-    SwapIntervals(R1.length(), R2.length(), R1.length() + R2.length(), R3.length(), all_regions);
+    mutator.SwapIntervals(R1.length(), R2.length(), R1.length() + R2.length(), R3.length(),
+                          all_regions);
 
     ASSERT_EQ(R1 + R3 + R2 + R4, all_regions);
 }
@@ -61,113 +86,137 @@
 // Swaps two consecutive regions not in the edge (not sorrounded by other
 // regions)
 TEST(SwapRegionsTest, SwapIntervalsConsecutiveNonEdge) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // this call should swap R4 with R5.
-    SwapIntervals(R1.length() + R2.length() + R3.length(), R4.length(),
-                  R1.length() + R2.length() + R3.length() + R4.length(), R5.length(), all_regions);
+    mutator.SwapIntervals(R1.length() + R2.length() + R3.length(), R4.length(),
+                          R1.length() + R2.length() + R3.length() + R4.length(), R5.length(),
+                          all_regions);
 
     ASSERT_EQ(R1 + R2 + R3 + R5 + R4, all_regions);
 }
 
 // Deletes the first region.
 TEST(DeleteRegionTest, DeleteFirstRegion) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // This call should delete R1.
-    DeleteInterval(0, R1.length(), all_regions);
+    mutator.DeleteInterval(0, R1.length(), all_regions);
 
     ASSERT_EQ(";" + R2 + R3 + R4 + R5, all_regions);
 }
 
 // Deletes the last region.
 TEST(DeleteRegionTest, DeleteLastRegion) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // This call should delete R5.
-    DeleteInterval(R1.length() + R2.length() + R3.length() + R4.length(), R5.length(), all_regions);
+    mutator.DeleteInterval(R1.length() + R2.length() + R3.length() + R4.length(), R5.length(),
+                           all_regions);
 
     ASSERT_EQ(R1 + R2 + R3 + R4 + ";", all_regions);
 }
 
 // Deletes the middle region.
 TEST(DeleteRegionTest, DeleteMiddleRegion) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // This call should delete R3.
-    DeleteInterval(R1.length() + R2.length(), R3.length(), all_regions);
+    mutator.DeleteInterval(R1.length() + R2.length(), R3.length(), all_regions);
 
     ASSERT_EQ(R1 + R2 + ";" + R4 + R5, all_regions);
 }
 
 TEST(InsertRegionTest, InsertRegionTest1) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // This call should insert R2 after R4.
-    DuplicateInterval(R1.length(), R2.length(),
-                      R1.length() + R2.length() + R3.length() + R4.length() - 1, all_regions);
+    mutator.DuplicateInterval(R1.length(), R2.length(),
+                              R1.length() + R2.length() + R3.length() + R4.length() - 1,
+                              all_regions);
 
     ASSERT_EQ(R1 + R2 + R3 + R4 + R2.substr(1, R2.size() - 1) + R5, all_regions);
 }
 
 TEST(InsertRegionTest, InsertRegionTest2) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
 
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // This call should insert R3 after R1.
-    DuplicateInterval(R1.length() + R2.length(), R3.length(), R1.length() - 1, all_regions);
+    mutator.DuplicateInterval(R1.length() + R2.length(), R3.length(), R1.length() - 1, all_regions);
 
     ASSERT_EQ(R1 + R3.substr(1, R3.length() - 1) + R2 + R3 + R4 + R5, all_regions);
 }
 
 TEST(InsertRegionTest, InsertRegionTest3) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = ";region1;", R2 = ";regionregion2;", R3 = ";regionregionregion3;",
                 R4 = ";regionregionregionregion4;", R5 = ";regionregionregionregionregion5;";
 
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // This call should insert R2 after R5.
-    DuplicateInterval(R1.length(), R2.length(), all_regions.length() - 1, all_regions);
+    mutator.DuplicateInterval(R1.length(), R2.length(), all_regions.length() - 1, all_regions);
 
     ASSERT_EQ(R1 + R2 + R3 + R4 + R5 + R2.substr(1, R2.length() - 1), all_regions);
 }
 
 TEST(ReplaceIdentifierTest, ReplaceIdentifierTest1) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = "|region1|", R2 = "; region2;", R3 = "---------region3---------",
                 R4 = "++region4++", R5 = "***region5***";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // Replaces R3 with R1.
-    ReplaceRegion(0, R1.length(), R1.length() + R2.length(), R3.length(), all_regions);
+    mutator.ReplaceRegion(0, R1.length(), R1.length() + R2.length(), R3.length(), all_regions);
 
     ASSERT_EQ(R1 + R2 + R1 + R4 + R5, all_regions);
 }
 
 TEST(ReplaceIdentifierTest, ReplaceIdentifierTest2) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string R1 = "|region1|", R2 = "; region2;", R3 = "---------region3---------",
                 R4 = "++region4++", R5 = "***region5***";
     std::string all_regions = R1 + R2 + R3 + R4 + R5;
 
     // Replaces R5 with R3.
-    ReplaceRegion(R1.length() + R2.length(), R3.length(),
-                  R1.length() + R2.length() + R3.length() + R4.length(), R5.length(), all_regions);
+    mutator.ReplaceRegion(R1.length() + R2.length(), R3.length(),
+                          R1.length() + R2.length() + R3.length() + R4.length(), R5.length(),
+                          all_regions);
 
     ASSERT_EQ(R1 + R2 + R3 + R4 + R3, all_regions);
 }
 
 TEST(GetIdentifierTest, GetIdentifierTest1) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string wgsl_code =
         R"(fn clamp_0acf8f() {
         var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
@@ -187,23 +236,19 @@
         clamp_0acf8f();
       })";
 
-    std::vector<std::pair<size_t, size_t>> identifiers_pos = GetIdentifiers(wgsl_code);
-
+    std::vector<std::pair<size_t, size_t>> identifiers_pos = mutator.GetIdentifiers(wgsl_code);
     std::vector<std::pair<size_t, size_t>> ground_truth = {
-        std::make_pair(3, 12),   std::make_pair(28, 3),  std::make_pair(37, 4),
-        std::make_pair(49, 5),   std::make_pair(60, 3),  std::make_pair(68, 4),
-        std::make_pair(81, 4),   std::make_pair(110, 6), std::make_pair(123, 2),
-        std::make_pair(133, 4),  std::make_pair(144, 7), std::make_pair(162, 4),
-        std::make_pair(183, 12), std::make_pair(209, 6), std::make_pair(221, 3),
-        std::make_pair(244, 8),  std::make_pair(259, 2), std::make_pair(271, 4),
-        std::make_pair(288, 12), std::make_pair(319, 7), std::make_pair(328, 14),
-        std::make_pair(352, 2),  std::make_pair(363, 4), std::make_pair(381, 3),
-        std::make_pair(394, 3),  std::make_pair(399, 3), std::make_pair(418, 12)};
-
+        {0, 2},   {3, 12},  {28, 3},   {32, 3},   {37, 4},   {42, 3},   {49, 5},  {55, 4},
+        {60, 3},  {68, 4},  {73, 3},   {81, 4},   {86, 3},   {110, 6},  {123, 2}, {126, 11},
+        {144, 7}, {152, 8}, {162, 4},  {167, 3},  {183, 12}, {209, 6},  {216, 4}, {221, 3},
+        {244, 8}, {259, 2}, {262, 13}, {288, 12}, {319, 7},  {328, 14}, {352, 2}, {355, 12},
+        {381, 3}, {385, 7}, {394, 3},  {399, 3},  {418, 12}};
     ASSERT_EQ(ground_truth, identifiers_pos);
 }
 
 TEST(TestGetLiteralsValues, TestGetLiteralsValues1) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string wgsl_code =
         R"(fn clamp_0acf8f() {
         var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
@@ -227,7 +272,7 @@
       foo_1 = 5 + 7;
       var foo_3 : i32 = -20;)";
 
-    std::vector<std::pair<size_t, size_t>> literals_pos = GetIntLiterals(wgsl_code);
+    std::vector<std::pair<size_t, size_t>> literals_pos = mutator.GetIntLiterals(wgsl_code);
 
     std::vector<std::string> ground_truth = {"3", "10", "5", "7", "-20"};
 
@@ -241,6 +286,8 @@
 }
 
 TEST(InsertReturnTest, FindClosingBrace) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string wgsl_code =
         R"(fn clamp_0acf8f() {
         if(false){
@@ -269,7 +316,7 @@
         var foo_3 : i32 = -20;
       )";
     size_t opening_bracket_pos = 18;
-    size_t closing_bracket_pos = FindClosingBrace(opening_bracket_pos, wgsl_code);
+    size_t closing_bracket_pos = mutator.FindClosingBrace(opening_bracket_pos, wgsl_code);
 
     // The -1 is needed since the function body starts after the left bracket.
     std::string function_body =
@@ -286,6 +333,8 @@
 }
 
 TEST(InsertReturnTest, FindClosingBraceFailing) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string wgsl_code =
         R"(fn clamp_0acf8f() {
       // This comment } causes the test to fail.
@@ -314,7 +363,7 @@
       foo_1 = 5 + 7;
       var foo_3 : i32 = -20;)";
     size_t opening_bracket_pos = 18;
-    size_t closing_bracket_pos = FindClosingBrace(opening_bracket_pos, wgsl_code);
+    size_t closing_bracket_pos = mutator.FindClosingBrace(opening_bracket_pos, wgsl_code);
 
     // The -1 is needed since the function body starts after the left bracket.
     std::string function_body =
@@ -329,67 +378,9 @@
     ASSERT_NE(expected, function_body);
 }
 
-TEST(TestInsertReturn, TestInsertReturn1) {
-    std::string wgsl_code =
-        R"(fn clamp_0acf8f() {
-        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
-      }
-      @vertex
-      fn vertex_main() -> @builtin(position) vec4<f32> {
-        clamp_0acf8f();
-        var foo_1: i32 = 3;
-        return vec4<f32>();
-      }
-      @fragment
-      fn fragment_main() {
-        clamp_0acf8f();
-      }
-      @compute @workgroup_size(1)
-      fn compute_main() {
-        var<private> foo: f32 = 0.0;
-        var foo_2: i32 = 10;
-        clamp_0acf8f();
-      }
-      foo_1 = 5 + 7;
-      var foo_3 : i32 = -20;)";
-
-    std::vector<size_t> semicolon_pos;
-    for (size_t pos = wgsl_code.find(";", 0); pos != std::string::npos;
-         pos = wgsl_code.find(";", pos + 1)) {
-        semicolon_pos.push_back(pos);
-    }
-
-    // should insert a return true statement after the first semicolon of the
-    // first function the the WGSL-like string above.
-    wgsl_code.insert(semicolon_pos[0] + 1, "return true;");
-
-    std::string expected_wgsl_code =
-        R"(fn clamp_0acf8f() {
-        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());return true;
-      }
-      @vertex
-      fn vertex_main() -> @builtin(position) vec4<f32> {
-        clamp_0acf8f();
-        var foo_1: i32 = 3;
-        return vec4<f32>();
-      }
-      @fragment
-      fn fragment_main() {
-        clamp_0acf8f();
-      }
-      @compute @workgroup_size(1)
-      fn compute_main() {
-        var<private> foo: f32 = 0.0;
-        var foo_2: i32 = 10;
-        clamp_0acf8f();
-      }
-      foo_1 = 5 + 7;
-      var foo_3 : i32 = -20;)";
-
-    ASSERT_EQ(expected_wgsl_code, wgsl_code);
-}
-
-TEST(TestInsertReturn, TestFunctionPositions) {
+TEST(TestInsertReturn, TestFunctionPositions1) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string wgsl_code =
         R"(fn clamp_0acf8f() {
           var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
@@ -418,12 +409,38 @@
         foo_1 = 5 + 7;
         var foo_3 : i32 = -20;)";
 
-    std::vector<size_t> function_positions = GetFunctionBodyPositions(wgsl_code);
-    std::vector<size_t> expected_positions = {180, 586};
+    std::vector<std::pair<size_t, bool>> function_positions =
+        mutator.GetFunctionBodyPositions(wgsl_code);
+    std::vector<std::pair<size_t, bool>> expected_positions = {
+        {18, false}, {180, true}, {323, false}, {423, false}, {586, true}};
+    ASSERT_EQ(expected_positions, function_positions);
+}
+
+TEST(TestInsertReturn, TestFunctionPositions2) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    std::string wgsl_code =
+        R"(fn some_loop_body() {
+}
+
+fn f() {
+  var j : i32; i = (i + 1)) {
+    some_loop_body(); ((i < 5) && (j < 10));
+  for(var i : i32 = 0;
+    j = (i * 30);
+  }
+}
+)";
+
+    std::vector<std::pair<size_t, bool>> function_positions =
+        mutator.GetFunctionBodyPositions(wgsl_code);
+    std::vector<std::pair<size_t, bool>> expected_positions = {{20, false}, {32, false}};
     ASSERT_EQ(expected_positions, function_positions);
 }
 
 TEST(TestInsertReturn, TestMissingSemicolon) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
     std::string wgsl_code =
         R"(fn clamp_0acf8f() {
           var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>())
@@ -452,8 +469,7 @@
         foo_1 = 5 + 7;
         var foo_3 : i32 = -20;)";
 
-    RandomGenerator generator(0);
-    InsertReturnStatement(wgsl_code, generator);
+    mutator.InsertReturnStatement(wgsl_code);
 
     // No semicolons found in the function's body, so wgsl_code
     // should remain unchanged.
@@ -487,5 +503,141 @@
     ASSERT_EQ(expected_wgsl_code, wgsl_code);
 }
 
+TEST(TestReplaceOperator, TestIdentifyOperators) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    std::string code =
+        R"(
+x += 2;
+y = a + b;
+z = -a;
+x *= b / c;
+t = t && t | t || t;
+b = b > c ^ c <= d;
+a >>= b;
+b <<= a;
+a = a << 2;
+b = b >> 3;
+c = a % 3;
+d %= e;
+)";
+    // These are the operator occurrences that will be observed by going through the file character
+    // by character. This includes, for example, identifying the ">" operator if search starts after
+    // the first character of ">>".
+    std::vector<std::pair<uint32_t, uint32_t>> operator_occurrences = {
+        {3, 2},   {4, 1},   {11, 1},  {15, 1},  {22, 1},  {24, 1},  {30, 2},  {31, 1},
+        {35, 1},  {42, 1},  {46, 2},  {47, 1},  {51, 1},  {55, 2},  {56, 1},  {63, 1},
+        {67, 1},  {71, 1},  {75, 2},  {76, 1},  {83, 3},  {84, 2},  {85, 1},  {92, 3},
+        {93, 2},  {94, 1},  {101, 1}, {105, 2}, {106, 1}, {113, 1}, {117, 2}, {118, 1},
+        {125, 1}, {129, 1}, {136, 2}, {137, 1}, {3, 2}};
+    uint32_t operator_occurrence_index = 0;
+    for (size_t i = 0; i < code.length(); i++) {
+        // Move on to the next operator occurrence if the current index into the code string exceeds
+        // the index associated with that operator occurrence. Exception: stay with the last
+        // operator occurrence if search has already passed the last operator in the file.
+        if (i < code.length() - 2 && i > operator_occurrences[operator_occurrence_index].first) {
+            operator_occurrence_index =
+                (operator_occurrence_index + 1) % operator_occurrences.size();
+        }
+        ASSERT_EQ(operator_occurrences[operator_occurrence_index],
+                  mutator.FindOperatorOccurrence(code, static_cast<uint32_t>(i)).value());
+    }
+}
+
+TEST(TestInsertBreakOrContinue, TestLoopPositions1) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    std::string wgsl_code = " loop { } loop { } loop { }";
+    std::vector<size_t> loop_positions = mutator.GetLoopBodyPositions(wgsl_code);
+    std::vector<size_t> expected_positions = {6, 15, 24};
+    ASSERT_EQ(expected_positions, loop_positions);
+}
+
+TEST(TestInsertBreakOrContinue, TestLoopPositions2) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    std::string wgsl_code = R"( loop { } loop
+{ } loop { })";
+    std::vector<size_t> loop_positions = mutator.GetLoopBodyPositions(wgsl_code);
+    std::vector<size_t> expected_positions = {6, 15, 24};
+    ASSERT_EQ(expected_positions, loop_positions);
+}
+
+TEST(TestInsertBreakOrContinue, TestLoopPositions3) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    // This WGSL-like code is not valid, but it suffices to test regex-based matching (which is
+    // intended to work well on semi-valid code).
+    std::string wgsl_code =
+        R"(fn compute_main() {
+  loop {
+    var twice: i32 = 2 * i;
+    i++;
+    if i == 5 { break; }
+      loop
+      {
+      var twice: i32 = 2 * i;
+      i++;
+      while (i < 100) { i++; }
+      if i == 5 { break; }
+    }
+  }
+  for (a = 0; a < 100; a++)   {
+    if (a > 50) {
+      break;
+    }
+      while (i < 100) { i++; }
+  }
+})";
+
+    std::vector<size_t> loop_positions = mutator.GetLoopBodyPositions(wgsl_code);
+    std::vector<size_t> expected_positions = {27, 108, 173, 249, 310};
+    ASSERT_EQ(expected_positions, loop_positions);
+}
+
+TEST(TestInsertBreakOrContinue, TestLoopPositions4) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    std::string wgsl_code =
+        R"(fn clamp_0acf8f() {
+        var res: vec2<f32> = clamp(vec2<f32>(), vec2<f32>(), vec2<f32>());
+      }
+      @vertex
+      fn vertex_main() -> @builtin(position) vec4<f32> {
+         clamp_0acf8f();"
+         return vec4<f32>();
+      }
+      @fragment
+      fn fragment_main() {
+        clamp_0acf8f();
+      }
+      @compute @workgroup_size(1)
+      fn compute_main() {"
+        var<private> foo: f32 = 0.0;
+        clamp_0acf8f    ();
+      })";
+
+    std::vector<size_t> loop_positions = mutator.GetLoopBodyPositions(wgsl_code);
+    ASSERT_TRUE(loop_positions.empty());
+}
+
+TEST(TestReplaceFunctionCallWithBuiltin, FindFunctionCalls) {
+    RandomGenerator generator(0);
+    WgslMutatorTest mutator(generator);
+    std::string function_body = R"({
+          var<private> foo: f32 = 0.0;
+          var foo_2: i32 = 10;
+          clamp_0acf8f  ();
+          _0acf8f();
+          f
+();
+          j = (i * 30);
+        })";
+    std::vector<std::pair<size_t, size_t>> call_identifiers =
+        mutator.GetFunctionCallIdentifiers(function_body);
+    std::vector<std::pair<size_t, size_t>> ground_truth{{82, 12}, {110, 7}, {131, 1}};
+    ASSERT_EQ(ground_truth, call_identifiers);
+}
+
 }  // namespace
 }  // namespace tint::fuzzers::regex_fuzzer
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc
index 677560b..a965613 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc
+++ b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.cc
@@ -26,8 +26,10 @@
 
 namespace tint::fuzzers::regex_fuzzer {
 
-std::vector<size_t> FindDelimiterIndices(const std::string& delimiter,
-                                         const std::string& wgsl_code) {
+WgslMutator::WgslMutator(RandomGenerator& generator) : generator_(generator) {}
+
+std::vector<size_t> WgslMutator::FindDelimiterIndices(const std::string& delimiter,
+                                                      const std::string& wgsl_code) {
     std::vector<size_t> result;
     for (size_t pos = wgsl_code.find(delimiter, 0); pos != std::string::npos;
          pos = wgsl_code.find(delimiter, pos + 1)) {
@@ -37,7 +39,7 @@
     return result;
 }
 
-std::vector<std::pair<size_t, size_t>> GetIdentifiers(const std::string& wgsl_code) {
+std::vector<std::pair<size_t, size_t>> WgslMutator::GetIdentifiers(const std::string& wgsl_code) {
     std::vector<std::pair<size_t, size_t>> result;
 
     // This regular expression works by looking for a character that
@@ -45,23 +47,37 @@
     // by a character which cannot be part of a WGSL identifer. The regex
     // for the WGSL identifier is obtained from:
     // https://www.w3.org/TR/WGSL/#identifiers.
-    std::regex wgsl_identifier_regex("[^a-zA-Z]([a-zA-Z][0-9a-zA-Z_]*)[^0-9a-zA-Z_]");
+    std::regex identifier_regex("[_a-zA-Z][0-9a-zA-Z_]*");
 
-    std::smatch match;
+    auto identifiers_begin =
+        std::sregex_iterator(wgsl_code.begin(), wgsl_code.end(), identifier_regex);
+    auto identifiers_end = std::sregex_iterator();
 
-    std::string::const_iterator search_start(wgsl_code.cbegin());
-    std::string prefix;
-
-    while (regex_search(search_start, wgsl_code.cend(), match, wgsl_identifier_regex) == true) {
-        prefix += match.prefix();
-        result.push_back(std::make_pair(prefix.size() + 1, match.str(1).size()));
-        prefix += match.str(0);
-        search_start = match.suffix().first;
+    for (std::sregex_iterator i = identifiers_begin; i != identifiers_end; ++i) {
+        result.push_back(
+            {static_cast<size_t>(i->prefix().second - wgsl_code.cbegin()), i->str().size()});
     }
     return result;
 }
 
-std::vector<std::pair<size_t, size_t>> GetIntLiterals(const std::string& s) {
+std::vector<std::pair<size_t, size_t>> WgslMutator::GetFunctionCallIdentifiers(
+    const std::string& wgsl_code) {
+    std::vector<std::pair<size_t, size_t>> result;
+
+    std::regex call_regex("([_a-zA-Z][0-9a-zA-Z_]*)[ \\n]*\\(");
+
+    auto identifiers_begin = std::sregex_iterator(wgsl_code.begin(), wgsl_code.end(), call_regex);
+    auto identifiers_end = std::sregex_iterator();
+
+    for (std::sregex_iterator i = identifiers_begin; i != identifiers_end; ++i) {
+        auto submatch = (*i)[1];
+        result.push_back(
+            {static_cast<size_t>(submatch.first - wgsl_code.cbegin()), submatch.str().size()});
+    }
+    return result;
+}
+
+std::vector<std::pair<size_t, size_t>> WgslMutator::GetIntLiterals(const std::string& s) {
     std::vector<std::pair<size_t, size_t>> result;
 
     // Looks for integer literals in decimal or hexadecimal form.
@@ -83,7 +99,7 @@
     return result;
 }
 
-size_t FindClosingBrace(size_t opening_bracket_pos, const std::string& wgsl_code) {
+size_t WgslMutator::FindClosingBrace(size_t opening_bracket_pos, const std::string& wgsl_code) {
     size_t open_bracket_count = 1;
     size_t pos = opening_bracket_pos + 1;
     while (open_bracket_count >= 1 && pos < wgsl_code.size()) {
@@ -97,33 +113,52 @@
     return (pos == wgsl_code.size() && open_bracket_count >= 1) ? 0 : pos - 1;
 }
 
-std::vector<size_t> GetFunctionBodyPositions(const std::string& wgsl_code) {
+std::vector<std::pair<size_t, bool>> WgslMutator::GetFunctionBodyPositions(
+    const std::string& wgsl_code) {
     // Finds all the functions with a non-void return value.
-    std::regex function_regex("fn.*?->.*?\\{");
-    std::smatch match;
-    std::vector<size_t> result;
+    std::regex function_regex("fn[^a-zA-Z_0-9][^\\{]*\\{");
+    std::vector<std::pair<size_t, bool>> result;
 
-    auto search_start(wgsl_code.cbegin());
-    std::string prefix = "";
+    auto functions_begin = std::sregex_iterator(wgsl_code.begin(), wgsl_code.end(), function_regex);
+    auto functions_end = std::sregex_iterator();
 
-    while (std::regex_search(search_start, wgsl_code.cend(), match, function_regex)) {
-        result.push_back(static_cast<size_t>(match.suffix().first - wgsl_code.cbegin() - 1L));
-        search_start = match.suffix().first;
+    for (std::sregex_iterator i = functions_begin; i != functions_end; ++i) {
+        bool returns_value = i->str().find("->") != std::string::npos;
+        result.push_back(
+            {static_cast<size_t>(i->suffix().first - wgsl_code.cbegin() - 1), returns_value});
     }
     return result;
 }
 
-bool InsertReturnStatement(std::string& wgsl_code, RandomGenerator& generator) {
-    std::vector<size_t> function_body_positions = GetFunctionBodyPositions(wgsl_code);
+std::vector<size_t> WgslMutator::GetLoopBodyPositions(const std::string& wgsl_code) {
+    // Finds all loops.
+    std::regex loop_regex("[^a-zA-Z_0-9](for|while|loop)[^\\{]*\\{");
+    std::vector<size_t> result;
+
+    auto loops_begin = std::sregex_iterator(wgsl_code.begin(), wgsl_code.end(), loop_regex);
+    auto loops_end = std::sregex_iterator();
+
+    for (std::sregex_iterator i = loops_begin; i != loops_end; ++i) {
+        result.push_back(static_cast<size_t>(i->suffix().first - wgsl_code.cbegin() - 1));
+    }
+    return result;
+}
+
+bool WgslMutator::InsertReturnStatement(std::string& wgsl_code) {
+    std::vector<std::pair<size_t, bool>> function_body_positions =
+        GetFunctionBodyPositions(wgsl_code);
 
     // No function was found in wgsl_code.
     if (function_body_positions.empty()) {
         return false;
     }
 
-    // Pick a random function's opening bracket, find the corresponding closing
-    // bracket, and find a semi-colon within the function body.
-    size_t left_bracket_pos = generator.GetRandomElement(function_body_positions);
+    // Pick a random function
+    auto function = generator_.GetRandomElement(function_body_positions);
+
+    // Find the corresponding closing bracket for the function, and find a semi-colon within the
+    // function body.
+    size_t left_bracket_pos = function.first;
 
     size_t right_bracket_pos = FindClosingBrace(left_bracket_pos, wgsl_code);
 
@@ -141,27 +176,64 @@
         return false;
     }
 
-    size_t semicolon_position = generator.GetRandomElement(semicolon_positions);
-
-    // Get all identifiers and integer literals to use as potential return values.
-    std::vector<std::pair<size_t, size_t>> identifiers = GetIdentifiers(wgsl_code);
-    auto return_values = identifiers;
-    std::vector<std::pair<size_t, size_t>> int_literals = GetIntLiterals(wgsl_code);
-    return_values.insert(return_values.end(), int_literals.begin(), int_literals.end());
-    std::pair<size_t, size_t> return_value = generator.GetRandomElement(return_values);
-    std::string return_statement =
-        "return " + wgsl_code.substr(return_value.first, return_value.second) + ";";
+    std::string return_statement = "return";
+    if (function.second) {
+        // The function returns a value. Get all identifiers and integer literals to use as
+        // potential return values.
+        std::vector<std::pair<size_t, size_t>> identifiers = GetIdentifiers(wgsl_code);
+        auto return_values = identifiers;
+        std::vector<std::pair<size_t, size_t>> int_literals = GetIntLiterals(wgsl_code);
+        return_values.insert(return_values.end(), int_literals.begin(), int_literals.end());
+        std::pair<size_t, size_t> return_value = generator_.GetRandomElement(return_values);
+        return_statement += " " + wgsl_code.substr(return_value.first, return_value.second);
+    }
+    return_statement += ";";
 
     // Insert the return statement immediately after the semicolon.
-    wgsl_code.insert(semicolon_position + 1, return_statement);
+    wgsl_code.insert(generator_.GetRandomElement(semicolon_positions) + 1, return_statement);
     return true;
 }
 
-void SwapIntervals(size_t idx1,
-                   size_t reg1_len,
-                   size_t idx2,
-                   size_t reg2_len,
-                   std::string& wgsl_code) {
+bool WgslMutator::InsertBreakOrContinue(std::string& wgsl_code) {
+    std::vector<size_t> loop_body_positions = GetLoopBodyPositions(wgsl_code);
+
+    // No loop was found in wgsl_code.
+    if (loop_body_positions.empty()) {
+        return false;
+    }
+
+    // Pick a random loop's opening bracket, find the corresponding closing
+    // bracket, and find a semi-colon within the loop body.
+    size_t left_bracket_pos = generator_.GetRandomElement(loop_body_positions);
+
+    size_t right_bracket_pos = FindClosingBrace(left_bracket_pos, wgsl_code);
+
+    if (right_bracket_pos == 0) {
+        return false;
+    }
+
+    std::vector<size_t> semicolon_positions;
+    for (size_t pos = wgsl_code.find(";", left_bracket_pos + 1); pos < right_bracket_pos;
+         pos = wgsl_code.find(";", pos + 1)) {
+        semicolon_positions.push_back(pos);
+    }
+
+    if (semicolon_positions.empty()) {
+        return false;
+    }
+
+    size_t semicolon_position = generator_.GetRandomElement(semicolon_positions);
+
+    // Insert a break or continue immediately after the semicolon.
+    wgsl_code.insert(semicolon_position + 1, generator_.GetBool() ? "break;" : "continue;");
+    return true;
+}
+
+void WgslMutator::SwapIntervals(size_t idx1,
+                                size_t reg1_len,
+                                size_t idx2,
+                                size_t reg2_len,
+                                std::string& wgsl_code) {
     std::string region_1 = wgsl_code.substr(idx1 + 1, reg1_len - 1);
 
     std::string region_2 = wgsl_code.substr(idx2 + 1, reg2_len - 1);
@@ -172,36 +244,37 @@
     wgsl_code.replace(idx1 + 1, region_1.size(), region_2);
 }
 
-void DeleteInterval(size_t idx1, size_t reg_len, std::string& wgsl_code) {
+void WgslMutator::DeleteInterval(size_t idx1, size_t reg_len, std::string& wgsl_code) {
     wgsl_code.erase(idx1 + 1, reg_len - 1);
 }
 
-void DuplicateInterval(size_t idx1, size_t reg1_len, size_t idx2, std::string& wgsl_code) {
+void WgslMutator::DuplicateInterval(size_t idx1,
+                                    size_t reg1_len,
+                                    size_t idx2,
+                                    std::string& wgsl_code) {
     std::string region = wgsl_code.substr(idx1 + 1, reg1_len - 1);
     wgsl_code.insert(idx2 + 1, region);
 }
 
-void ReplaceRegion(size_t idx1,
-                   size_t id1_len,
-                   size_t idx2,
-                   size_t id2_len,
-                   std::string& wgsl_code) {
+void WgslMutator::ReplaceRegion(size_t idx1,
+                                size_t id1_len,
+                                size_t idx2,
+                                size_t id2_len,
+                                std::string& wgsl_code) {
     std::string region_1 = wgsl_code.substr(idx1, id1_len);
     std::string region_2 = wgsl_code.substr(idx2, id2_len);
     wgsl_code.replace(idx2, region_2.size(), region_1);
 }
 
-void ReplaceInterval(size_t start_index,
-                     size_t length,
-                     std::string replacement_text,
-                     std::string& wgsl_code) {
+void WgslMutator::ReplaceInterval(size_t start_index,
+                                  size_t length,
+                                  std::string replacement_text,
+                                  std::string& wgsl_code) {
     std::string region_1 = wgsl_code.substr(start_index, length);
     wgsl_code.replace(start_index, length, replacement_text);
 }
 
-bool SwapRandomIntervals(const std::string& delimiter,
-                         std::string& wgsl_code,
-                         RandomGenerator& generator) {
+bool WgslMutator::SwapRandomIntervals(const std::string& delimiter, std::string& wgsl_code) {
     std::vector<size_t> delimiter_positions = FindDelimiterIndices(delimiter, wgsl_code);
 
     // Need to have at least 3 indices.
@@ -212,12 +285,12 @@
     // Choose indices:
     //   interval_1_start < interval_1_end <= interval_2_start < interval_2_end
     uint32_t interval_1_start =
-        generator.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()) - 2u);
-    uint32_t interval_1_end = generator.GetUInt32(
+        generator_.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()) - 2u);
+    uint32_t interval_1_end = generator_.GetUInt32(
         interval_1_start + 1u, static_cast<uint32_t>(delimiter_positions.size()) - 1u);
-    uint32_t interval_2_start =
-        generator.GetUInt32(interval_1_end, static_cast<uint32_t>(delimiter_positions.size()) - 1u);
-    uint32_t interval_2_end = generator.GetUInt32(
+    uint32_t interval_2_start = generator_.GetUInt32(
+        interval_1_end, static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+    uint32_t interval_2_end = generator_.GetUInt32(
         interval_2_start + 1u, static_cast<uint32_t>(delimiter_positions.size()));
 
     SwapIntervals(delimiter_positions[interval_1_start],
@@ -229,9 +302,7 @@
     return true;
 }
 
-bool DeleteRandomInterval(const std::string& delimiter,
-                          std::string& wgsl_code,
-                          RandomGenerator& generator) {
+bool WgslMutator::DeleteRandomInterval(const std::string& delimiter, std::string& wgsl_code) {
     std::vector<size_t> delimiter_positions = FindDelimiterIndices(delimiter, wgsl_code);
 
     // Need to have at least 2 indices.
@@ -240,9 +311,9 @@
     }
 
     uint32_t interval_start =
-        generator.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()) - 1u);
-    uint32_t interval_end =
-        generator.GetUInt32(interval_start + 1u, static_cast<uint32_t>(delimiter_positions.size()));
+        generator_.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+    uint32_t interval_end = generator_.GetUInt32(interval_start + 1u,
+                                                 static_cast<uint32_t>(delimiter_positions.size()));
 
     DeleteInterval(delimiter_positions[interval_start],
                    delimiter_positions[interval_end] - delimiter_positions[interval_start],
@@ -251,9 +322,7 @@
     return true;
 }
 
-bool DuplicateRandomInterval(const std::string& delimiter,
-                             std::string& wgsl_code,
-                             RandomGenerator& generator) {
+bool WgslMutator::DuplicateRandomInterval(const std::string& delimiter, std::string& wgsl_code) {
     std::vector<size_t> delimiter_positions = FindDelimiterIndices(delimiter, wgsl_code);
 
     // Need to have at least 2 indices
@@ -262,11 +331,11 @@
     }
 
     uint32_t interval_start =
-        generator.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()) - 1u);
-    uint32_t interval_end =
-        generator.GetUInt32(interval_start + 1u, static_cast<uint32_t>(delimiter_positions.size()));
+        generator_.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()) - 1u);
+    uint32_t interval_end = generator_.GetUInt32(interval_start + 1u,
+                                                 static_cast<uint32_t>(delimiter_positions.size()));
     uint32_t duplication_point =
-        generator.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()));
+        generator_.GetUInt32(static_cast<uint32_t>(delimiter_positions.size()));
 
     DuplicateInterval(delimiter_positions[interval_start],
                       delimiter_positions[interval_end] - delimiter_positions[interval_start],
@@ -275,7 +344,7 @@
     return true;
 }
 
-bool ReplaceRandomIdentifier(std::string& wgsl_code, RandomGenerator& generator) {
+bool WgslMutator::ReplaceRandomIdentifier(std::string& wgsl_code) {
     std::vector<std::pair<size_t, size_t>> identifiers = GetIdentifiers(wgsl_code);
 
     // Need at least 2 identifiers
@@ -283,12 +352,12 @@
         return false;
     }
 
-    uint32_t id1_index = generator.GetUInt32(static_cast<uint32_t>(identifiers.size()));
-    uint32_t id2_index = generator.GetUInt32(static_cast<uint32_t>(identifiers.size()));
+    uint32_t id1_index = generator_.GetUInt32(static_cast<uint32_t>(identifiers.size()));
+    uint32_t id2_index = generator_.GetUInt32(static_cast<uint32_t>(identifiers.size()));
 
     // The two identifiers must be different
     while (id1_index == id2_index) {
-        id2_index = generator.GetUInt32(static_cast<uint32_t>(identifiers.size()));
+        id2_index = generator_.GetUInt32(static_cast<uint32_t>(identifiers.size()));
     }
 
     ReplaceRegion(identifiers[id1_index].first, identifiers[id1_index].second,
@@ -297,7 +366,7 @@
     return true;
 }
 
-bool ReplaceRandomIntLiteral(std::string& wgsl_code, RandomGenerator& generator) {
+bool WgslMutator::ReplaceRandomIntLiteral(std::string& wgsl_code) {
     std::vector<std::pair<size_t, size_t>> literals = GetIntLiterals(wgsl_code);
 
     // Need at least one integer literal
@@ -305,13 +374,13 @@
         return false;
     }
 
-    uint32_t literal_index = generator.GetUInt32(static_cast<uint32_t>(literals.size()));
+    uint32_t literal_index = generator_.GetUInt32(static_cast<uint32_t>(literals.size()));
 
     // INT_MAX = 2147483647, INT_MIN = -2147483648
     std::vector<std::string> boundary_values = {"2147483647", "-2147483648", "1",
                                                 "-1",         "0",           "4294967295"};
 
-    uint32_t boundary_index = generator.GetUInt32(static_cast<uint32_t>(boundary_values.size()));
+    uint32_t boundary_index = generator_.GetUInt32(static_cast<uint32_t>(boundary_values.size()));
 
     ReplaceInterval(literals[literal_index].first, literals[literal_index].second,
                     boundary_values[boundary_index], wgsl_code);
@@ -319,4 +388,268 @@
     return true;
 }
 
+std::string WgslMutator::ChooseRandomReplacementForOperator(const std::string& existing_operator) {
+    // Operators are partitioned into three classes: assignment, expression and increment. The regex
+    // mutator will swap operators in the same class. The hypothesis is that this should exercise a
+    // number of type-safe swaps (e.g. changing += to *=), as well as some badly-typed yet
+    // interesting swaps (e.g. changing + to ^ when the operators are matrices), while avoiding
+    // making totally nonsensical replacements (such as changing ++ too /).
+    std::vector<std::string> assignment_operators{
+        "=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "<<=", ">>="};
+    std::vector<std::string> expression_operators{"+",  "-",  "*", "/",  "%",  "&&", "||",
+                                                  "&",  "|",  "^", "<<", ">>", "<",  ">",
+                                                  "<=", ">=", "!", "!=", "~"};
+    std::vector<std::string> increment_operators{"++", "--"};
+    for (auto operators : {assignment_operators, expression_operators, increment_operators}) {
+        auto it = std::find(operators.begin(), operators.end(), existing_operator);
+        if (it != operators.end()) {
+            // The operator falls into this category, so select another operator from the category.
+            operators.erase(it);
+            return generator_.GetRandomElement(operators);
+        }
+    }
+    assert(false && "Unknown operator");
+    return "";
+}
+
+bool WgslMutator::ReplaceRandomOperator(std::string& wgsl_code) {
+    // Choose an index into the code at random.
+    const uint32_t start_index = generator_.GetUInt32(static_cast<uint32_t>(wgsl_code.size()));
+    // Find the first operator occurrence from the chosen point, wrapping back to the start of the
+    // file if needed.
+    auto maybe_operator_occurrence = FindOperatorOccurrence(wgsl_code, start_index);
+    if (!maybe_operator_occurrence.has_value()) {
+        // It is unlikely that there will be *no* operators in the file, but if this is the case
+        // then this mutation cannot be applied.
+        return false;
+    }
+    std::string existing_operator =
+        wgsl_code.substr(maybe_operator_occurrence->first, maybe_operator_occurrence->second);
+    // Replace the identified operator with a randomly-chosen alternative.
+    wgsl_code.replace(maybe_operator_occurrence->first, maybe_operator_occurrence->second,
+                      ChooseRandomReplacementForOperator(existing_operator));
+    return true;
+}
+
+std::optional<std::pair<uint32_t, uint32_t>> WgslMutator::FindOperatorOccurrence(
+    const std::string& wgsl_code,
+    uint32_t start_index) {
+    // Loops through the characters of the code in a wrap-around fashion, looking for the first
+    // encountered token that is a WGSL operator.
+
+    for (size_t i = 0; i < wgsl_code.size(); i++) {
+        uint32_t current_index = static_cast<uint32_t>((start_index + i) % wgsl_code.size());
+
+        // To cater for multi-character operator tokens, get the three consecutive characters from
+        // the code string starting at the current index. Use null characters to account for the
+        // case where search has reached the end of the code string.
+        char first_character = wgsl_code[current_index];
+        char second_character =
+            current_index == wgsl_code.size() - 1 ? '\0' : wgsl_code[current_index + 1];
+        char third_character =
+            current_index >= wgsl_code.size() - 2 ? '\0' : wgsl_code[current_index + 2];
+
+        // This uses the extracted characters to match for the various WGSL operators.
+        switch (first_character) {
+            case '!':
+            case '^':
+                switch (second_character) {
+                    case '=':
+                        return {{current_index, 2}};
+                    default:
+                        return {{current_index, 1}};
+                }
+            case '|':
+            case '&':
+            case '+':
+            case '-':
+                if (second_character == first_character || second_character == '=') {
+                    return {{current_index, 2}};
+                }
+                return {{current_index, 1}};
+            case '*':
+            case '/':
+            case '%':
+                switch (second_character) {
+                    case '=':
+                        return {{current_index, 2}};
+                    default:
+                        return {{current_index, 1}};
+                }
+            case '=':
+                if (second_character == '=') {
+                    return {{current_index, 2}};
+                }
+                return {{current_index, 1}};
+            case '<':
+            case '>':
+                if (second_character == '=') {
+                    return {{current_index, 2}};
+                }
+                if (second_character == first_character) {
+                    if (third_character == '=') {
+                        return {{current_index, 3}};
+                    }
+                    return {{current_index, 2}};
+                }
+                return {{current_index, 1}};
+            case '~':
+                return {{current_index, 1}};
+            default:
+                break;
+        }
+    }
+    // No operator was found, so empty is returned.
+    return {};
+}
+
+bool WgslMutator::ReplaceFunctionCallWithBuiltin(std::string& wgsl_code) {
+    std::vector<std::pair<size_t, bool>> function_body_positions =
+        GetFunctionBodyPositions(wgsl_code);
+
+    // No function was found in wgsl_code.
+    if (function_body_positions.empty()) {
+        return false;
+    }
+
+    // Pick a random function
+    auto function = generator_.GetRandomElement(function_body_positions);
+
+    // Find the corresponding closing bracket for the function, and find a semi-colon within the
+    // function body.
+    size_t left_bracket_pos = function.first;
+
+    size_t right_bracket_pos = FindClosingBrace(left_bracket_pos, wgsl_code);
+
+    if (right_bracket_pos == 0) {
+        return false;
+    }
+
+    std::string function_body(
+        wgsl_code.substr(left_bracket_pos, right_bracket_pos - left_bracket_pos));
+
+    std::vector<std::pair<size_t, size_t>> function_call_identifiers =
+        GetFunctionCallIdentifiers(function_body);
+    if (function_call_identifiers.empty()) {
+        return false;
+    }
+    auto function_call_identifier = generator_.GetRandomElement(function_call_identifiers);
+
+    std::vector<std::string> builtin_functions{"all",
+                                               "any",
+                                               "select",
+                                               "arrayLength",
+                                               "abs",
+                                               "acos",
+                                               "acosh",
+                                               "asin",
+                                               "asinh",
+                                               "atan",
+                                               "atanh",
+                                               "atan2",
+                                               "ceil",
+                                               "clamp",
+                                               "cos",
+                                               "cosh",
+                                               "cross",
+                                               "degrees",
+                                               "distance",
+                                               "exp",
+                                               "exp2",
+                                               "faceForward",
+                                               "floor",
+                                               "fma",
+                                               "fract",
+                                               "frexp",
+                                               "inverseSqrt",
+                                               "ldexp",
+                                               "length",
+                                               "log",
+                                               "log2",
+                                               "max",
+                                               "min",
+                                               "mix",
+                                               "modf",
+                                               "normalize",
+                                               "pow",
+                                               "quantizeToF16",
+                                               "radians",
+                                               "reflect",
+                                               "refract",
+                                               "round",
+                                               "saturate",
+                                               "sign",
+                                               "sin",
+                                               "sinh",
+                                               "smoothstep",
+                                               "sqrt",
+                                               "step",
+                                               "tan",
+                                               "tanh",
+                                               "trunc",
+                                               "abs",
+                                               "clamp",
+                                               "countLeadingZeros",
+                                               "countOneBits",
+                                               "countTrailingZeros",
+                                               "extractBits",
+                                               "firstLeadingBit",
+                                               "firstTrailingBit",
+                                               "insertBits",
+                                               "max",
+                                               "min",
+                                               "reverseBits",
+                                               "determinant",
+                                               "transpose",
+                                               "dot",
+                                               "dpdx",
+                                               "dpdxCoarse",
+                                               "dpdxFine",
+                                               "dpdy",
+                                               "dpdyCoarse",
+                                               "dpdyFine",
+                                               "fwidth",
+                                               "fwidthCoarse",
+                                               "fwidthFine",
+                                               "textureDimensions",
+                                               "textureGather",
+                                               "textureGatherCompare",
+                                               "textureLoad",
+                                               "textureNumLayers",
+                                               "textureNumLevels",
+                                               "textureNumSamples",
+                                               "textureSample",
+                                               "textureSampleBias",
+                                               "textureSampleCompare",
+                                               "textureSampleCompareLevel",
+                                               "textureSampleGrad",
+                                               "textureSampleLevel",
+                                               "textureStore",
+                                               "atomicLoad",
+                                               "atomicStore",
+                                               "atomicAdd",
+                                               "atomicSub",
+                                               "atomicMax",
+                                               "atomicMin",
+                                               "atomicAnd",
+                                               "atomicOr",
+                                               "atomicXor",
+                                               "pack4x8snorm",
+                                               "pack4x8unorm",
+                                               "pack2x16snorm",
+                                               "pack2x16unorm",
+                                               "pack2x16float",
+                                               "unpack4x8snorm",
+                                               "unpack4x8unorm",
+                                               "unpack2x16snorm",
+                                               "unpack2x16unorm",
+                                               "unpack2x16float",
+                                               "storageBarrier",
+                                               "workgroupBarrier"};
+    wgsl_code.replace(left_bracket_pos + function_call_identifier.first,
+                      function_call_identifier.second,
+                      generator_.GetRandomElement(builtin_functions));
+    return true;
+}
+
 }  // namespace tint::fuzzers::regex_fuzzer
diff --git a/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h
index 23c45bb..fde4611 100644
--- a/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h
+++ b/src/tint/fuzzers/tint_regex_fuzzer/wgsl_mutator.h
@@ -15,6 +15,7 @@
 #ifndef SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_WGSL_MUTATOR_H_
 #define SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_WGSL_MUTATOR_H_
 
+#include <optional>
 #include <string>
 #include <utility>
 #include <vector>
@@ -23,152 +24,207 @@
 
 namespace tint::fuzzers::regex_fuzzer {
 
-/// A function that given a delimiter, returns a vector that contains
-/// all the positions of the delimiter in the WGSL code.
-/// @param delimiter - the delimiter of the enclosed region.
-/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
-/// @return a vector with the positions of the delimiter in the WGSL code.
-std::vector<size_t> FindDelimiterIndices(const std::string& delimiter,
-                                         const std::string& wgsl_code);
+/// Class encapsulating code for regex-based mutation of WGSL shaders.
+class WgslMutator {
+  public:
+    /// Constructor
+    /// @param generator - pseudo-random generator to use in mutator
+    explicit WgslMutator(RandomGenerator& generator);
 
-/// A function that finds all the identifiers in a WGSL-like string.
-/// @param wgsl_code - the WGSL-like string where the identifiers will be found.
-/// @return a vector with the positions and the length of all the
-/// identifiers in wgsl_code.
-std::vector<std::pair<size_t, size_t>> GetIdentifiers(const std::string& wgsl_code);
+    /// A function that, given WGSL-like string and a delimiter,
+    /// generates another WGSL-like string by picking two random regions
+    /// enclosed by the delimiter and swapping them.
+    /// @param delimiter - the delimiter that will be used to find enclosed regions.
+    /// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+    /// @return true if a swap happened or false otherwise.
+    bool SwapRandomIntervals(const std::string& delimiter, std::string& wgsl_code);
 
-/// A function that returns returns the starting position
-/// and the length of all the integer literals in a WGSL-like string.
-/// @param wgsl_code - the WGSL-like string where the int literals
-/// will be found.
-/// @return a vector with the starting positions and the length
-/// of all the integer literals.
-std::vector<std::pair<size_t, size_t>> GetIntLiterals(const std::string& wgsl_code);
+    /// A function that, given a WGSL-like string and a delimiter,
+    /// generates another WGSL-like string by deleting a random
+    /// region enclosed by the delimiter.
+    /// @param delimiter - the delimiter that will be used to find enclosed regions.
+    /// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+    /// @return true if a deletion happened or false otherwise.
+    bool DeleteRandomInterval(const std::string& delimiter, std::string& wgsl_code);
 
-/// Finds a possible closing brace corresponding to the opening
-/// brace at position opening_bracket_pos.
-/// @param opening_bracket_pos - the position of the opening brace.
-/// @param wgsl_code - the WGSL-like string where the closing brace.
-/// @return the position of the closing bracket or 0 if there is no closing
-/// brace.
-size_t FindClosingBrace(size_t opening_bracket_pos, const std::string& wgsl_code);
+    /// A function that, given a WGSL-like string and a delimiter,
+    /// generates another WGSL-like string by duplicating a random
+    /// region enclosed by the delimiter.
+    /// @param delimiter - the delimiter that will be used to find enclosed regions.
+    /// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+    /// @return true if a duplication happened or false otherwise.
+    bool DuplicateRandomInterval(const std::string& delimiter, std::string& wgsl_code);
 
-/// Returns the starting_position of the bodies of the functions
-/// that follow the regular expression: fn.*?->.*?\\{, which searches for the
-/// keyword fn followed by the function name, its return type and opening brace.
-/// @param wgsl_code - the WGSL-like string where the functions will be
-/// searched.
-/// @return a vector with the starting position of the function bodies in
-/// wgsl_code.
-std::vector<size_t> GetFunctionBodyPositions(const std::string& wgsl_code);
+    /// Replaces a randomly-chosen identifier in wgsl_code.
+    /// @param wgsl_code - WGSL-like string where the replacement will occur.
+    /// @return true if a replacement happened or false otherwise.
+    bool ReplaceRandomIdentifier(std::string& wgsl_code);
 
-/// Given 4 indices, idx1, idx2, idx3 and idx4 it swaps the regions
-/// in the interval (idx1, idx2] with the region in the interval (idx3, idx4]
-/// in wgsl_text.
-/// @param idx1 - starting index of the first region.
-/// @param reg1_len - length of the first region.
-/// @param idx2 - starting index of the second region.
-/// @param reg2_len - length of the second region.
-/// @param wgsl_code - the string where the swap will occur.
-void SwapIntervals(size_t idx1,
-                   size_t reg1_len,
-                   size_t idx2,
-                   size_t reg2_len,
-                   std::string& wgsl_code);
+    /// Replaces the value of a randomly-chosen integer with one of
+    /// the values in the set {INT_MAX, INT_MIN, 0, -1}.
+    /// @param wgsl_code - WGSL-like string where the replacement will occur.
+    /// @return true if a replacement happened or false otherwise.
+    bool ReplaceRandomIntLiteral(std::string& wgsl_code);
 
-/// Given index idx1 it delets the region of length interval_len
-/// starting at index idx1;
-/// @param idx1 - starting index of the first region.
-/// @param reg_len - terminating index of the second region.
-/// @param wgsl_code - the string where the swap will occur.
-void DeleteInterval(size_t idx1, size_t reg_len, std::string& wgsl_code);
+    /// Inserts a return statement in a randomly chosen function of a
+    /// WGSL-like string. The return value is a randomly-chosen identifier
+    /// or literal in the string.
+    /// @param wgsl_code - WGSL-like string that will be mutated.
+    /// @return true if the mutation was succesful or false otherwise.
+    bool InsertReturnStatement(std::string& wgsl_code);
 
-/// Given 2 indices, idx1, idx2, it inserts the region of length
-/// reg1_len starting at idx1 after idx2.
-/// @param idx1 - starting index of region.
-/// @param reg1_len - length of the region.
-/// @param idx2 - the position where the region will be inserted.
-/// @param wgsl_code - the string where the swap will occur.
-void DuplicateInterval(size_t idx1, size_t reg1_len, size_t idx2, std::string& wgsl_code);
+    /// Inserts a break or continue statement in a randomly chosen loop of a WGSL-like string.
+    /// @param wgsl_code - WGSL-like string that will be mutated.
+    /// @return true if the mutation was succesful or false otherwise.
+    bool InsertBreakOrContinue(std::string& wgsl_code);
 
-/// Replaces a region of a WGSL-like string of length id2_len starting
-/// at position idx2 with a region of length id1_len starting at
-/// position idx1.
-/// @param idx1 - starting position of the first region.
-/// @param id1_len - length of the first region.
-/// @param idx2 - starting position of the second region.
-/// @param id2_len - length of the second region.
-/// @param wgsl_code - the string where the replacement will occur.
-void ReplaceRegion(size_t idx1,
-                   size_t id1_len,
-                   size_t idx2,
-                   size_t id2_len,
-                   std::string& wgsl_code);
+    /// A function that, given WGSL-like string, generates a new WGSL-like string by replacing one
+    /// randomly-chosen operator in the original string with another operator.
+    /// @param wgsl_code - the initial WGSL-like string that will be mutated.
+    /// @return true if an operator replacement happened or false otherwise.
+    bool ReplaceRandomOperator(std::string& wgsl_code);
 
-/// Replaces an interval of length `length` starting at start_index
-/// with the `replacement_text`.
-/// @param start_index - starting position of the interval to be replaced.
-/// @param length - length of the interval to be replaced.
-/// @param replacement_text - the interval that will be used as a replacement.
-/// @param wgsl_code - the WGSL-like string where the replacement will occur.
-void ReplaceInterval(size_t start_index,
-                     size_t length,
-                     std::string replacement_text,
-                     std::string& wgsl_code);
+    /// Given a WGSL-like string, replaces a random identifier that appears to be a function call
+    /// with the name of a built-in function. This will often lead to an invalid module, as the
+    /// mutation does not aim to check whether the original and replacement function have the same
+    /// number or types of arguments.
+    /// @param wgsl_code - the initial WGSL-like string that will be mutated.
+    /// @return true if a function call replacement happened or false otherwise.
+    bool ReplaceFunctionCallWithBuiltin(std::string& wgsl_code);
 
-/// A function that, given WGSL-like string and a delimiter,
-/// generates another WGSL-like string by picking two random regions
-/// enclosed by the delimiter and swapping them.
-/// @param delimiter - the delimiter that will be used to find enclosed regions.
-/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
-/// @param generator - the random number generator.
-/// @return true if a swap happened or false otherwise.
-bool SwapRandomIntervals(const std::string& delimiter,
-                         std::string& wgsl_code,
-                         RandomGenerator& generator);
+  protected:
+    /// Given index idx1 it delets the region of length interval_len
+    /// starting at index idx1;
+    /// @param idx1 - starting index of the first region.
+    /// @param reg_len - terminating index of the second region.
+    /// @param wgsl_code - the string where the swap will occur.
+    void DeleteInterval(size_t idx1, size_t reg_len, std::string& wgsl_code);
 
-/// A function that, given a WGSL-like string and a delimiter,
-/// generates another WGSL-like string by deleting a random
-/// region enclosed by the delimiter.
-/// @param delimiter - the delimiter that will be used to find enclosed regions.
-/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
-/// @param generator - the random number generator.
-/// @return true if a deletion happened or false otherwise.
-bool DeleteRandomInterval(const std::string& delimiter,
-                          std::string& wgsl_code,
-                          RandomGenerator& generator);
+    /// Given 2 indices, idx1, idx2, it inserts the region of length
+    /// reg1_len starting at idx1 after idx2.
+    /// @param idx1 - starting index of region.
+    /// @param reg1_len - length of the region.
+    /// @param idx2 - the position where the region will be inserted.
+    /// @param wgsl_code - the string where the swap will occur.
+    void DuplicateInterval(size_t idx1, size_t reg1_len, size_t idx2, std::string& wgsl_code);
 
-/// A function that, given a WGSL-like string and a delimiter,
-/// generates another WGSL-like string by duplicating a random
-/// region enclosed by the delimiter.
-/// @param delimiter - the delimiter that will be used to find enclosed regions.
-/// @param wgsl_code - the initial string (WGSL code) that will be mutated.
-/// @param generator - the random number generator.
-/// @return true if a duplication happened or false otherwise.
-bool DuplicateRandomInterval(const std::string& delimiter,
-                             std::string& wgsl_code,
-                             RandomGenerator& generator);
+    /// Finds a possible closing brace corresponding to the opening
+    /// brace at position opening_bracket_pos.
+    /// @param opening_bracket_pos - the position of the opening brace.
+    /// @param wgsl_code - the WGSL-like string where the closing brace.
+    /// @return the position of the closing bracket or 0 if there is no closing
+    /// brace.
+    size_t FindClosingBrace(size_t opening_bracket_pos, const std::string& wgsl_code);
 
-/// Replaces a randomly-chosen identifier in wgsl_code.
-/// @param wgsl_code - WGSL-like string where the replacement will occur.
-/// @param generator - the random number generator.
-/// @return true if a replacement happened or false otherwise.
-bool ReplaceRandomIdentifier(std::string& wgsl_code, RandomGenerator& generator);
+    /// Returns the starting position of the bodies of the functions identified by an appropriate
+    /// function, together with a boolean indicating whether the function returns a value or not.
+    /// @param wgsl_code - the WGSL-like string where the functions will be
+    /// searched.
+    /// @return a vector of pairs, where each pair provides the starting position of the function
+    /// body, and the value true if and only if the function returns a value.
+    std::vector<std::pair<size_t, bool>> GetFunctionBodyPositions(const std::string& wgsl_code);
 
-/// Replaces the value of a randomly-chosen integer with one of
-/// the values in the set {INT_MAX, INT_MIN, 0, -1}.
-/// @param wgsl_code - WGSL-like string where the replacement will occur.
-/// @param generator - the random number generator.
-/// @return true if a replacement happened or false otherwise.
-bool ReplaceRandomIntLiteral(std::string& wgsl_code, RandomGenerator& generator);
+    /// Returns the starting position of the bodies of the loops identified by an appropriate
+    /// regular expressions.
+    /// @param wgsl_code - the WGSL-like string in which loops will be searched for.
+    /// @return a vector with the starting position of the loop bodies in wgsl_code.
+    std::vector<size_t> GetLoopBodyPositions(const std::string& wgsl_code);
 
-/// Inserts a return statement in a randomly chosen function of a
-/// WGSL-like string. The return value is a randomly-chosen identifier
-/// or literal in the string.
-/// @param wgsl_code - WGSL-like string that will be mutated.
-/// @param generator - the random number generator.
-/// @return true if the mutation was succesful or false otherwise.
-bool InsertReturnStatement(std::string& wgsl_code, RandomGenerator& generator);
+    /// A function that finds all the identifiers in a WGSL-like string.
+    /// @param wgsl_code - the WGSL-like string where the identifiers will be found.
+    /// @return a vector with the positions and the length of all the
+    /// identifiers in wgsl_code.
+    std::vector<std::pair<size_t, size_t>> GetIdentifiers(const std::string& wgsl_code);
+
+    /// A function that finds the identifiers in a WGSL-like string that appear to be used as
+    /// function names in function call expressions.
+    /// @param wgsl_code - the WGSL-like string where the identifiers will be found.
+    /// @return a vector with the positions and the length of all the
+    /// identifiers in wgsl_code.
+    std::vector<std::pair<size_t, size_t>> GetFunctionCallIdentifiers(const std::string& wgsl_code);
+
+    /// A function that returns returns the starting position
+    /// and the length of all the integer literals in a WGSL-like string.
+    /// @param wgsl_code - the WGSL-like string where the int literals
+    /// will be found.
+    /// @return a vector with the starting positions and the length
+    /// of all the integer literals.
+    std::vector<std::pair<size_t, size_t>> GetIntLiterals(const std::string& wgsl_code);
+
+    /// Replaces a region of a WGSL-like string of length id2_len starting
+    /// at position idx2 with a region of length id1_len starting at
+    /// position idx1.
+    /// @param idx1 - starting position of the first region.
+    /// @param id1_len - length of the first region.
+    /// @param idx2 - starting position of the second region.
+    /// @param id2_len - length of the second region.
+    /// @param wgsl_code - the string where the replacement will occur.
+    void ReplaceRegion(size_t idx1,
+                       size_t id1_len,
+                       size_t idx2,
+                       size_t id2_len,
+                       std::string& wgsl_code);
+
+    /// Given 4 indices, idx1, idx2, idx3 and idx4 it swaps the regions
+    /// in the interval (idx1, idx2] with the region in the interval (idx3, idx4]
+    /// in wgsl_text.
+    /// @param idx1 - starting index of the first region.
+    /// @param reg1_len - length of the first region.
+    /// @param idx2 - starting index of the second region.
+    /// @param reg2_len - length of the second region.
+    /// @param wgsl_code - the string where the swap will occur.
+    void SwapIntervals(size_t idx1,
+                       size_t reg1_len,
+                       size_t idx2,
+                       size_t reg2_len,
+                       std::string& wgsl_code);
+
+    /// Finds the next occurrence of an operator in a WGSL-like string from a given starting
+    /// position, wrapping around to the start of the string if no operator is found before reaching
+    /// the end, and returning empty of no operator is found at all. There is no guarantee that the
+    /// result will correspond to a WGSL operator token, e.g. the identified characters could be
+    /// part of a comment, or e.g. the file might contain >>=, in which case the operator
+    /// >= will be identified should it happen that the starting index corresponds to the second >
+    /// character of this operator. Given that the regex mutator does not aim to guarantee
+    /// well-formed programs, these issues are acceptable.
+    /// @param wgsl_code - the WGSL-like string in which operator occurrences will be found.
+    /// @param start_index - the index at which search should start
+    /// @return empty if no operator is found, otherwise a pair comprising the index at which the
+    /// operator starts and the character length of the operator.
+    std::optional<std::pair<uint32_t, uint32_t>> FindOperatorOccurrence(
+        const std::string& wgsl_code,
+        uint32_t start_index);
+
+  private:
+    /// A function that given a delimiter, returns a vector that contains
+    /// all the positions of the delimiter in the WGSL code.
+    /// @param delimiter - the delimiter of the enclosed region.
+    /// @param wgsl_code - the initial string (WGSL code) that will be mutated.
+    /// @return a vector with the positions of the delimiter in the WGSL code.
+    std::vector<size_t> FindDelimiterIndices(const std::string& delimiter,
+                                             const std::string& wgsl_code);
+
+    /// Replaces an interval of length `length` starting at start_index
+    /// with the `replacement_text`.
+    /// @param start_index - starting position of the interval to be replaced.
+    /// @param length - length of the interval to be replaced.
+    /// @param replacement_text - the interval that will be used as a replacement.
+    /// @param wgsl_code - the WGSL-like string where the replacement will occur.
+    void ReplaceInterval(size_t start_index,
+                         size_t length,
+                         std::string replacement_text,
+                         std::string& wgsl_code);
+
+    /// Given a string representing a WGSL operator, randomly returns a different WGSL operator in
+    /// the same category as the original, where the three categories are assignment operators (such
+    /// as = and +=), expression operators (such as + and ^) and increment operators (++ and --).
+    /// @param existing_operator - the characters comprising some WGSL operator
+    /// @return another WGSL operator falling into the same category.
+    std::string ChooseRandomReplacementForOperator(const std::string& existing_operator);
+
+    RandomGenerator& generator_;
+};
+
 }  // namespace tint::fuzzers::regex_fuzzer
 
 #endif  // SRC_TINT_FUZZERS_TINT_REGEX_FUZZER_WGSL_MUTATOR_H_
diff --git a/src/tint/intrinsics.def b/src/tint/intrinsics.def
index ab77532..16f2ff4 100644
--- a/src/tint/intrinsics.def
+++ b/src/tint/intrinsics.def
@@ -134,6 +134,7 @@
 match fi32f16: f32 | f16 | i32
 match iu32: i32 | u32
 match aiu32: ai | i32 | u32
+match afi32f16: ai | af | f32 | i32 | f16
 match scalar: f32 | f16 | i32 | u32 | bool
 match abstract_or_scalar: ai | af | f32 | f16 | i32 | u32 | bool
 match af_f32: af | f32
@@ -822,8 +823,8 @@
 @const op ~ <T: aiu32>(T) -> T
 @const op ~ <T: aiu32, N: num> (vec<N, T>) -> vec<N, T>
 
-op - <T: fi32f16>(T) -> T
-op - <T: fi32f16, N: num> (vec<N, T>) -> vec<N, T>
+@const op - <T: afi32f16>(T) -> T
+@const op - <T: afi32f16, N: num> (vec<N, T>) -> vec<N, T>
 
 ////////////////////////////////////////////////////////////////////////////////
 // Binary Operators                                                           //
diff --git a/src/tint/resolver/const_eval.cc b/src/tint/resolver/const_eval.cc
index 4a8c097..e2b16b5 100644
--- a/src/tint/resolver/const_eval.cc
+++ b/src/tint/resolver/const_eval.cc
@@ -18,6 +18,7 @@
 #include <limits>
 #include <optional>
 #include <string>
+#include <type_traits>
 #include <unordered_map>
 #include <utility>
 
@@ -45,14 +46,38 @@
 
 namespace {
 
-/// TypeDispatch is a helper for calling the function `f`, passing a single zero-value argument of
-/// the C++ type that corresponds to the sem::Type `type`. For example, calling `TypeDispatch()`
-/// with a type of `sem::I32*` will call the function f with a single argument of `i32(0)`.
+/// Helper that calls 'f' passing in `c`'s value
+template <typename F>
+auto aiu32Dispatch(const sem::Constant* c, F&& f) {
+    return Switch(
+        c->Type(), [&](const sem::AbstractInt*) { return f(c->As<AInt>()); },
+        [&](const sem::I32*) { return f(c->As<i32>()); },
+        [&](const sem::U32*) { return f(c->As<u32>()); });
+}
+
+/// Helper that calls 'f' passing in `c`'s value
+template <typename F>
+auto afi32f16Dispatch(const sem::Constant* c, F&& f) {
+    return Switch(
+        c->Type(), [&](const sem::AbstractInt*) { return f(c->As<AInt>()); },
+        [&](const sem::AbstractFloat*) { return f(c->As<AFloat>()); },
+        [&](const sem::F32*) { return f(c->As<f32>()); },
+        [&](const sem::I32*) { return f(c->As<i32>()); },
+        [&](const sem::F16*) {
+            // TODO(crbug.com/tint/1502): Support const eval for f16
+            return nullptr;
+        });
+}
+
+/// ZeroTypeDispatch is a helper for calling the function `f`, passing a single zero-value argument
+/// of the C++ type that corresponds to the sem::Type `type`. For example, calling
+/// `ZeroTypeDispatch()` with a type of `sem::I32*` will call the function f with a single argument
+/// of `i32(0)`.
 /// @returns the value returned by calling `f`.
 /// @note `type` must be a scalar or abstract numeric type. Other types will not call `f`, and will
 /// return the zero-initialized value of the return type for `f`.
 template <typename F>
-auto TypeDispatch(const sem::Type* type, F&& f) {
+auto ZeroTypeDispatch(const sem::Type* type, F&& f) {
     return Switch(
         type,                                                     //
         [&](const sem::AbstractInt*) { return f(AInt(0)); },      //
@@ -64,20 +89,6 @@
         [&](const sem::Bool*) { return f(static_cast<bool>(0)); });
 }
 
-/// IntegerDispatch is a helper for calling the function `f`, passing the integer value of the
-/// constant c.
-/// @returns the value returned by calling `f`.
-/// @note `c` must be of an integer type. Other types will not call `f`, and will return the
-/// zero-initialized value of the return type for `f`
-template <typename F>
-auto IntegerDispatch(const sem::Constant* c, F&& f) {
-    return Switch(
-        c->Type(),                                                  //
-        [&](const sem::AbstractInt*) { return f(c->As<AInt>()); },  //
-        [&](const sem::I32*) { return f(c->As<i32>()); },           //
-        [&](const sem::U32*) { return f(c->As<u32>()); });
-}
-
 /// @returns `value` if `T` is not a Number, otherwise ValueOf returns the inner value of the
 /// Number.
 template <typename T>
@@ -142,7 +153,7 @@
             return this;
         }
         bool failed = false;
-        auto* res = TypeDispatch(target_ty, [&](auto zero_to) -> const Constant* {
+        auto* res = ZeroTypeDispatch(target_ty, [&](auto zero_to) -> const Constant* {
             // `T` is the source type, `value` is the source value.
             // `TO` is the target type.
             using TO = std::decay_t<decltype(zero_to)>;
@@ -333,7 +344,7 @@
             return CreateComposite(builder, s, std::move(zeros));
         },
         [&](Default) -> const Constant* {
-            return TypeDispatch(type, [&](auto zero) -> const Constant* {
+            return ZeroTypeDispatch(type, [&](auto zero) -> const Constant* {
                 return CreateElement(builder, type, zero);
             });
         });
@@ -643,12 +654,34 @@
                                              sem::Expression const* const* args,
                                              size_t) {
     return TransformElements(builder, args[0]->ConstantValue(), [&](const sem::Constant* c) {
-        return IntegerDispatch(c, [&](auto i) {  //
+        return aiu32Dispatch(c, [&](auto i) {  //
             return CreateElement(builder, c->Type(), decltype(i)(~i.value));
         });
     });
 }
 
+const sem::Constant* ConstEval::OpMinus(const sem::Type*,
+                                        sem::Expression const* const* args,
+                                        size_t) {
+    return TransformElements(builder, args[0]->ConstantValue(), [&](const sem::Constant* c) {
+        return afi32f16Dispatch(c, [&](auto i) {  //
+            // For signed integrals, avoid C++ UB by not negating the smallest negative number. In
+            // WGSL, this operation is well defined to return the same value, see:
+            // https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr.
+            using T = UnwrapNumber<decltype(i)>;
+            if constexpr (std::is_integral_v<T>) {
+                auto v = i.value;
+                if (v != std::numeric_limits<T>::min()) {
+                    v = -v;
+                }
+                return CreateElement(builder, c->Type(), decltype(i)(v));
+            } else {
+                return CreateElement(builder, c->Type(), decltype(i)(-i.value));
+            }
+        });
+    });
+}
+
 utils::Result<const sem::Constant*> ConstEval::Convert(const sem::Type* target_ty,
                                                        const sem::Constant* value,
                                                        const Source& source) {
diff --git a/src/tint/resolver/const_eval.h b/src/tint/resolver/const_eval.h
index b1e7352..6a85c85 100644
--- a/src/tint/resolver/const_eval.h
+++ b/src/tint/resolver/const_eval.h
@@ -187,6 +187,15 @@
                                       sem::Expression const* const* args,
                                       size_t num_args);
 
+    /// Minus operator '-'
+    /// @param ty the expression type
+    /// @param args the input arguments
+    /// @param num_args the number of input arguments (must be 1)
+    /// @return the result value, or null if the value cannot be calculated
+    const sem::Constant* OpMinus(const sem::Type* ty,
+                                 sem::Expression const* const* args,
+                                 size_t num_args);
+
   private:
     /// Adds the given error message to the diagnostics
     void AddError(const std::string& msg, const Source& source) const;
diff --git a/src/tint/resolver/const_eval_test.cc b/src/tint/resolver/const_eval_test.cc
index dcc1604..7991f6f 100644
--- a/src/tint/resolver/const_eval_test.cc
+++ b/src/tint/resolver/const_eval_test.cc
@@ -13,6 +13,7 @@
 // limitations under the License.
 
 #include <cmath>
+#include <type_traits>
 
 #include "gtest/gtest.h"
 #include "src/tint/resolver/resolver_test_helper.h"
@@ -2930,13 +2931,36 @@
 namespace unary_op {
 
 template <typename T>
+auto Highest() {
+    return T(T::kHighest);
+}
+
+template <typename T>
+auto Lowest() {
+    return T(T::kLowest);
+}
+
+template <typename T>
+constexpr auto Negate(const Number<T>& v) {
+    // For signed integrals, avoid C++ UB by not negating the smallest negative number. In
+    // WGSL, this operation is well defined to return the same value, see:
+    // https://gpuweb.github.io/gpuweb/wgsl/#arithmetic-expr.
+    if constexpr (std::is_integral_v<T> && std::is_signed_v<T>) {
+        if (v == std::numeric_limits<T>::min()) {
+            return v;
+        }
+    }
+    return -v;
+}
+
+template <typename T>
 struct Values {
     T input;
     T expect;
 };
 
 struct Case {
-    std::variant<Values<AInt>, Values<u32>, Values<i32>> values;
+    std::variant<Values<AInt>, Values<AFloat>, Values<u32>, Values<i32>, Values<f32>> values;
 };
 
 static std::ostream& operator<<(std::ostream& o, const Case& c) {
@@ -2952,6 +2976,8 @@
 using ResolverConstEvalUnaryOpTest = ResolverTestWithParam<std::tuple<ast::UnaryOp, Case>>;
 
 TEST_P(ResolverConstEvalUnaryOpTest, Test) {
+    Enable(ast::Extension::kF16);
+
     auto op = std::get<0>(GetParam());
     auto c = std::get<1>(GetParam());
     std::visit(
@@ -3000,6 +3026,60 @@
                                               C(2_i, -3_i),
                                               C(-3_i, 2_i),
                                           })));
+
+INSTANTIATE_TEST_SUITE_P(Negation,
+                         ResolverConstEvalUnaryOpTest,
+                         testing::Combine(testing::Values(ast::UnaryOp::kNegation),
+                                          testing::ValuesIn({
+                                              // AInt
+                                              C(0_a, -0_a),
+                                              C(-0_a, 0_a),
+                                              C(1_a, -1_a),
+                                              C(-1_a, 1_a),
+                                              C(Highest<AInt>(), -Highest<AInt>()),
+                                              C(-Highest<AInt>(), Highest<AInt>()),
+                                              C(Lowest<AInt>(), Negate(Lowest<AInt>())),
+                                              C(Negate(Lowest<AInt>()), Lowest<AInt>()),
+                                              // i32
+                                              C(0_i, -0_i),
+                                              C(-0_i, 0_i),
+                                              C(1_i, -1_i),
+                                              C(-1_i, 1_i),
+                                              C(Highest<i32>(), -Highest<i32>()),
+                                              C(-Highest<i32>(), Highest<i32>()),
+                                              C(Lowest<i32>(), Negate(Lowest<i32>())),
+                                              C(Negate(Lowest<i32>()), Lowest<i32>()),
+                                              // AFloat
+                                              C(0.0_a, -0.0_a),
+                                              C(-0.0_a, 0.0_a),
+                                              C(1.0_a, -1.0_a),
+                                              C(-1.0_a, 1.0_a),
+                                              C(Highest<AFloat>(), -Highest<AFloat>()),
+                                              C(-Highest<AFloat>(), Highest<AFloat>()),
+                                              C(Lowest<AFloat>(), Negate(Lowest<AFloat>())),
+                                              C(Negate(Lowest<AFloat>()), Lowest<AFloat>()),
+                                              // f32
+                                              C(0.0_f, -0.0_f),
+                                              C(-0.0_f, 0.0_f),
+                                              C(1.0_f, -1.0_f),
+                                              C(-1.0_f, 1.0_f),
+                                              C(Highest<f32>(), -Highest<f32>()),
+                                              C(-Highest<f32>(), Highest<f32>()),
+                                              C(Lowest<f32>(), Negate(Lowest<f32>())),
+                                              C(Negate(Lowest<f32>()), Lowest<f32>()),
+                                          })));
+
+// Make sure UBSan doesn't trip on C++'s undefined behaviour of negating the smallest negative
+// number.
+TEST_F(ResolverConstEvalTest, UnaryNegateLowestAbstract) {
+    // const break_me = -(-9223372036854775808);
+    auto* c = GlobalConst("break_me", nullptr, Negation(Negation(Expr(9223372036854775808_a))));
+    (void)c;
+    EXPECT_TRUE(r()->Resolve()) << r()->error();
+    auto* sem = Sem().Get(c);
+    EXPECT_EQ(sem->ConstantValue()->As<AInt>(), 9223372036854775808_a);
+}
+
 }  // namespace unary_op
 
 }  // namespace
diff --git a/src/tint/resolver/intrinsic_table.inl b/src/tint/resolver/intrinsic_table.inl
index e7b44a3..811aa34 100644
--- a/src/tint/resolver/intrinsic_table.inl
+++ b/src/tint/resolver/intrinsic_table.inl
@@ -1832,8 +1832,52 @@
   return ss.str();
 }
 
-/// TypeMatcher for 'match scalar'
+/// TypeMatcher for 'match afi32f16'
 /// @see src/tint/intrinsics.def:137:7
+class Afi32F16 : public TypeMatcher {
+ public:
+  /// Checks whether the given type matches the matcher rules, and returns the
+  /// expected, canonicalized type on success.
+  /// Match may define and refine the template types and numbers in state.
+  /// @param state the MatchState
+  /// @param type the type to match
+  /// @returns the canonicalized type on match, otherwise nullptr
+  const sem::Type* Match(MatchState& state,
+                         const sem::Type* type) const override;
+  /// @param state the MatchState
+  /// @return a string representation of the matcher.
+  std::string String(MatchState* state) const override;
+};
+
+const sem::Type* Afi32F16::Match(MatchState& state, const sem::Type* ty) const {
+  if (match_af(ty)) {
+    return build_af(state);
+  }
+  if (match_ai(ty)) {
+    return build_ai(state);
+  }
+  if (match_i32(ty)) {
+    return build_i32(state);
+  }
+  if (match_f32(ty)) {
+    return build_f32(state);
+  }
+  if (match_f16(ty)) {
+    return build_f16(state);
+  }
+  return nullptr;
+}
+
+std::string Afi32F16::String(MatchState*) const {
+  std::stringstream ss;
+  // Note: We pass nullptr to the TypeMatcher::String() functions, as 'matcher's do not support
+  // template arguments, nor can they match sub-types. As such, they have no use for the MatchState.
+  ss << Ai().String(nullptr) << ", " << Af().String(nullptr) << ", " << F32().String(nullptr) << ", " << I32().String(nullptr) << " or " << F16().String(nullptr);
+  return ss.str();
+}
+
+/// TypeMatcher for 'match scalar'
+/// @see src/tint/intrinsics.def:138:7
 class Scalar : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -1877,7 +1921,7 @@
 }
 
 /// TypeMatcher for 'match abstract_or_scalar'
-/// @see src/tint/intrinsics.def:138:7
+/// @see src/tint/intrinsics.def:139:7
 class AbstractOrScalar : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -1927,7 +1971,7 @@
 }
 
 /// TypeMatcher for 'match af_f32'
-/// @see src/tint/intrinsics.def:139:7
+/// @see src/tint/intrinsics.def:140:7
 class AfF32 : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -1962,7 +2006,7 @@
 }
 
 /// TypeMatcher for 'match af_f32f16'
-/// @see src/tint/intrinsics.def:140:7
+/// @see src/tint/intrinsics.def:141:7
 class AfF32F16 : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -2000,7 +2044,7 @@
 }
 
 /// TypeMatcher for 'match scalar_no_f32'
-/// @see src/tint/intrinsics.def:141:7
+/// @see src/tint/intrinsics.def:142:7
 class ScalarNoF32 : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -2041,7 +2085,7 @@
 }
 
 /// TypeMatcher for 'match scalar_no_f16'
-/// @see src/tint/intrinsics.def:142:7
+/// @see src/tint/intrinsics.def:143:7
 class ScalarNoF16 : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -2082,7 +2126,7 @@
 }
 
 /// TypeMatcher for 'match scalar_no_i32'
-/// @see src/tint/intrinsics.def:143:7
+/// @see src/tint/intrinsics.def:144:7
 class ScalarNoI32 : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -2123,7 +2167,7 @@
 }
 
 /// TypeMatcher for 'match scalar_no_u32'
-/// @see src/tint/intrinsics.def:144:7
+/// @see src/tint/intrinsics.def:145:7
 class ScalarNoU32 : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -2164,7 +2208,7 @@
 }
 
 /// TypeMatcher for 'match scalar_no_bool'
-/// @see src/tint/intrinsics.def:145:7
+/// @see src/tint/intrinsics.def:146:7
 class ScalarNoBool : public TypeMatcher {
  public:
   /// Checks whether the given type matches the matcher rules, and returns the
@@ -2205,7 +2249,7 @@
 }
 
 /// EnumMatcher for 'match f32_texel_format'
-/// @see src/tint/intrinsics.def:156:7
+/// @see src/tint/intrinsics.def:157:7
 class F32TexelFormat : public NumberMatcher {
  public:
   /// Checks whether the given number matches the enum matcher rules.
@@ -2238,7 +2282,7 @@
 }
 
 /// EnumMatcher for 'match i32_texel_format'
-/// @see src/tint/intrinsics.def:158:7
+/// @see src/tint/intrinsics.def:159:7
 class I32TexelFormat : public NumberMatcher {
  public:
   /// Checks whether the given number matches the enum matcher rules.
@@ -2270,7 +2314,7 @@
 }
 
 /// EnumMatcher for 'match u32_texel_format'
-/// @see src/tint/intrinsics.def:160:7
+/// @see src/tint/intrinsics.def:161:7
 class U32TexelFormat : public NumberMatcher {
  public:
   /// Checks whether the given number matches the enum matcher rules.
@@ -2302,7 +2346,7 @@
 }
 
 /// EnumMatcher for 'match write_only'
-/// @see src/tint/intrinsics.def:163:7
+/// @see src/tint/intrinsics.def:164:7
 class WriteOnly : public NumberMatcher {
  public:
   /// Checks whether the given number matches the enum matcher rules.
@@ -2328,7 +2372,7 @@
 }
 
 /// EnumMatcher for 'match function_private_workgroup'
-/// @see src/tint/intrinsics.def:165:7
+/// @see src/tint/intrinsics.def:166:7
 class FunctionPrivateWorkgroup : public NumberMatcher {
  public:
   /// Checks whether the given number matches the enum matcher rules.
@@ -2358,7 +2402,7 @@
 }
 
 /// EnumMatcher for 'match workgroup_or_storage'
-/// @see src/tint/intrinsics.def:166:7
+/// @see src/tint/intrinsics.def:167:7
 class WorkgroupOrStorage : public NumberMatcher {
  public:
   /// Checks whether the given number matches the enum matcher rules.
@@ -2524,6 +2568,7 @@
   Fi32F16 Fi32F16_;
   Iu32 Iu32_;
   Aiu32 Aiu32_;
+  Afi32F16 Afi32F16_;
   Scalar Scalar_;
   AbstractOrScalar AbstractOrScalar_;
   AfF32 AfF32_;
@@ -2550,7 +2595,7 @@
   ~Matchers();
 
   /// The template types, types, and type matchers
-  TypeMatcher const* const type[66] = {
+  TypeMatcher const* const type[67] = {
     /* [0] */ &template_type_0_,
     /* [1] */ &template_type_1_,
     /* [2] */ &Bool_,
@@ -2608,15 +2653,16 @@
     /* [54] */ &Fi32F16_,
     /* [55] */ &Iu32_,
     /* [56] */ &Aiu32_,
-    /* [57] */ &Scalar_,
-    /* [58] */ &AbstractOrScalar_,
-    /* [59] */ &AfF32_,
-    /* [60] */ &AfF32F16_,
-    /* [61] */ &ScalarNoF32_,
-    /* [62] */ &ScalarNoF16_,
-    /* [63] */ &ScalarNoI32_,
-    /* [64] */ &ScalarNoU32_,
-    /* [65] */ &ScalarNoBool_,
+    /* [57] */ &Afi32F16_,
+    /* [58] */ &Scalar_,
+    /* [59] */ &AbstractOrScalar_,
+    /* [60] */ &AfF32_,
+    /* [61] */ &AfF32F16_,
+    /* [62] */ &ScalarNoF32_,
+    /* [63] */ &ScalarNoF16_,
+    /* [64] */ &ScalarNoI32_,
+    /* [65] */ &ScalarNoU32_,
+    /* [66] */ &ScalarNoBool_,
   };
 
   /// The template numbers, and number matchers
@@ -7914,7 +7960,7 @@
   {
     /* [1] */
     /* name */ "U",
-    /* matcher index */ 65,
+    /* matcher index */ 66,
   },
   {
     /* [2] */
@@ -7924,7 +7970,7 @@
   {
     /* [3] */
     /* name */ "U",
-    /* matcher index */ 61,
+    /* matcher index */ 62,
   },
   {
     /* [4] */
@@ -7934,7 +7980,7 @@
   {
     /* [5] */
     /* name */ "U",
-    /* matcher index */ 62,
+    /* matcher index */ 63,
   },
   {
     /* [6] */
@@ -7944,7 +7990,7 @@
   {
     /* [7] */
     /* name */ "U",
-    /* matcher index */ 63,
+    /* matcher index */ 64,
   },
   {
     /* [8] */
@@ -7954,7 +8000,7 @@
   {
     /* [9] */
     /* name */ "U",
-    /* matcher index */ 64,
+    /* matcher index */ 65,
   },
   {
     /* [10] */
@@ -7964,7 +8010,7 @@
   {
     /* [11] */
     /* name */ "T",
-    /* matcher index */ 60,
+    /* matcher index */ 61,
   },
   {
     /* [12] */
@@ -7979,12 +8025,12 @@
   {
     /* [14] */
     /* name */ "T",
-    /* matcher index */ 58,
+    /* matcher index */ 59,
   },
   {
     /* [15] */
     /* name */ "T",
-    /* matcher index */ 57,
+    /* matcher index */ 58,
   },
   {
     /* [16] */
@@ -7994,27 +8040,27 @@
   {
     /* [17] */
     /* name */ "T",
-    /* matcher index */ 65,
+    /* matcher index */ 66,
   },
   {
     /* [18] */
     /* name */ "T",
-    /* matcher index */ 62,
+    /* matcher index */ 63,
   },
   {
     /* [19] */
     /* name */ "T",
-    /* matcher index */ 61,
+    /* matcher index */ 62,
   },
   {
     /* [20] */
     /* name */ "T",
-    /* matcher index */ 64,
+    /* matcher index */ 65,
   },
   {
     /* [21] */
     /* name */ "T",
-    /* matcher index */ 63,
+    /* matcher index */ 64,
   },
   {
     /* [22] */
@@ -8024,7 +8070,7 @@
   {
     /* [23] */
     /* name */ "T",
-    /* matcher index */ 54,
+    /* matcher index */ 57,
   },
   {
     /* [24] */
@@ -13161,7 +13207,7 @@
     /* parameters */ &kParameters[862],
     /* return matcher indices */ &kMatcherIndices[1],
     /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
-    /* const eval */ nullptr,
+    /* const eval */ &ConstEval::OpMinus,
   },
   {
     /* [423] */
@@ -13173,7 +13219,7 @@
     /* parameters */ &kParameters[863],
     /* return matcher indices */ &kMatcherIndices[39],
     /* flags */ OverloadFlags(OverloadFlag::kIsOperator, OverloadFlag::kSupportsVertexPipeline, OverloadFlag::kSupportsFragmentPipeline, OverloadFlag::kSupportsComputePipeline),
-    /* const eval */ nullptr,
+    /* const eval */ &ConstEval::OpMinus,
   },
   {
     /* [424] */
@@ -14521,8 +14567,8 @@
   },
   {
     /* [2] */
-    /* op -<T : fi32f16>(T) -> T */
-    /* op -<T : fi32f16, N : num>(vec<N, T>) -> vec<N, T> */
+    /* op -<T : afi32f16>(T) -> T */
+    /* op -<T : afi32f16, N : num>(vec<N, T>) -> vec<N, T> */
     /* num overloads */ 2,
     /* overloads */ &kOverloads[422],
   },
diff --git a/src/tint/resolver/intrinsic_table_test.cc b/src/tint/resolver/intrinsic_table_test.cc
index 30d7909..affcc76 100644
--- a/src/tint/resolver/intrinsic_table_test.cc
+++ b/src/tint/resolver/intrinsic_table_test.cc
@@ -604,8 +604,8 @@
     EXPECT_EQ(Diagnostics().str(), R"(12:34 error: no matching overload for operator - (bool)
 
 2 candidate operators:
-  operator - (T) -> T  where: T is f32, f16 or i32
-  operator - (vecN<T>) -> vecN<T>  where: T is f32, f16 or i32
+  operator - (T) -> T  where: T is abstract-int, abstract-float, f32, i32 or f16
+  operator - (vecN<T>) -> vecN<T>  where: T is abstract-int, abstract-float, f32, i32 or f16
 )");
 }
 
diff --git a/src/tint/resolver/storage_class_validation_test.cc b/src/tint/resolver/storage_class_validation_test.cc
index cb3ab16..f3e6f32 100644
--- a/src/tint/resolver/storage_class_validation_test.cc
+++ b/src/tint/resolver/storage_class_validation_test.cc
@@ -134,7 +134,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              "56:78 error: using f16 types in 'uniform' or 'storage' storage class is not "
+              "56:78 error: using f16 types in 'storage' storage class is not "
               "implemented yet");
 }
 
@@ -153,7 +153,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              "56:78 error: using f16 types in 'uniform' or 'storage' storage class is not "
+              "56:78 error: using f16 types in 'storage' storage class is not "
               "implemented yet");
 }
 
@@ -169,7 +169,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              "56:78 error: using f16 types in 'uniform' or 'storage' storage class is not "
+              "56:78 error: using f16 types in 'storage' storage class is not "
               "implemented yet");
 }
 
@@ -188,9 +188,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("56:78 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("56:78 error: using f16 types in 'storage' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, StorageBufferStructF16_TemporallyBan) {
@@ -207,9 +206,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: using f16 types in 'storage' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, StorageBufferNoErrorStructF16Aliases_TemporallyBan) {
@@ -229,9 +227,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: using f16 types in 'storage' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, StorageBufferPointer) {
@@ -421,7 +418,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              "56:78 error: using f16 types in 'uniform' or 'storage' storage class is not "
+              "56:78 error: using f16 types in 'uniform' storage class is not "
               "implemented yet");
 }
 
@@ -440,7 +437,7 @@
     ASSERT_FALSE(r()->Resolve());
 
     EXPECT_EQ(r()->error(),
-              "56:78 error: using f16 types in 'uniform' or 'storage' storage class is not "
+              "56:78 error: using f16 types in 'uniform' storage class is not "
               "implemented yet");
 }
 
@@ -455,9 +452,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("56:78 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("56:78 error: using f16 types in 'uniform' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, UniformBufferArrayF16_TemporallyBan) {
@@ -477,9 +473,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("56:78 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("56:78 error: using f16 types in 'uniform' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, UniformBufferStructF16_TemporallyBan) {
@@ -496,9 +491,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: using f16 types in 'uniform' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, UniformBufferStructF16Aliases_TemporallyBan) {
@@ -517,9 +511,8 @@
 
     ASSERT_FALSE(r()->Resolve());
 
-    EXPECT_THAT(r()->error(),
-                HasSubstr("12:34 error: using f16 types in 'uniform' or 'storage' storage "
-                          "class is not implemented yet"));
+    EXPECT_THAT(r()->error(), HasSubstr("12:34 error: using f16 types in 'uniform' storage "
+                                        "class is not implemented yet"));
 }
 
 TEST_F(ResolverStorageClassValidationTest, UniformBufferPointer) {
diff --git a/src/tint/resolver/validator.cc b/src/tint/resolver/validator.cc
index 0b8fb8c..f962e3b 100644
--- a/src/tint/resolver/validator.cc
+++ b/src/tint/resolver/validator.cc
@@ -396,7 +396,8 @@
     // TODO(tint:1473, tint:1502): Remove this error after f16 is supported in "uniform" and
     // "storage" storage class.
     if (Is<sem::F16>(sem::Type::DeepestElementOf(store_ty))) {
-        AddError("using f16 types in 'uniform' or 'storage' storage class is not implemented yet",
+        AddError("using f16 types in '" + std::string(ast::ToString(sc)) +
+                     "' storage class is not implemented yet",
                  source);
         return false;
     }
diff --git a/src/tint/sem/f16.h b/src/tint/sem/f16.h
index 72984c1..87543ed 100644
--- a/src/tint/sem/f16.h
+++ b/src/tint/sem/f16.h
@@ -22,7 +22,7 @@
 namespace tint::sem {
 
 /// A float 16 type
-class F16 : public Castable<F16, Type> {
+class F16 final : public Castable<F16, Type> {
   public:
     /// Constructor
     F16();
diff --git a/src/tint/sem/struct.h b/src/tint/sem/struct.h
index e026b11..5ee93e5 100644
--- a/src/tint/sem/struct.h
+++ b/src/tint/sem/struct.h
@@ -170,7 +170,7 @@
 };
 
 /// StructMember holds the semantic information for structure members.
-class StructMember : public Castable<StructMember, Node> {
+class StructMember final : public Castable<StructMember, Node> {
   public:
     /// Constructor
     /// @param declaration the AST declaration node
diff --git a/src/tint/sem/type.cc b/src/tint/sem/type.cc
index 9d4c469..5b2baa0 100644
--- a/src/tint/sem/type.cc
+++ b/src/tint/sem/type.cc
@@ -136,6 +136,30 @@
     return is_unsigned_scalar_or_vector() || is_signed_scalar_or_vector();
 }
 
+bool Type::is_abstract_scalar_vector() const {
+    return Is([](const Vector* v) { return v->type()->Is<sem::AbstractNumeric>(); });
+}
+
+bool Type::is_abstract_integer_vector() const {
+    return Is([](const Vector* v) { return v->type()->Is<sem::AbstractInt>(); });
+}
+
+bool Type::is_abstract_float_vector() const {
+    return Is([](const Vector* v) { return v->type()->Is<sem::AbstractFloat>(); });
+}
+
+bool Type::is_abstract_scalar_or_vector() const {
+    return Is<sem::AbstractNumeric>() || is_abstract_scalar_vector();
+}
+
+bool Type::is_abstract_integer_scalar_or_vector() const {
+    return Is<sem::AbstractInt>() || is_abstract_integer_vector();
+}
+
+bool Type::is_abstract_float_scalar_or_vector() const {
+    return Is<sem::AbstractFloat>() || is_abstract_float_vector();
+}
+
 bool Type::is_bool_vector() const {
     return Is([](const Vector* v) { return v->type()->Is<Bool>(); });
 }
diff --git a/src/tint/sem/type.h b/src/tint/sem/type.h
index 25f3a43..3866e8d 100644
--- a/src/tint/sem/type.h
+++ b/src/tint/sem/type.h
@@ -103,6 +103,18 @@
     bool is_signed_scalar_or_vector() const;
     /// @returns true if this type is an integer scalar or vector
     bool is_integer_scalar_or_vector() const;
+    /// @returns true if this type is an abstract scalar vector
+    bool is_abstract_scalar_vector() const;
+    /// @returns true if this type is an abstract integer vector
+    bool is_abstract_integer_vector() const;
+    /// @returns true if this type is an abstract float vector
+    bool is_abstract_float_vector() const;
+    /// @returns true if this type is an abstract scalar or vector
+    bool is_abstract_scalar_or_vector() const;
+    /// @returns true if this type is an abstract integer scalar or vector
+    bool is_abstract_integer_scalar_or_vector() const;
+    /// @returns true if this type is an abstract float scalar or vector
+    bool is_abstract_float_scalar_or_vector() const;
     /// @returns true if this type is a boolean vector
     bool is_bool_vector() const;
     /// @returns true if this type is boolean scalar or vector
diff --git a/src/tint/transform/decompose_memory_access.cc b/src/tint/transform/decompose_memory_access.cc
index c3d5c8e..a714581 100644
--- a/src/tint/transform/decompose_memory_access.cc
+++ b/src/tint/transform/decompose_memory_access.cc
@@ -73,7 +73,7 @@
 
 /// OffsetLiteral is an implementation of Offset that constructs a u32 literal
 /// value.
-struct OffsetLiteral : Castable<OffsetLiteral, Offset> {
+struct OffsetLiteral final : Castable<OffsetLiteral, Offset> {
     uint32_t const literal = 0;
 
     explicit OffsetLiteral(uint32_t lit) : literal(lit) {}
diff --git a/src/tint/transform/expand_compound_assignment.h b/src/tint/transform/expand_compound_assignment.h
index d38d297..1081df7 100644
--- a/src/tint/transform/expand_compound_assignment.h
+++ b/src/tint/transform/expand_compound_assignment.h
@@ -38,7 +38,7 @@
 ///
 /// This transform also handles increment and decrement statements in the same
 /// manner, by replacing `i++` with `i = i + 1`.
-class ExpandCompoundAssignment : public Castable<ExpandCompoundAssignment, Transform> {
+class ExpandCompoundAssignment final : public Castable<ExpandCompoundAssignment, Transform> {
   public:
     /// Constructor
     ExpandCompoundAssignment();
diff --git a/src/tint/transform/localize_struct_array_assignment.h b/src/tint/transform/localize_struct_array_assignment.h
index 129c849..130f8cc 100644
--- a/src/tint/transform/localize_struct_array_assignment.h
+++ b/src/tint/transform/localize_struct_array_assignment.h
@@ -27,7 +27,8 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * SimplifyPointers
-class LocalizeStructArrayAssignment : public Castable<LocalizeStructArrayAssignment, Transform> {
+class LocalizeStructArrayAssignment final
+    : public Castable<LocalizeStructArrayAssignment, Transform> {
   public:
     /// Constructor
     LocalizeStructArrayAssignment();
diff --git a/src/tint/transform/loop_to_for_loop.h b/src/tint/transform/loop_to_for_loop.h
index 0623d79..0e948c8 100644
--- a/src/tint/transform/loop_to_for_loop.h
+++ b/src/tint/transform/loop_to_for_loop.h
@@ -21,7 +21,7 @@
 
 /// LoopToForLoop is a Transform that attempts to convert WGSL `loop {}`
 /// statements into a for-loop statement.
-class LoopToForLoop : public Castable<LoopToForLoop, Transform> {
+class LoopToForLoop final : public Castable<LoopToForLoop, Transform> {
   public:
     /// Constructor
     LoopToForLoop();
diff --git a/src/tint/transform/manager.h b/src/tint/transform/manager.h
index 9f5c6bc..04bf9fe 100644
--- a/src/tint/transform/manager.h
+++ b/src/tint/transform/manager.h
@@ -27,7 +27,7 @@
 /// The inner transforms will execute in the appended order.
 /// If any inner transform fails the manager will return immediately and
 /// the error can be retrieved with the Output's diagnostics.
-class Manager : public Castable<Manager, Transform> {
+class Manager final : public Castable<Manager, Transform> {
   public:
     /// Constructor
     Manager();
diff --git a/src/tint/transform/module_scope_var_to_entry_point_param.h b/src/tint/transform/module_scope_var_to_entry_point_param.h
index e3a50f4..40e6b7d 100644
--- a/src/tint/transform/module_scope_var_to_entry_point_param.h
+++ b/src/tint/transform/module_scope_var_to_entry_point_param.h
@@ -61,7 +61,7 @@
 ///   foo(&p, sptr);
 /// }
 /// ```
-class ModuleScopeVarToEntryPointParam
+class ModuleScopeVarToEntryPointParam final
     : public Castable<ModuleScopeVarToEntryPointParam, Transform> {
   public:
     /// Constructor
diff --git a/src/tint/transform/multiplanar_external_texture.h b/src/tint/transform/multiplanar_external_texture.h
index fcb5156..afd15a1 100644
--- a/src/tint/transform/multiplanar_external_texture.h
+++ b/src/tint/transform/multiplanar_external_texture.h
@@ -50,7 +50,7 @@
 /// decoding, gamut conversion, and gamma encoding steps. Specifically
 // for BT.709 to SRGB conversion, it takes the fast path only doing the yuv->rgb
 // step and skipping all other steps.
-class MultiplanarExternalTexture : public Castable<MultiplanarExternalTexture, Transform> {
+class MultiplanarExternalTexture final : public Castable<MultiplanarExternalTexture, Transform> {
   public:
     /// BindingsMap is a map where the key is the binding location of a
     /// texture_external and the value is a struct containing the desired
@@ -60,7 +60,7 @@
     /// NewBindingPoints is consumed by the MultiplanarExternalTexture transform.
     /// Data holds information about location of each texture_external binding and
     /// which binding slots it should expand into.
-    struct NewBindingPoints : public Castable<Data, transform::Data> {
+    struct NewBindingPoints final : public Castable<Data, transform::Data> {
         /// Constructor
         /// @param bm a map to the new binding slots to use.
         explicit NewBindingPoints(BindingsMap bm);
diff --git a/src/tint/transform/num_workgroups_from_uniform.h b/src/tint/transform/num_workgroups_from_uniform.h
index 0111ccc..292c823 100644
--- a/src/tint/transform/num_workgroups_from_uniform.h
+++ b/src/tint/transform/num_workgroups_from_uniform.h
@@ -44,7 +44,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * CanonicalizeEntryPointIO
-class NumWorkgroupsFromUniform : public Castable<NumWorkgroupsFromUniform, Transform> {
+class NumWorkgroupsFromUniform final : public Castable<NumWorkgroupsFromUniform, Transform> {
   public:
     /// Constructor
     NumWorkgroupsFromUniform();
@@ -52,7 +52,7 @@
     ~NumWorkgroupsFromUniform() override;
 
     /// Configuration options for the NumWorkgroupsFromUniform transform.
-    struct Config : public Castable<Data, transform::Data> {
+    struct Config final : public Castable<Data, transform::Data> {
         /// Constructor
         /// @param ubo_bp the binding point to use for the generated uniform buffer. If ubo_bp
         /// contains no value, a free binding point will be used to ensure the generated program is
diff --git a/src/tint/transform/promote_initializers_to_let.h b/src/tint/transform/promote_initializers_to_let.h
index 41f99d7..226c7d8 100644
--- a/src/tint/transform/promote_initializers_to_let.h
+++ b/src/tint/transform/promote_initializers_to_let.h
@@ -25,7 +25,7 @@
 /// array or structure. For example, the following is not immediately expressable for HLSL:
 ///   `array<i32, 2>(1, 2)[0]`
 /// @see crbug.com/tint/406
-class PromoteInitializersToLet : public Castable<PromoteInitializersToLet, Transform> {
+class PromoteInitializersToLet final : public Castable<PromoteInitializersToLet, Transform> {
   public:
     /// Constructor
     PromoteInitializersToLet();
diff --git a/src/tint/transform/promote_side_effects_to_decl.h b/src/tint/transform/promote_side_effects_to_decl.h
index 1e629b3..d5d1126 100644
--- a/src/tint/transform/promote_side_effects_to_decl.h
+++ b/src/tint/transform/promote_side_effects_to_decl.h
@@ -23,7 +23,7 @@
 /// declarations before the statement of usage with the goal of ensuring
 /// left-to-right order of evaluation, while respecting short-circuit
 /// evaluation.
-class PromoteSideEffectsToDecl : public Castable<PromoteSideEffectsToDecl, Transform> {
+class PromoteSideEffectsToDecl final : public Castable<PromoteSideEffectsToDecl, Transform> {
   public:
     /// Constructor
     PromoteSideEffectsToDecl();
diff --git a/src/tint/transform/remove_continue_in_switch.h b/src/tint/transform/remove_continue_in_switch.h
index e706225..9e5a4d5 100644
--- a/src/tint/transform/remove_continue_in_switch.h
+++ b/src/tint/transform/remove_continue_in_switch.h
@@ -23,7 +23,7 @@
 /// bool variable, and checking if the variable is set after the switch to
 /// continue. It is necessary to work around FXC "error X3708: continue cannot
 /// be used in a switch". See crbug.com/tint/1080.
-class RemoveContinueInSwitch : public Castable<RemoveContinueInSwitch, Transform> {
+class RemoveContinueInSwitch final : public Castable<RemoveContinueInSwitch, Transform> {
   public:
     /// Constructor
     RemoveContinueInSwitch();
diff --git a/src/tint/transform/remove_phonies.h b/src/tint/transform/remove_phonies.h
index 20128a0..d04023b 100644
--- a/src/tint/transform/remove_phonies.h
+++ b/src/tint/transform/remove_phonies.h
@@ -25,7 +25,7 @@
 /// RemovePhonies is a Transform that removes all phony-assignment statements,
 /// while preserving function call expressions in the RHS of the assignment that
 /// may have side-effects.
-class RemovePhonies : public Castable<RemovePhonies, Transform> {
+class RemovePhonies final : public Castable<RemovePhonies, Transform> {
   public:
     /// Constructor
     RemovePhonies();
diff --git a/src/tint/transform/remove_unreachable_statements.h b/src/tint/transform/remove_unreachable_statements.h
index c75da3d..7f8b947 100644
--- a/src/tint/transform/remove_unreachable_statements.h
+++ b/src/tint/transform/remove_unreachable_statements.h
@@ -24,7 +24,7 @@
 
 /// RemoveUnreachableStatements is a Transform that removes all statements
 /// marked as unreachable.
-class RemoveUnreachableStatements : public Castable<RemoveUnreachableStatements, Transform> {
+class RemoveUnreachableStatements final : public Castable<RemoveUnreachableStatements, Transform> {
   public:
     /// Constructor
     RemoveUnreachableStatements();
diff --git a/src/tint/transform/renamer.h b/src/tint/transform/renamer.h
index 354acda..000aee9 100644
--- a/src/tint/transform/renamer.h
+++ b/src/tint/transform/renamer.h
@@ -23,11 +23,11 @@
 namespace tint::transform {
 
 /// Renamer is a Transform that renames all the symbols in a program.
-class Renamer : public Castable<Renamer, Transform> {
+class Renamer final : public Castable<Renamer, Transform> {
   public:
     /// Data is outputted by the Renamer transform.
     /// Data holds information about shader usage and constant buffer offsets.
-    struct Data : public Castable<Data, transform::Data> {
+    struct Data final : public Castable<Data, transform::Data> {
         /// Remappings is a map of old symbol name to new symbol name
         using Remappings = std::unordered_map<std::string, std::string>;
 
@@ -59,7 +59,7 @@
 
     /// Optional configuration options for the transform.
     /// If omitted, then the renamer will use Target::kAll.
-    struct Config : public Castable<Config, transform::Data> {
+    struct Config final : public Castable<Config, transform::Data> {
         /// Constructor
         /// @param tgt the targets to rename
         /// @param keep_unicode if false, symbols with non-ascii code-points are
diff --git a/src/tint/transform/robustness.h b/src/tint/transform/robustness.h
index 138b48c..549b666 100644
--- a/src/tint/transform/robustness.h
+++ b/src/tint/transform/robustness.h
@@ -31,7 +31,7 @@
 /// the bounds of the array. Any access before the start of the array will clamp
 /// to zero and any access past the end of the array will clamp to
 /// (array length - 1).
-class Robustness : public Castable<Robustness, Transform> {
+class Robustness final : public Castable<Robustness, Transform> {
   public:
     /// Storage class to be skipped in the transform
     enum class StorageClass {
@@ -40,7 +40,7 @@
     };
 
     /// Configuration options for the transform
-    struct Config : public Castable<Config, Data> {
+    struct Config final : public Castable<Config, Data> {
         /// Constructor
         Config();
 
diff --git a/src/tint/transform/simplify_pointers.h b/src/tint/transform/simplify_pointers.h
index 267b7b2..787c7d8 100644
--- a/src/tint/transform/simplify_pointers.h
+++ b/src/tint/transform/simplify_pointers.h
@@ -31,7 +31,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * Unshadow
-class SimplifyPointers : public Castable<SimplifyPointers, Transform> {
+class SimplifyPointers final : public Castable<SimplifyPointers, Transform> {
   public:
     /// Constructor
     SimplifyPointers();
diff --git a/src/tint/transform/single_entry_point.h b/src/tint/transform/single_entry_point.h
index 0a922a7..59aa021 100644
--- a/src/tint/transform/single_entry_point.h
+++ b/src/tint/transform/single_entry_point.h
@@ -25,10 +25,10 @@
 ///
 /// All module-scope variables, types, and functions that are not used by the
 /// target entry point will also be removed.
-class SingleEntryPoint : public Castable<SingleEntryPoint, Transform> {
+class SingleEntryPoint final : public Castable<SingleEntryPoint, Transform> {
   public:
     /// Configuration options for the transform
-    struct Config : public Castable<Config, Data> {
+    struct Config final : public Castable<Config, Data> {
         /// Constructor
         /// @param entry_point the name of the entry point to keep
         explicit Config(std::string entry_point = "");
diff --git a/src/tint/transform/unshadow.h b/src/tint/transform/unshadow.h
index ce5e975..5ffe839 100644
--- a/src/tint/transform/unshadow.h
+++ b/src/tint/transform/unshadow.h
@@ -21,7 +21,7 @@
 
 /// Unshadow is a Transform that renames any variables that shadow another
 /// variable.
-class Unshadow : public Castable<Unshadow, Transform> {
+class Unshadow final : public Castable<Unshadow, Transform> {
   public:
     /// Constructor
     Unshadow();
diff --git a/src/tint/transform/unwind_discard_functions.h b/src/tint/transform/unwind_discard_functions.h
index 3b1d838..105a9d8 100644
--- a/src/tint/transform/unwind_discard_functions.h
+++ b/src/tint/transform/unwind_discard_functions.h
@@ -36,7 +36,7 @@
 ///
 /// @note Depends on the following transforms to have been run first:
 /// * PromoteSideEffectsToDecl
-class UnwindDiscardFunctions : public Castable<UnwindDiscardFunctions, Transform> {
+class UnwindDiscardFunctions final : public Castable<UnwindDiscardFunctions, Transform> {
   public:
     /// Constructor
     UnwindDiscardFunctions();
diff --git a/src/tint/transform/vectorize_scalar_matrix_constructors.h b/src/tint/transform/vectorize_scalar_matrix_constructors.h
index 83c4ce1..31c57f0 100644
--- a/src/tint/transform/vectorize_scalar_matrix_constructors.h
+++ b/src/tint/transform/vectorize_scalar_matrix_constructors.h
@@ -20,7 +20,7 @@
 namespace tint::transform {
 
 /// A transform that converts scalar matrix constructors to the vector form.
-class VectorizeScalarMatrixConstructors
+class VectorizeScalarMatrixConstructors final
     : public Castable<VectorizeScalarMatrixConstructors, Transform> {
   public:
     /// Constructor
diff --git a/src/tint/transform/vertex_pulling.h b/src/tint/transform/vertex_pulling.h
index 7875600..92eb627 100644
--- a/src/tint/transform/vertex_pulling.h
+++ b/src/tint/transform/vertex_pulling.h
@@ -128,10 +128,10 @@
 /// code, but these are types that the data may arrive as. We need to convert
 /// these smaller types into the base types such as `f32` and `u32` for the
 /// shader to use.
-class VertexPulling : public Castable<VertexPulling, Transform> {
+class VertexPulling final : public Castable<VertexPulling, Transform> {
   public:
     /// Configuration options for the transform
-    struct Config : public Castable<Config, Data> {
+    struct Config final : public Castable<Config, Data> {
         /// Constructor
         Config();
 
diff --git a/src/tint/transform/zero_init_workgroup_memory.h b/src/tint/transform/zero_init_workgroup_memory.h
index c757725..07feaa8 100644
--- a/src/tint/transform/zero_init_workgroup_memory.h
+++ b/src/tint/transform/zero_init_workgroup_memory.h
@@ -22,7 +22,7 @@
 /// ZeroInitWorkgroupMemory is a transform that injects code at the top of entry
 /// points to zero-initialize workgroup memory used by that entry point (and all
 /// transitive functions called by that entry point)
-class ZeroInitWorkgroupMemory : public Castable<ZeroInitWorkgroupMemory, Transform> {
+class ZeroInitWorkgroupMemory final : public Castable<ZeroInitWorkgroupMemory, Transform> {
   public:
     /// Constructor
     ZeroInitWorkgroupMemory();
diff --git a/src/tint/writer/msl/generator_impl_unary_op_test.cc b/src/tint/writer/msl/generator_impl_unary_op_test.cc
index 843e2a4..21d6357 100644
--- a/src/tint/writer/msl/generator_impl_unary_op_test.cc
+++ b/src/tint/writer/msl/generator_impl_unary_op_test.cc
@@ -81,16 +81,15 @@
     EXPECT_EQ(out.str(), "tint_unary_minus(expr)");
 }
 
-TEST_F(MslUnaryOpTest, NegationOfIntMin) {
-    auto* op = create<ast::UnaryOpExpression>(ast::UnaryOp::kNegation,
-                                              Expr(i32(std::numeric_limits<int32_t>::min())));
+TEST_F(MslUnaryOpTest, IntMin) {
+    auto* op = Expr(i32(std::numeric_limits<int32_t>::min()));
     WrapInFunction(op);
 
     GeneratorImpl& gen = Build();
 
     std::stringstream out;
     ASSERT_TRUE(gen.EmitExpression(out, op)) << gen.error();
-    EXPECT_EQ(out.str(), "tint_unary_minus((-2147483647 - 1))");
+    EXPECT_EQ(out.str(), "(-2147483647 - 1)");
 }
 
 }  // namespace